From 5848e903378482a8ac1b376dac92b5d2ef70b5ef Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Mon, 3 Aug 2026 17:03:28 +0200 Subject: [PATCH 1/8] fix(ci): desktop-native-linux honors the classifier it depends on (#1151) The job declared needs: classify but never read the verdict, so it built a .deb and signed updater on every PR (20/20 in the last-20 audit, ~41 min). It now runs only when the classifier says either scaffold tier runs, short-circuiting to the scaffold-static skipped-by-policy pattern otherwise so it stays eligible as a required check. Fail-closed: a failed classify still forces a full run; a dedicated needs_desktop signal is #1152 scope. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVEZJ1CBtRYNXzGQRZrTat --- .github/workflows/e2e-cli.yml | 47 +++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e-cli.yml b/.github/workflows/e2e-cli.yml index c7ba8b8be..98f7d7b7d 100644 --- a/.github/workflows/e2e-cli.yml +++ b/.github/workflows/e2e-cli.yml @@ -25,15 +25,18 @@ name: e2e-cli # any other PR carrying the `e2e-cli-gate` label, PLUS manual dispatch. # # Skip policy (see `.github/scripts/ci-classify-changes.ts`): the `classify` job -# derives `run_static` / `run_runtime` from the PR diff and labels. Both expensive -# jobs still START and report SUCCESS, but short-circuit to a "skipped-by-policy" -# step when their work is not needed: +# derives `run_static` / `run_runtime` from the PR diff and labels. All three +# expensive jobs still START and report SUCCESS, but short-circuit to a +# "skipped-by-policy" step when their work is not needed: # # - docs-only diff (only `**/*.md(x)`, `docs/`, `.llm/`, `.agents/`, `.claude/`, # and never `packages/`/`plugins/`/`apps/`/`deno.json*`/`deno.lock`/ # `.github/workflows/`) -> skip both. # - `ci:skip-scaffold` -> skip scaffold-static; `ci:skip-e2e` -> skip # scaffold-runtime; `ci:full` -> force both regardless. +# - `desktop-native-linux` has no dedicated classifier output yet (#1152 adds +# `needs_desktop`); until then it runs whenever EITHER scaffold tier runs and +# skips only when the classifier said neither is needed (#1151). # # 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. The @@ -254,28 +257,52 @@ jobs: desktop-native-linux: name: desktop-native-linux (deb + signed updater) needs: classify + # Job always starts (so its status reports); real work is guarded by the + # classifier exactly like its two siblings (#1151). No dedicated desktop + # classifier output exists yet (#1152 adds `needs_desktop`), so the + # conservative proxy is: run whenever EITHER scaffold tier runs, skip only + # when the classifier said neither is needed. + # 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_static=false` AND `run_runtime=false`. `skipped` classify + # (applicability gate) keeps the old skip behavior. if: ${{ !cancelled() && needs.classify.result != 'skipped' }} runs-on: ubuntu-latest timeout-minutes: 30 + env: + RUN: ${{ needs.classify.result != 'success' || needs.classify.outputs.run_static == 'true' || needs.classify.outputs.run_runtime == '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::desktop-native-linux skipped by policy. %s\n' "$SKIP_REASON" + - name: Checkout + if: env.RUN == 'true' uses: actions/checkout@v4 - name: Setup Deno + if: env.RUN == 'true' uses: denoland/setup-deno@v2 with: deno-version: "2.9.0" - name: Install native packaging and desktop prerequisites + if: env.RUN == 'true' run: >- sudo apt-get update && sudo apt-get install --yes bsdiff libgtk-3-0 libwebkit2gtk-4.1-0 - name: Install workspace dependencies + if: env.RUN == 'true' run: deno install - name: Native desktop package, install, update, and rollback (one pass) id: native_suite + if: env.RUN == 'true' # TEMPORARY NON-BLOCKING POLICY: remove step tolerance when the packaged # runtime ships the #841 verification op. See the G7 drift entry # "Packaged runtime lacks the #841 verification op" for fail-closed evidence. @@ -283,7 +310,7 @@ jobs: run: deno task e2e:cli run deploy.desktop-native --cleanup --format pretty - name: Summarize native desktop upstream gap (LOUD, never masks suite evidence) - if: always() + if: always() && env.RUN == 'true' env: EVIDENCE: .llm/tmp/desktop-native-e2e/evidence.json NATIVE_SUITE_OUTCOME: ${{ steps.native_suite.outcome }} @@ -309,7 +336,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: Upload native desktop evidence - if: always() + if: always() && env.RUN == 'true' uses: actions/upload-artifact@v4 with: name: e2e-cli-desktop-native-linux-report @@ -353,6 +380,15 @@ jobs: static_outcome="$(describe_scaffold_lane "$RUN_STATIC" "$STATIC_RESULT")" runtime_outcome="$(describe_scaffold_lane "$RUN_RUNTIME" "$RUNTIME_RESULT")" + # Desktop has no dedicated classifier output yet (#1152): it is + # selected whenever either scaffold tier is (#1151). + if [ "$RUN_STATIC" = "true" ] || [ "$RUN_RUNTIME" = "true" ]; then + desktop_selected="true" + else + desktop_selected="false" + fi + desktop_outcome="$(describe_scaffold_lane "$desktop_selected" "$DESKTOP_NATIVE_LINUX_RESULT")" + { echo "## Scaffold CI lane visibility" echo @@ -363,4 +399,5 @@ jobs: printf '| `classify` | %s |\n' "$classify_outcome" printf '| `scaffold-static` | %s |\n' "$static_outcome" printf '| `scaffold-runtime` | %s |\n' "$runtime_outcome" + printf '| `desktop-native-linux` | %s |\n' "$desktop_outcome" } >> "$GITHUB_STEP_SUMMARY" From d79c98538fdeb28b225c6bb731440ecfde01b8ba Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Mon, 3 Aug 2026 17:10:27 +0200 Subject: [PATCH 2/8] chore(harness): bootstrap ci-scope-expensive-jobs--1152 run (research + plan for #1152) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVEZJ1CBtRYNXzGQRZrTat --- .../context-pack.md | 15 +++ .../ci-scope-expensive-jobs--1152/drift.md | 8 ++ .../ci-scope-expensive-jobs--1152/plan.md | 104 ++++++++++++++++++ .../ci-scope-expensive-jobs--1152/research.md | 66 +++++++++++ .../supervisor.md | 15 +++ .../ci-scope-expensive-jobs--1152/worklog.md | 14 +++ 6 files changed, 222 insertions(+) create mode 100644 .llm/runs/ci-scope-expensive-jobs--1152/context-pack.md create mode 100644 .llm/runs/ci-scope-expensive-jobs--1152/drift.md create mode 100644 .llm/runs/ci-scope-expensive-jobs--1152/plan.md create mode 100644 .llm/runs/ci-scope-expensive-jobs--1152/research.md create mode 100644 .llm/runs/ci-scope-expensive-jobs--1152/supervisor.md create mode 100644 .llm/runs/ci-scope-expensive-jobs--1152/worklog.md diff --git a/.llm/runs/ci-scope-expensive-jobs--1152/context-pack.md b/.llm/runs/ci-scope-expensive-jobs--1152/context-pack.md new file mode 100644 index 000000000..d48b5e93a --- /dev/null +++ b/.llm/runs/ci-scope-expensive-jobs--1152/context-pack.md @@ -0,0 +1,15 @@ +# Context pack — ci-scope-expensive-jobs--1152 + +**State**: plan written, awaiting PLAN-EVAL. No implementation slices landed. + +- Branch `ci/scope-expensive-jobs` (base `origin/main`), worktree `/home/codex/repos/ns-ci-scope`. +- #1151 already shipped separately: PR #1153, verified by run 30825776156 (docs-only demo → + desktop skipped-by-policy). Not part of this run's remaining scope. +- Read order to resume: `supervisor.md` → `research.md` → `plan.md` (D1–D5 decisions, S1–S6 + slices, open questions 1–4). +- Next actions: (1) dispatch PLAN-EVAL on the open-model evaluator lane; (2) on PASS, implement + S1→S6 sequentially on this branch, one commit+push+PR comment per slice; (3) before/after + measurement posted on #1152. +- Hard constraints: label set frozen (`ci:full`/`ci:skip-scaffold`/`ci:skip-e2e`), skipped jobs + still report, unrecognised path ⇒ whole vector true, no routing tables in YAML, no + docker/scaffold runs from this shared machine. diff --git a/.llm/runs/ci-scope-expensive-jobs--1152/drift.md b/.llm/runs/ci-scope-expensive-jobs--1152/drift.md new file mode 100644 index 000000000..055275df2 --- /dev/null +++ b/.llm/runs/ci-scope-expensive-jobs--1152/drift.md @@ -0,0 +1,8 @@ +# Drift — ci-scope-expensive-jobs--1152 (append-only) + +- 2026-08-03 `minor` — #1151 was executed outside a run dir, per the owner brief's explicit + "you may just fix, verify and push" waiver; its trail lives on PR #1153 (body + evidence + comment) instead of harness artifacts. +- 2026-08-03 `minor` — PLAN-EVAL not yet dispatched: the evaluator lane (separate open-model + session per `lane-policy.md`) is supervisor-triggered; this session stopped at the plan report + as the brief requires ("Report your plan before implementing #1152"). diff --git a/.llm/runs/ci-scope-expensive-jobs--1152/plan.md b/.llm/runs/ci-scope-expensive-jobs--1152/plan.md new file mode 100644 index 000000000..2cf55c150 --- /dev/null +++ b/.llm/runs/ci-scope-expensive-jobs--1152/plan.md @@ -0,0 +1,104 @@ +# Plan — ci-scope-expensive-jobs--1152 (status: awaiting PLAN-EVAL) + +Closes #1152. Owner-ratified constraints are locked (see `supervisor.md`); this plan decides only +mechanism and rollout. + +## Locked design decisions + +### D1 — One classifier, five outputs (capability vector) + +`decide()` grows from `{run_static, run_runtime}` to also emit `needs_deno`, `needs_docker`, +`needs_desktop`, `needs_docs`, `needs_surface`. One pure function, one test file, many workflow +consumers. **Safety invariant (unit-tested):** an unrecognised (non-docs, non-listed) path sets the +ENTIRE vector true — every output fails toward running. `ci:full` forces the entire vector true. +`ci:skip-scaffold`/`ci:skip-e2e` keep exactly their current semantics (scaffold tiers only); they +never widen to `check-test`/`quality`. No other labels — routing stays in tested code. + +### D2 — Output semantics (v1, deliberately wide) + +| Output | v1 rule | Consumers | +| --- | --- | --- | +| `needs_deno` | `NOT docs_only` — any non-docs-only path | `check-test`, `quality` (ci.yml) | +| `needs_docker` | any `packages/**`/`plugins/**`/`apps/**`, tier-defining workflow, toolchain `deno.json*`, any `deno.lock` | `scaffold-runtime` | +| `needs_desktop` | `packages/cli/**`, tier-defining workflow, toolchain `deno.json*`, `deno.lock` | `desktop-native-linux` | +| `needs_docs` | any `docs/**` or `**/*.md(x)` change | docs-accuracy/tagline steps in `quality` | +| `needs_surface` | any `packages/**` change (mirrors today's `paths:` filter) | `surface-diff` | + +- `needs_deno = !docsOnly` is the maximally conservative start: the only PRs that skip the required + trio's work are the pure docs/agent-context class (5/20 in the audit, ~10 min each). Tightening + (e.g. exempting non-tier workflow edits) comes later, against observed green history only. +- `needs_docker` v1 keeps ALL of `packages/**` — per the ratified "start deliberately wide, tighten + only against observed green" rule, the v1 docker win is **precision on workflows and + `deno.json`** (the measured #1122 waste), not package-set guessing. Package-level tightening + (e.g. `fresh-ui`-only diffs) is a follow-up driven by the post-landing measurement, recorded on + #1152. +- `needs_desktop` is the narrow one v1 can afford: the `.deb`/updater surface is entirely + `packages/cli` (deploy feature + e2e gates). Everything else skips desktop. This supersedes the + #1151 proxy (`run_static || run_runtime`). + +### D3 — Precision fixes from #1122 + +- `.github/workflows/` stops escalating wholesale. `TIER_DEFINING_WORKFLOWS = e2e-cli.yml, ci.yml` + (+ `.github/scripts/**`, which stays impacting-by-fallback and gets a pinning test) escalate the + scaffold vector. Other workflow edits (`release-canary.yml`, `pages.yml`, `publish.yml`, …) set + `needs_deno` only (conservative; they are still not docs). +- `deno.json`/`deno.jsonc` gets structural discrimination: a new pure function + `classifyDenoConfigChange(oldText, newText)` → `tasks-only` | `toolchain`. `tasks-only` (only the + `tasks` key differs) ⇒ `needs_deno` only. Anything else — `imports`, `workspace`, + `compilerOptions`, unparseable, file added/deleted — ⇒ full vector. The classify job feeds + base/head file contents via `git show BASE:deno.json` / `git show HEAD:deno.json`; absent or + unreadable content ⇒ `toolchain` (fail toward running). `deno.lock` stays unconditionally + impacting. + +### D4 — Consumers reuse the ONE skip pattern + +Every newly gated job copies `scaffold-static`'s exact pattern: job always starts, +`RUN: ${{ needs.classify.result != 'success' || needs.classify.outputs. == 'true' }}`, +"Skipped by policy" step, every real step guarded by `env.RUN == 'true'`. Required checks +(`check-test`, `quality`) keep reporting SUCCESS; no `paths:`/job-`if:` skips, no second pattern, +no routing tables in YAML. `ci.yml` gets its own `classify` job running the same script (non-PR +events — push to main — classify as "run everything", preserving today's push behavior). +`surface-diff.yml` drops its `paths:` filter for a classify+`needs_surface` gate, making it +eligible as a required check. + +### D5 — "Invert the docker default" without a third suite + +The deno-only default tier IS `scaffold-static` (it exists, runs green, and covers scaffold + +registry + typecheck). v1 therefore implements the split as: `run_runtime := needs_docker` +(docker becomes the exception), `run_static` unchanged (any impacting change). No new e2e suite and +no `packages/cli/e2e` surgery in this PR — inventing a third "runtime-deno" job would duplicate +scaffold-static and drag framework-source work into a CI-tooling PR (wrong lane per CLAUDE.md). +**Flagged for PLAN-EVAL/owner:** if a genuine third tier is wanted later, it is a separate +WSL-Codex slice on `packages/cli/e2e`. + +## Slices (sequential, one branch, one commit each) + +- **S1** — classifier: emit the five outputs with v1-wide semantics; no workflow consumer changes. + Tests: per-output positive AND negative cases, unknown-path-forces-all, ci:full-forces-all, + `.github/scripts/**`-forces-all. +- **S2** — classifier precision: `TIER_DEFINING_WORKFLOWS` + `classifyDenoConfigChange`. Tests: + #1122 replay (`release-canary.yml` + tasks-only `deno.json` ⇒ scaffold vector false, + `needs_deno` true), tier workflow ⇒ all true, unparseable/deleted `deno.json` ⇒ all true. + Settle the `.llm/tools/**/*.ts` edge (research item 1) empirically; encode + test the verdict. +- **S3** — `e2e-cli.yml`: `scaffold-runtime` reads `run_runtime` (now docker-gated), desktop reads + `needs_desktop` (replacing the #1151 proxy), lane-visibility updated. +- **S4** — `ci.yml`: classify job + `needs_deno` gates on `check-test`/`quality` with + skipped-by-policy; `needs_docs` step-gates for docs-accuracy/tagline; lane-visibility updated. +- **S5** — `surface-diff.yml`: fold the paths filter into `needs_surface`. +- **S6** — live verification (stacked demo PRs w/ `e2e-cli-gate`: docs-only ⇒ all skip; + release-workflow-only ⇒ scaffold/docker/desktop skip) + before/after measurement posted on + #1152 + workflow header docs. + +Each slice: commit → push → PR comment with evidence; classifier test run is the per-slice gate +(`deno test .github/scripts/ci-classify-changes.test.ts`); no docker/scaffold runs from this +shared machine — live verification rides GitHub Actions via the stacked-demo technique. + +## Risks / open questions for PLAN-EVAL + +1. **D5 interpretation** of the "splits into a deno-only default tier" acceptance box (recommended: + scaffold-static is that tier; no third suite in this PR). +2. **`needs_deno` for non-tier workflow edits** (v1 keeps true — costs ~9 min on rare + workflow-only PRs, safe; tightening is data-driven later). +3. **`ci.yml` consumers cannot be demoed pre-merge** (branch-filtered trigger) — verified by + expression parity with the proven e2e-cli pattern + first post-merge docs-only PR. +4. `deps-report` and `close-gate` stay ungated (cheap/informational or must-always-run). diff --git a/.llm/runs/ci-scope-expensive-jobs--1152/research.md b/.llm/runs/ci-scope-expensive-jobs--1152/research.md new file mode 100644 index 000000000..1ebd1ef18 --- /dev/null +++ b/.llm/runs/ci-scope-expensive-jobs--1152/research.md @@ -0,0 +1,66 @@ +# Research — ci-scope-expensive-jobs--1152 + +Sources: issues #1151/#1152 (owner-authored, measured over the last 20 closed PRs), owner brief +`.llm/tmp/BRIEF.md`, `#1122` worked example (owner comment on #1152), direct reads of +`.github/workflows/{ci,e2e-cli,surface-diff}.yml` and `.github/scripts/ci-classify-changes.ts`. + +## Measured baseline (last 20 closed PRs, from #1152) + +| Job | Gate today | Total | Ran on | +| --- | --- | --- | --- | +| `check-test` | none | ~138 min | 20/20 | +| `scaffold-runtime` | classifier | ~128 min | 15/20 | +| `scaffold-static` | classifier | ~46 min | 15/20 | +| `desktop-native-linux` | declared, not honored (#1151, fixed in PR #1153) | ~41 min | 20/20 | +| `quality` | none | ~38 min | 20/20 | + +Five of twenty PRs touched no shipping code and still paid ~10 min each. + +## Current mechanics + +- `ci-classify-changes.ts`: pure `decide()` emits `run_static`/`run_runtime`/`docs_only`/`reason`. + Conservative: unrecognised path ⇒ impacting ⇒ run. Consumed only by `e2e-cli.yml`. +- `e2e-cli.yml`: `classify` job → `scaffold-static`/`scaffold-runtime` read `env.RUN` with the + fail-closed pattern (`classify.result != 'success' || run_* == 'true'`), short-circuiting to a + "Skipped by policy" step that still reports SUCCESS. `desktop-native-linux` now reads the proxy + `run_static || run_runtime` (PR #1153). +- `ci.yml`: `check-test` and `quality` have **no** condition; both are required checks via the + `main-branch-protection` ruleset, so they must keep reporting even when skipped. `deps-report` is + informational. Triggers include `push` to main (no PR diff ⇒ classifier already runs everything + for non-PR events). +- `surface-diff.yml`: workflow-level `paths: packages/**` filter — job does not report at all on + non-matching PRs (cannot become a required check as-is). + +## The #1122 finding (precision failure, not just cost) + +`IMPACTING_PREFIXES` contains `.github/workflows/` wholesale and `IMPACTING_EXACT` contains +`deno.json`. PR #1122 edited `release-canary.yml` + added a `deno.json` task + `.llm/tools/release` +scripts — zero `packages/`/`plugins/` — and paid 2× the full aspire+docker+postgres suite +(including a 15-min timeout) plus desktop. Only `e2e-cli.yml` and `ci.yml` *define* the scaffold +tiers; `release-canary.yml`, `pages.yml`, `publish.yml` etc. cannot affect them by construction. +Likewise a `tasks`-only `deno.json` edit is not a toolchain change. + +## Edges found during re-baseline (must be settled in implementation) + +1. **`.llm/tools/**/*.ts` is classified docs-only today** (`.llm/` is a `DOCS_PREFIXES` entry), yet + root `deno task test` (`deno test --allow-all` from repo root) may discover `.llm/tools` + test files, and several `deno task` gates execute `.llm/tools` scripts. If root test discovery + includes them, non-md files under `.llm/tools/` must set `needs_deno`. Verify empirically; + default toward running. +2. **`.github/scripts/**` (the classifier itself)** is unclassified today ⇒ impacting by fallback. + Keep that behavior under the redesign and pin it with an explicit unit test — a classifier edit + must force everything. +3. **Required-check inventory**: `check-test` and `quality` are ruleset-required; `scaffold-static` + intends to become required. Every newly gated job must keep the start-and-report-SUCCESS + pattern. +4. **`desktop` path surface**: desktop packaging/deploy code lives entirely under + `packages/cli/src/public/features/deploy/` (+ its e2e gates under `packages/cli/e2e/`); no other + package participates in the `.deb`/updater build. + +## Verification technique proven on #1151 + +A stacked demo PR against the feature branch carrying the `e2e-cli-gate` label runs the edited +workflow (pull_request events use the merged workflow file) — run 30825776156 proved the +skipped-by-policy path live before merge. Reusable for #1152's negative cases. `ci.yml` cannot be +exercised this way (its `pull_request` trigger is branch-filtered to main/feat/epic), so its +consumers are verified by expression-parity review pre-merge + first docs-only PR post-merge. diff --git a/.llm/runs/ci-scope-expensive-jobs--1152/supervisor.md b/.llm/runs/ci-scope-expensive-jobs--1152/supervisor.md new file mode 100644 index 000000000..74a4f0432 --- /dev/null +++ b/.llm/runs/ci-scope-expensive-jobs--1152/supervisor.md @@ -0,0 +1,15 @@ +# Supervisor — ci-scope-expensive-jobs--1152 + +- **Model / session**: Claude Fable 5 (Claude Code), session `session_01PVEZJ1CBtRYNXzGQRZrTat` +- **Host / worktree**: WSL2, `/home/codex/repos/ns-ci-scope` +- **Branch**: `ci/scope-expensive-jobs` (base `origin/main`) +- **Scope**: #1152 — CI capability-vector redesign. `.github/scripts/` + workflow YAML only; no + `packages/`/`plugins/` source. +- **Related**: #1151 shipped separately as PR #1153 (branch `fix/desktop-native-honors-classifier`). +- **Lanes**: This session generates research/plan and (post PLAN-EVAL PASS) implements — CI tooling, + not framework source, per the owner brief `.llm/tmp/BRIEF.md` which delegates both issues to this + session. PLAN-EVAL / IMPL-EVAL on the open-model evaluator lane per + `.llm/harness/workflow/lane-policy.md`; the supervisor triggers, never auto-dispatched. +- **Constraints (owner-ratified, not relitigable)**: paths are the mechanism, labels are the + override; label set stays exactly `ci:full`/`ci:skip-scaffold`/`ci:skip-e2e`; skipped jobs must + still report; unrecognised paths always force the gate to run. diff --git a/.llm/runs/ci-scope-expensive-jobs--1152/worklog.md b/.llm/runs/ci-scope-expensive-jobs--1152/worklog.md new file mode 100644 index 000000000..7ac620300 --- /dev/null +++ b/.llm/runs/ci-scope-expensive-jobs--1152/worklog.md @@ -0,0 +1,14 @@ +# Worklog — ci-scope-expensive-jobs--1152 + +## 2026-08-03 — Bootstrap + Research + Plan + +- Read #1151/#1152 in full + owner brief (`.llm/tmp/BRIEF.md`). +- #1151 shipped separately first, per the brief: PR #1153 + (`fix/desktop-native-honors-classifier`, commit 5848e903), desktop-native-linux gated on + `run_static || run_runtime` with the scaffold-static skipped-by-policy pattern. Verified live via + stacked docs-only demo PR #1154 → run 30825776156: desktop reported SUCCESS with only the + "Skipped by policy" step. Acceptance evidence mirrored on PR #1153; demo PR closed, branch + deleted. Classifier untouched; its 30 unit tests green locally. +- Wrote `research.md` (baseline measurements, #1122 precision failure, required-check inventory, + `.llm/tools` classification edge) and `plan.md` (capability vector D1–D5, slices S1–S6). +- **Design checkpoint**: plan locked pending PLAN-EVAL. No implementation before PASS. From 99bdd37803def1726d5c23ec8e2de7b4d9c59952 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Mon, 3 Aug 2026 17:25:16 +0200 Subject: [PATCH 3/8] feat(ci): classifier emits a capability vector with #1122 precision (#1152 S1+S2) decide() now emits needs_deno/needs_docker/needs_desktop/needs_docs/ needs_surface alongside run_static/run_runtime. Only the tier-defining workflows (e2e-cli.yml, ci.yml) escalate the scaffold tiers; a root deno.json diff touching only tasks is a script alias, not a toolchain change. Unrecognised paths force the ENTIRE vector true, root deno test discovery keeps .llm/tools code on needs_deno, and every output carries positive and negative unit coverage (50 tests). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVEZJ1CBtRYNXzGQRZrTat --- .github/scripts/ci-classify-changes.test.ts | 312 +++++++++++++++- .github/scripts/ci-classify-changes.ts | 392 +++++++++++++++----- 2 files changed, 606 insertions(+), 98 deletions(-) diff --git a/.github/scripts/ci-classify-changes.test.ts b/.github/scripts/ci-classify-changes.test.ts index e445289c6..ba77cb7cb 100644 --- a/.github/scripts/ci-classify-changes.test.ts +++ b/.github/scripts/ci-classify-changes.test.ts @@ -1,6 +1,9 @@ import { assertEquals } from 'jsr:@std/assert@^1.0.0'; import { + classifyDenoConfigChange, + classifyPath, decide, + type Decision, isDocsOnlyPath, isImpacting, parseFiles, @@ -9,6 +12,19 @@ import { sanitizeReason, } from './ci-classify-changes.ts'; +function vector(d: Decision) { + return { + deno: d.needsDeno, + docker: d.needsDocker, + desktop: d.needsDesktop, + docs: d.needsDocs, + surface: d.needsSurface, + }; +} + +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 }; + // ── rename-hole regression (adversarial review, defect 1) ──────────────────── Deno.test('regression: packages/cli/a.ts -> docs/a.md rename is NOT docs-only', () => { @@ -55,6 +71,8 @@ Deno.test('rename of a Markdown file under packages stays docs-only', () => { assertEquals(d.docsOnly, true); assertEquals(d.runStatic, false); assertEquals(d.runRuntime, false); + // Mirrors the old surface-diff `paths: packages/**` trigger. + assertEquals(d.needsSurface, true); }); Deno.test('parseNameStatus: unrecognisable line degrades to a bare path (forces run)', () => { @@ -98,6 +116,8 @@ Deno.test('reason with a GITHUB_OUTPUT-injection filename stays one line', () => assertEquals(d.runStatic, true); }); +// ── per-path classification ────────────────────────────────────────────────── + Deno.test('docs surfaces are docs-only', () => { for ( const p of [ @@ -125,7 +145,7 @@ Deno.test('critical workflow paths win over the markdown allowlist', () => { assertEquals(isDocsOnlyPath('.github/workflows/README.md'), false); }); -Deno.test('impacting surfaces force the gate', () => { +Deno.test('impacting surfaces force the scaffold gate', () => { for ( const p of [ 'packages/cli/e2e/cli.ts', @@ -137,6 +157,7 @@ Deno.test('impacting surfaces force the gate', () => { 'deno.lock', 'examples/x/deno.json', '.github/workflows/e2e-cli.yml', + '.github/workflows/ci.yml', ] ) { assertEquals(isImpacting(p), true, `expected impacting: ${p}`); @@ -150,6 +171,273 @@ Deno.test('unknown root paths force the gate (conservative default)', () => { assertEquals(isDocsOnlyPath('.gitignore'), false); }); +Deno.test('SAFETY: an unrecognised path forces EVERY output true', () => { + for (const p of ['tools/foo.sh', 'scripts/x.py', '.github/labels.yml', 'Makefile']) { + const caps = classifyPath(p); + assertEquals(caps, { + scaffold: true, + docker: true, + desktop: true, + deno: true, + docs: true, + surface: true, + }, `expected full escalation for: ${p}`); + const d = decide({ eventName: 'pull_request', files: [p], labels: [] }); + assertEquals(d.runStatic, true, p); + assertEquals(d.runRuntime, true, p); + assertEquals(vector(d), ALL_TRUE, p); + } +}); + +Deno.test('SAFETY: the classifier own sources force everything (.github/scripts)', () => { + const d = decide({ + eventName: 'pull_request', + files: ['.github/scripts/ci-classify-changes.ts'], + labels: [], + }); + assertEquals(d.runStatic, true); + assertEquals(d.runRuntime, true); + assertEquals(vector(d), ALL_TRUE); +}); + +// ── tier-defining vs non-tier workflows (#1122 precision) ──────────────────── + +Deno.test('tier-defining workflow edits escalate scaffold, docker and desktop', () => { + for (const wf of ['.github/workflows/e2e-cli.yml', '.github/workflows/ci.yml']) { + const d = decide({ eventName: 'pull_request', files: [wf], labels: [] }); + assertEquals(d.runStatic, true, wf); + assertEquals(d.runRuntime, true, wf); + assertEquals(d.needsDocker, true, wf); + assertEquals(d.needsDesktop, true, wf); + assertEquals(d.needsDeno, true, wf); + } +}); + +Deno.test('NEGATIVE: non-tier workflow edits set needs_deno ONLY', () => { + for ( + const wf of [ + '.github/workflows/release-canary.yml', + '.github/workflows/pages.yml', + '.github/workflows/publish.yml', + '.github/workflows/surface-diff.yml', + ] + ) { + const d = decide({ eventName: 'pull_request', files: [wf], labels: [] }); + assertEquals(d.runStatic, false, wf); + assertEquals(d.runRuntime, false, wf); + assertEquals(vector(d), { ...ALL_FALSE, deno: true }, wf); + } +}); + +// ── root deno.json discrimination (#1122 precision) ────────────────────────── + +const DENO_BASE = JSON.stringify({ + version: '0.0.3', + workspace: ['packages/*'], + imports: { '@std/assert': 'jsr:@std/assert@^1.0.0' }, + tasks: { check: 'deno check .' }, +}); +const DENO_TASKS_ONLY = JSON.stringify({ + version: '0.0.3', + workspace: ['packages/*'], + imports: { '@std/assert': 'jsr:@std/assert@^1.0.0' }, + tasks: { 'check': 'deno check .', 'release:canary-label': 'deno run x.ts' }, +}); +const DENO_TOOLCHAIN = JSON.stringify({ + version: '0.0.3', + workspace: ['packages/*', 'plugins/*'], + imports: { '@std/assert': 'jsr:@std/assert@^1.0.0' }, + tasks: { check: 'deno check .' }, +}); + +Deno.test('classifyDenoConfigChange: tasks-only vs toolchain vs unparseable', () => { + assertEquals(classifyDenoConfigChange(DENO_BASE, DENO_TASKS_ONLY), 'tasks-only'); + assertEquals(classifyDenoConfigChange(DENO_BASE, DENO_BASE), 'tasks-only'); + assertEquals(classifyDenoConfigChange(DENO_BASE, DENO_TOOLCHAIN), 'toolchain'); + assertEquals(classifyDenoConfigChange(DENO_BASE, '{not json'), 'toolchain'); + assertEquals(classifyDenoConfigChange(undefined, DENO_BASE), 'toolchain'); + assertEquals(classifyDenoConfigChange(DENO_BASE, undefined), 'toolchain'); + assertEquals(classifyDenoConfigChange('', DENO_BASE), 'toolchain'); + assertEquals(classifyDenoConfigChange(DENO_BASE, '[1,2]'), 'toolchain'); +}); + +Deno.test('#1122 REPLAY: release workflow + tasks-only deno.json + .llm tools stays off the scaffold tiers', () => { + const d = decide({ + eventName: 'pull_request', + files: [ + '.github/workflows/release-canary.yml', + 'deno.json', + '.llm/tools/release/canary-label.ts', + '.llm/tools/generate-publish-assets.ts', + '.llm/runs/some-run/worklog.md', + ], + labels: [], + rootDenoConfig: { base: DENO_BASE, head: DENO_TASKS_ONLY }, + }); + assertEquals(d.runStatic, false); + assertEquals(d.runRuntime, false); + assertEquals(d.needsDocker, false); + assertEquals(d.needsDesktop, false); + // check-test still runs: root `deno test` discovers `.llm/tools` tests and + // `.llm/tools/release/release-canary-workflow_test.ts` reads the workflow. + assertEquals(d.needsDeno, true); + assertEquals(d.docsOnly, false); +}); + +Deno.test('root deno.json toolchain change (workspace/imports) escalates everything', () => { + const d = decide({ + eventName: 'pull_request', + files: ['deno.json'], + labels: [], + rootDenoConfig: { base: DENO_BASE, head: DENO_TOOLCHAIN }, + }); + assertEquals(d.runStatic, true); + assertEquals(d.runRuntime, true); + assertEquals(d.needsDesktop, true); + assertEquals(d.needsDeno, true); +}); + +Deno.test('NEGATIVE: root deno.json with NO base/head content escalates (fail toward running)', () => { + const d = decide({ eventName: 'pull_request', files: ['deno.json'], labels: [] }); + assertEquals(d.runStatic, true); + assertEquals(d.runRuntime, true); + assertEquals(d.needsDesktop, true); +}); + +Deno.test('deno.lock always escalates (tasks-only discrimination never applies)', () => { + const d = decide({ + eventName: 'pull_request', + files: ['deno.lock'], + labels: [], + rootDenoConfig: { base: DENO_BASE, head: DENO_TASKS_ONLY }, + }); + assertEquals(d.runStatic, true); + assertEquals(d.runRuntime, true); + assertEquals(d.needsDesktop, true); +}); + +Deno.test('nested workspace deno.json escalates regardless of root discrimination', () => { + const d = decide({ + eventName: 'pull_request', + files: ['examples/x/deno.json'], + labels: [], + rootDenoConfig: { base: DENO_BASE, head: DENO_TASKS_ONLY }, + }); + assertEquals(d.runStatic, true); + assertEquals(d.runRuntime, true); +}); + +// ── capability vector per source area ──────────────────────────────────────── + +Deno.test('packages (non-cli) code: deno+docker+scaffold+surface, NOT desktop', () => { + const d = decide({ + eventName: 'pull_request', + files: ['packages/database/src/mod.ts'], + labels: [], + }); + assertEquals(d.runStatic, true); + assertEquals(d.runRuntime, true); + assertEquals(vector(d), { deno: true, docker: true, desktop: false, docs: false, surface: true }); +}); + +Deno.test('packages/cli code: the .deb surface -> needs_desktop', () => { + const d = decide({ + eventName: 'pull_request', + files: ['packages/cli/src/public/features/deploy/target/desktop/release/release-group.ts'], + labels: [], + }); + assertEquals(d.needsDesktop, true); + assertEquals(d.needsDocker, true); +}); + +Deno.test('plugins code: docker yes, desktop/surface no', () => { + const d = decide({ + eventName: 'pull_request', + files: ['plugins/workers/mod.ts'], + labels: [], + }); + assertEquals(d.runRuntime, true); + assertEquals(vector(d), { + deno: true, + docker: true, + desktop: false, + docs: false, + surface: false, + }); +}); + +Deno.test('NEGATIVE: agent-context-only change (.agents one-file PR, #1055) skips everything', () => { + const d = decide({ + eventName: 'pull_request', + files: ['.agents/skills/netscript-pr/SKILL.md'], + labels: [], + }); + assertEquals(d.docsOnly, true); + assertEquals(d.runStatic, false); + assertEquals(d.runRuntime, false); + assertEquals(vector(d), ALL_FALSE); +}); + +Deno.test('docs/ markdown: needs_docs only', () => { + const d = decide({ + eventName: 'pull_request', + files: ['docs/site/index.md'], + labels: [], + }); + assertEquals(d.docsOnly, true); + assertEquals(vector(d), { ...ALL_FALSE, docs: true }); +}); + +Deno.test('package README: docs + surface, no toolchain', () => { + const d = decide({ + eventName: 'pull_request', + files: ['packages/cli/README.md'], + labels: [], + }); + assertEquals(d.docsOnly, true); + assertEquals(d.runStatic, false); + assertEquals(vector(d), { + deno: false, + docker: false, + desktop: false, + docs: true, + surface: true, + }); +}); + +Deno.test('.llm/tools code files set needs_deno (root deno test discovers them)', () => { + const d = decide({ + eventName: 'pull_request', + files: ['.llm/tools/release/canary-label.ts'], + labels: [], + }); + assertEquals(d.docsOnly, false); + assertEquals(d.runStatic, false); + assertEquals(d.runRuntime, false); + assertEquals(vector(d), { ...ALL_FALSE, deno: true }); +}); + +Deno.test('NEGATIVE: .llm non-code files stay fully skipped', () => { + const d = decide({ + eventName: 'pull_request', + files: ['.llm/runs/x/worklog.md', '.llm/harness/workflow/lane-policy.md'], + labels: [], + }); + assertEquals(d.docsOnly, true); + assertEquals(vector(d), ALL_FALSE); +}); + +Deno.test('docs/ code files set docs AND deno', () => { + const d = decide({ + eventName: 'pull_request', + files: ['docs/site/build.ts'], + labels: [], + }); + assertEquals(vector(d), { ...ALL_FALSE, deno: true, docs: true }); +}); + +// ── decide: aggregate + labels ─────────────────────────────────────────────── + Deno.test('decide: docs-only PR skips both jobs', () => { const d = decide({ eventName: 'pull_request', @@ -205,11 +493,12 @@ Deno.test('decide: one code file forces both jobs', () => { assertEquals(d.runRuntime, true); }); -Deno.test('decide: empty diff is not docs-only -> runs', () => { +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.runRuntime, true); + assertEquals(vector(d), ALL_TRUE); }); Deno.test('decide: ci:skip-e2e skips runtime only', () => { @@ -242,7 +531,20 @@ Deno.test('decide: both skip labels skip both jobs', () => { assertEquals(d.runRuntime, false); }); -Deno.test('decide: ci:full overrides docs-only', () => { +Deno.test('NEGATIVE: skip labels never widen to the vector (frozen semantics)', () => { + const d = decide({ + eventName: 'pull_request', + files: ['packages/cli/mod.ts'], + labels: ['ci:skip-scaffold', 'ci:skip-e2e'], + }); + // The scaffold tiers are label-skipped, but check-test/quality/desktop + // still see the code change. + assertEquals(d.needsDeno, true); + assertEquals(d.needsDesktop, true); + assertEquals(d.needsDocker, true); +}); + +Deno.test('decide: ci:full overrides docs-only and forces the ENTIRE vector', () => { const d = decide({ eventName: 'pull_request', files: ['docs/site/a.md'], @@ -250,6 +552,7 @@ Deno.test('decide: ci:full overrides docs-only', () => { }); assertEquals(d.runStatic, true); assertEquals(d.runRuntime, true); + assertEquals(vector(d), ALL_TRUE); }); Deno.test('decide: ci:full overrides skip labels', () => { @@ -262,10 +565,11 @@ Deno.test('decide: ci:full overrides skip labels', () => { assertEquals(d.runRuntime, true); }); -Deno.test('decide: workflow_dispatch runs both (no diff)', () => { +Deno.test('decide: workflow_dispatch runs everything (no diff)', () => { const d = decide({ eventName: 'workflow_dispatch', files: [], labels: [] }); assertEquals(d.runStatic, true); assertEquals(d.runRuntime, true); + assertEquals(vector(d), ALL_TRUE); }); Deno.test('decide: workflow_dispatch honours skip labels', () => { diff --git a/.github/scripts/ci-classify-changes.ts b/.github/scripts/ci-classify-changes.ts index 5bb16eb69..5da82a55b 100644 --- a/.github/scripts/ci-classify-changes.ts +++ b/.github/scripts/ci-classify-changes.ts @@ -1,52 +1,64 @@ /** - * CI change classifier for the expensive `e2e-cli` gate. + * CI change classifier — the single decision point for every expensive job. * - * Decides whether the two expensive jobs in `.github/workflows/e2e-cli.yml` - * (`scaffold-static`, `scaffold-runtime`) need to run for a given pull request, - * based on: + * Emits a capability vector, consumed by `.github/workflows/e2e-cli.yml`, + * `.github/workflows/ci.yml`, and `.github/workflows/surface-diff.yml` + * (#1152; paths are the mechanism, labels are the override): * - * 1. the set of changed files (docs-only detection), and - * 2. explicit opt-in / opt-out labels. + * - `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) + * - `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 + * files there count even though the dirs are docs-ish) + * - `needs_docker` -> the path-pure docker signal behind `run_runtime` + * - `needs_desktop` -> desktop-native-linux: the `.deb`/updater surface + * lives entirely under `packages/cli/` + * - `needs_docs` -> quality's docs lane: `docs/**` or Markdown outside + * the agent-context prefixes (`.llm/`, `.agents/`, + * `.claude/`) + * - `needs_surface` -> surface-diff (mirrors its old `paths: packages/**`) * - * Design contract (see PR "ci: docs-only + label-gated skip lanes"): + * Precision rules (#1122): only the tier-defining workflows (`e2e-cli.yml`, + * `ci.yml`) escalate the scaffold tiers — `release-canary.yml`, `pages.yml` + * and peers cannot affect scaffold behaviour by construction and set only + * `needs_deno`. A root `deno.json` edit that changes ONLY the `tasks` key is + * a script alias, not a toolchain change: it sets `needs_deno` only. Any + * other root config change — and any unparseable or unavailable content — + * escalates everything. * - * - Markdown / MDX is docs-only regardless of directory, except for explicit - * critical paths (`deno.json*`, `deno.lock`, workflows), which always win. - * - Non-Markdown paths under `packages/`, `plugins/`, and `apps/` remain - * impacting; known documentation/agent-context directories remain allowed. - * - `docsOnly` is true only when there is at least one changed file and every - * changed file is docs-only. - * - * Label precedence (highest first): - * 1. `ci:full` -> force BOTH jobs to run (overrides docs-only + skip). + * 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`. - * 3. docs-only -> skip both. - * 4. otherwise -> run both. + * (Skip labels keep their scaffold-tier-only semantics; they never + * widen to the required trio or desktop.) + * 3. otherwise -> paths decide. * - * The classifier is intentionally conservative: an unrecognised path forces the - * gate to run. It NEVER skips because it failed to classify. + * SAFETY PROPERTY (unit-tested): an unrecognised path forces EVERY output + * true. The classifier NEVER skips because it failed to classify. Non-PR + * events (no diff) and empty diffs also force everything. * - * This module is pure (`decide`) plus a thin CLI. The CLI reads inputs from env - * and appends job outputs to `$GITHUB_OUTPUT`. It is exercised by - * `ci-classify-changes.test.ts` and run as a self-check step in the classify - * job. + * This module is pure (`decide`) plus a thin CLI. The CLI reads inputs from + * env and appends job outputs to `$GITHUB_OUTPUT`. It is exercised by + * `ci-classify-changes.test.ts` and run as a self-check step in each + * consuming workflow's classify job. */ -/** Prefix/exact matches that force the gate to run, regardless of extension. */ -const IMPACTING_PREFIXES = [ +/** Workflows that DEFINE the scaffold tiers; editing them escalates them. */ +const TIER_DEFINING_WORKFLOWS = new Set([ + '.github/workflows/e2e-cli.yml', + '.github/workflows/ci.yml', +]); + +/** Prefixes whose non-Markdown contents are shipping code. */ +const CODE_PREFIXES = [ 'packages/', 'plugins/', 'apps/', - '.github/workflows/', ] as const; -const IMPACTING_EXACT = new Set([ - 'deno.json', - 'deno.jsonc', - 'deno.lock', -]); - /** Directory prefixes whose contents are docs / agent-context only. */ const DOCS_PREFIXES = [ 'docs/', @@ -55,51 +67,179 @@ const DOCS_PREFIXES = [ '.claude/', ] as const; +/** Agent-context prefixes: Markdown here is invisible to the docs surface. */ +const AGENT_CONTEXT_PREFIXES = [ + '.llm/', + '.agents/', + '.claude/', +] as const; + /** Extensions that are docs-only wherever they live (subject to critical paths). */ const DOCS_EXTENSIONS = ['.md', '.mdx'] as const; +/** + * Extensions the Deno toolchain checks/tests even under docs prefixes + * (root `deno test` discovers `.llm/tools/**` and `.github/scripts/**`). + */ +const CODE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'] as const; + +/** One capability contribution per changed path; OR-ed across the diff. */ +export interface Capabilities { + scaffold: boolean; + docker: boolean; + desktop: boolean; + deno: boolean; + docs: boolean; + surface: boolean; +} + +const NONE: Capabilities = { + scaffold: false, + docker: false, + desktop: false, + deno: false, + docs: false, + surface: false, +}; + +const ALL: Capabilities = { + scaffold: true, + docker: true, + desktop: true, + deno: true, + docs: true, + surface: true, +}; + /** Normalise a git path: strip a leading `./`, collapse backslashes. */ function normalise(path: string): string { return path.replace(/\\/g, '/').replace(/^\.\//, ''); } -/** Does this path force the expensive gate to run (impacting surface)? */ -export function isImpacting(rawPath: string): boolean { - const path = normalise(rawPath); - if (IMPACTING_EXACT.has(path)) return true; - // Any `deno.json*` at any depth (e.g. generated workspace roots) is impacting. +function hasExtension(path: string, extensions: readonly string[]): boolean { + return extensions.some((ext) => path.endsWith(ext)); +} + +function isDenoConfigBase(path: string): boolean { const base = path.slice(path.lastIndexOf('/') + 1); - if (base === 'deno.json' || base === 'deno.jsonc' || base === 'deno.lock') { - return true; + return base === 'deno.json' || base === 'deno.jsonc' || base === 'deno.lock'; +} + +export type DenoConfigChange = 'tasks-only' | 'toolchain'; + +/** + * Structural discrimination for the ROOT `deno.json`/`deno.jsonc` (#1122): + * a diff that touches ONLY the `tasks` key is a script alias, not a + * toolchain change. Anything else — a differing top-level key, missing or + * unparseable content, a non-object document — is `toolchain`. Fails toward + * running. + */ +export function classifyDenoConfigChange( + oldText: string | undefined, + newText: string | undefined, +): DenoConfigChange { + if (oldText === undefined || newText === undefined) return 'toolchain'; + if (oldText.trim().length === 0 || newText.trim().length === 0) return 'toolchain'; + let oldValue: unknown; + let newValue: unknown; + try { + oldValue = JSON.parse(oldText); + newValue = JSON.parse(newText); + } catch { + return 'toolchain'; } - return IMPACTING_PREFIXES.some((prefix) => path.startsWith(prefix)); + if (!isPlainObject(oldValue) || !isPlainObject(newValue)) return 'toolchain'; + const keys = new Set([...Object.keys(oldValue), ...Object.keys(newValue)]); + for (const key of keys) { + if (key === 'tasks') continue; + if (JSON.stringify(oldValue[key]) !== JSON.stringify(newValue[key])) return 'toolchain'; + } + return 'tasks-only'; } -/** Does this explicit configuration/workflow path always force the gate? */ -function isCritical(rawPath: string): boolean { - const path = normalise(rawPath); - const base = path.slice(path.lastIndexOf('/') + 1); - return path.startsWith('.github/workflows/') || - base === 'deno.json' || base === 'deno.jsonc' || base === 'deno.lock'; +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); } -/** Is this path a docs-only surface (only meaningful when not impacting)? */ -export function isDocs(rawPath: string): boolean { +/** + * Capability contribution of a single changed path. `rootDenoConfig` is the + * verdict for the root `deno.json`/`deno.jsonc`, when that file is in the + * diff. Precedence: root config -> nested config/lock -> tier workflow -> + * other workflow -> Markdown -> code prefixes -> docs prefixes -> + * unrecognised (which forces EVERYTHING). + */ +export function classifyPath( + rawPath: string, + rootDenoConfig: DenoConfigChange = 'toolchain', +): Capabilities { const path = normalise(rawPath); - if (DOCS_EXTENSIONS.some((ext) => path.endsWith(ext))) return true; - if (DOCS_PREFIXES.some((prefix) => path.startsWith(prefix))) return true; - return false; + if (path === 'deno.json' || path === 'deno.jsonc') { + return rootDenoConfig === 'tasks-only' + ? { ...NONE, deno: true } + : { ...NONE, scaffold: true, docker: true, desktop: true, deno: true }; + } + if (isDenoConfigBase(path)) { + // Any other deno.json*/deno.lock (root lock, nested workspace roots): + // toolchain surface, always escalates. + return { + ...NONE, + scaffold: true, + docker: true, + desktop: true, + deno: true, + surface: path.startsWith('packages/'), + }; + } + if (TIER_DEFINING_WORKFLOWS.has(path)) { + return { ...NONE, scaffold: true, docker: true, desktop: true, deno: true }; + } + if (path.startsWith('.github/workflows/')) { + // Non-tier workflows cannot affect scaffold behaviour by construction + // (#1122), but workflow content is exercised by repo tests + // (e.g. `.llm/tools/release/release-canary-workflow_test.ts`). + return { ...NONE, deno: true }; + } + if (hasExtension(path, DOCS_EXTENSIONS)) { + return { + ...NONE, + docs: !AGENT_CONTEXT_PREFIXES.some((prefix) => path.startsWith(prefix)), + surface: path.startsWith('packages/'), + }; + } + if (CODE_PREFIXES.some((prefix) => path.startsWith(prefix))) { + return { + ...NONE, + scaffold: true, + docker: true, + deno: true, + desktop: path.startsWith('packages/cli/'), + surface: path.startsWith('packages/'), + }; + } + if (DOCS_PREFIXES.some((prefix) => path.startsWith(prefix))) { + return { + ...NONE, + docs: path.startsWith('docs/'), + // Root `deno test`/task gates execute code under `.llm/tools` etc. + deno: hasExtension(path, CODE_EXTENSIONS), + }; + } + // Unrecognised path: force EVERY gate. NEVER skip on a failed classification. + return { ...ALL }; +} + +/** Does this path force the scaffold tiers (back-compat helper for tests)? */ +export function isImpacting(rawPath: string, rootDenoConfig?: DenoConfigChange): boolean { + return classifyPath(rawPath, rootDenoConfig).scaffold; } /** - * True iff `path` is docs-only. Explicit critical paths win; Markdown / MDX - * then wins over ordinary impacting directory prefixes. + * True iff `path` contributes to neither the scaffold tiers nor the Deno + * toolchain lane — the informational docs-only notion behind `docs_only`. */ -export function isDocsOnlyPath(path: string): boolean { - if (isCritical(path)) return false; - if (DOCS_EXTENSIONS.some((ext) => normalise(path).endsWith(ext))) return true; - if (isImpacting(path)) return false; - return isDocs(path); +export function isDocsOnlyPath(path: string, rootDenoConfig?: DenoConfigChange): boolean { + const caps = classifyPath(path, rootDenoConfig); + return !caps.scaffold && !caps.deno; } export interface DecisionInput { @@ -109,18 +249,43 @@ export interface DecisionInput { files: readonly string[]; /** PR label names. */ labels: readonly string[]; + /** + * Root `deno.json` contents at base/head, for the tasks-only + * discrimination. Missing content classifies as `toolchain`. + */ + rootDenoConfig?: { base?: string; head?: string }; } export interface Decision { runStatic: boolean; runRuntime: boolean; docsOnly: boolean; + needsDeno: boolean; + needsDocker: boolean; + needsDesktop: boolean; + needsDocs: boolean; + needsSurface: boolean; reason: string; } +function fullDecision(docsOnly: boolean, reason: string): Decision { + return { + runStatic: true, + runRuntime: true, + docsOnly, + needsDeno: true, + needsDocker: true, + needsDesktop: true, + needsDocs: true, + needsSurface: true, + reason, + }; +} + /** * Pure decision function. `workflow_dispatch` (and any non-PR event) always - * runs both jobs because there is no diff to classify. + * runs everything because there is no diff to classify; an empty PR diff + * does the same. */ export function decide(input: DecisionInput): Decision { const labels = new Set(input.labels); @@ -128,36 +293,48 @@ export function decide(input: DecisionInput): Decision { const skipScaffold = labels.has('ci:skip-scaffold'); const skipE2e = labels.has('ci:skip-e2e'); - // No diff to classify (manual dispatch, push, etc.) -> run everything unless - // an explicit skip label is present. `ci:full` still wins. + // No diff to classify (manual dispatch, push, etc.) -> run everything + // unless an explicit skip label is present. `ci:full` still wins. if (input.eventName !== 'pull_request') { if (forceFull) { - return { - runStatic: true, - runRuntime: true, - docsOnly: false, - reason: `${input.eventName}: ci:full -> run both`, - }; + return fullDecision(false, `${input.eventName}: ci:full -> run everything`); } return { + ...fullDecision( + false, + `${input.eventName}: no diff to classify -> run (skip labels honoured)`, + ), runStatic: !skipScaffold, runRuntime: !skipE2e, - docsOnly: false, - reason: `${input.eventName}: no diff to classify -> run (skip labels honoured)`, }; } const changed = input.files.map(normalise).filter((p) => p.length > 0); - const impacting = changed.filter((p) => !isDocsOnlyPath(p)); - const docsOnly = changed.length > 0 && impacting.length === 0; + if (changed.length === 0) { + return fullDecision(false, 'empty diff: nothing to classify -> run everything'); + } + + const rootConfigChanged = changed.some((p) => p === 'deno.json' || p === 'deno.jsonc'); + const rootDenoConfig: DenoConfigChange = rootConfigChanged + ? classifyDenoConfigChange(input.rootDenoConfig?.base, input.rootDenoConfig?.head) + : 'toolchain'; + + const caps = { ...NONE }; + for (const path of changed) { + const contribution = classifyPath(path, rootDenoConfig); + caps.scaffold ||= contribution.scaffold; + caps.docker ||= contribution.docker; + caps.desktop ||= contribution.desktop; + caps.deno ||= contribution.deno; + caps.docs ||= contribution.docs; + caps.surface ||= contribution.surface; + } + + const impacting = changed.filter((p) => !isDocsOnlyPath(p, rootDenoConfig)); + const docsOnly = impacting.length === 0; if (forceFull) { - return { - runStatic: true, - runRuntime: true, - docsOnly, - reason: 'ci:full label present -> force both jobs', - }; + return fullDecision(docsOnly, 'ci:full label present -> force everything'); } // scaffold-static @@ -166,26 +343,26 @@ export function decide(input: DecisionInput): Decision { if (skipScaffold) { runStatic = false; staticReason = 'scaffold-static skipped by ci:skip-scaffold'; - } else if (docsOnly) { + } else if (!caps.scaffold) { runStatic = false; - staticReason = 'scaffold-static skipped: docs-only change'; + staticReason = 'scaffold-static skipped: no scaffold-impacting change'; } else { runStatic = true; - staticReason = 'scaffold-static: code change detected'; + staticReason = 'scaffold-static: scaffold-impacting change detected'; } - // scaffold-runtime + // scaffold-runtime (the docker tier: docker is the exception, not the default) let runRuntime: boolean; let runtimeReason: string; if (skipE2e) { runRuntime = false; runtimeReason = 'scaffold-runtime skipped by ci:skip-e2e'; - } else if (docsOnly) { + } else if (!caps.docker) { runRuntime = false; - runtimeReason = 'scaffold-runtime skipped: docs-only change'; + runtimeReason = 'scaffold-runtime skipped: no docker-tier change'; } else { runRuntime = true; - runtimeReason = 'scaffold-runtime: code change detected'; + runtimeReason = 'scaffold-runtime: docker-tier change detected'; } const impactingNote = docsOnly @@ -193,12 +370,19 @@ export function decide(input: DecisionInput): Decision { : `${impacting.length}/${changed.length} impacting file(s), e.g. ${ impacting.slice(0, 3).join(', ') || '(none)' }`; + const vectorNote = `vector deno=${caps.deno} docker=${caps.docker} desktop=${caps.desktop} ` + + `docs=${caps.docs} surface=${caps.surface}`; return { runStatic, runRuntime, docsOnly, - reason: `${impactingNote}. ${staticReason}; ${runtimeReason}`, + needsDeno: caps.deno, + needsDocker: caps.docker, + needsDesktop: caps.desktop, + needsDocs: caps.docs, + needsSurface: caps.surface, + reason: `${impactingNote}. ${staticReason}; ${runtimeReason}. ${vectorNote}`, }; } @@ -284,26 +468,46 @@ async function main(): Promise { ? parseNameStatus(nameStatus) : parseFiles(Deno.env.get('CHANGED_FILES')); const labels = parseLabels(Deno.env.get('PR_LABELS')); + const rootBase = Deno.env.get('ROOT_DENO_JSON_BASE'); + const rootHead = Deno.env.get('ROOT_DENO_JSON_HEAD'); - const decision = decide({ eventName, files, labels }); + const decision = decide({ + eventName, + files, + labels, + rootDenoConfig: { + base: rootBase && rootBase.trim().length > 0 ? rootBase : undefined, + head: rootHead && rootHead.trim().length > 0 ? rootHead : undefined, + }, + }); const reason = sanitizeReason(decision.reason); const lines = [ `run_static=${decision.runStatic}`, `run_runtime=${decision.runRuntime}`, `docs_only=${decision.docsOnly}`, + `needs_deno=${decision.needsDeno}`, + `needs_docker=${decision.needsDocker}`, + `needs_desktop=${decision.needsDesktop}`, + `needs_docs=${decision.needsDocs}`, + `needs_surface=${decision.needsSurface}`, `reason=${reason}`, ]; // Human-readable log. console.log('[ci-classify-changes]'); - console.log(` event: ${eventName}`); - console.log(` labels: ${labels.join(', ') || '(none)'}`); - console.log(` changed: ${files.length} file(s)`); - console.log(` run_static: ${decision.runStatic}`); - console.log(` run_runtime: ${decision.runRuntime}`); - console.log(` docs_only: ${decision.docsOnly}`); - console.log(` reason: ${reason}`); + console.log(` event: ${eventName}`); + console.log(` labels: ${labels.join(', ') || '(none)'}`); + console.log(` changed: ${files.length} file(s)`); + console.log(` run_static: ${decision.runStatic}`); + console.log(` run_runtime: ${decision.runRuntime}`); + console.log(` docs_only: ${decision.docsOnly}`); + console.log(` needs_deno: ${decision.needsDeno}`); + console.log(` needs_docker: ${decision.needsDocker}`); + console.log(` needs_desktop: ${decision.needsDesktop}`); + console.log(` needs_docs: ${decision.needsDocs}`); + console.log(` needs_surface: ${decision.needsSurface}`); + console.log(` reason: ${reason}`); const outPath = Deno.env.get('GITHUB_OUTPUT'); if (outPath) { From 9bb8f6a4bcfaee832c2303e84280e0faf45015d2 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Mon, 3 Aug 2026 17:28:48 +0200 Subject: [PATCH 4/8] feat(ci): desktop and runtime tiers read the capability vector (#1152 S3) desktop-native-linux swaps the #1151 run_static||run_runtime proxy for the dedicated needs_desktop output (packages/cli is the whole .deb surface); scaffold-runtime is documented as the docker tier; classify extracts root deno.json base/head for the tasks-only discrimination and lane visibility reports the desktop selection directly. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVEZJ1CBtRYNXzGQRZrTat --- .github/workflows/e2e-cli.yml | 73 ++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/.github/workflows/e2e-cli.yml b/.github/workflows/e2e-cli.yml index 98f7d7b7d..a567246b8 100644 --- a/.github/workflows/e2e-cli.yml +++ b/.github/workflows/e2e-cli.yml @@ -25,18 +25,22 @@ name: e2e-cli # any other PR carrying the `e2e-cli-gate` label, PLUS manual dispatch. # # Skip policy (see `.github/scripts/ci-classify-changes.ts`): the `classify` job -# derives `run_static` / `run_runtime` from the PR diff and labels. All three -# expensive jobs still START and report SUCCESS, but short-circuit to a -# "skipped-by-policy" step when their work is not needed: +# emits the #1152 capability vector (`run_static`, `run_runtime`, `needs_*`) +# from the PR diff and labels. All three expensive jobs still START and report +# SUCCESS, but short-circuit to a "skipped-by-policy" step when their work is +# not needed: # -# - docs-only diff (only `**/*.md(x)`, `docs/`, `.llm/`, `.agents/`, `.claude/`, -# and never `packages/`/`plugins/`/`apps/`/`deno.json*`/`deno.lock`/ -# `.github/workflows/`) -> skip both. +# - `run_static`: any scaffold-impacting change (`packages/`/`plugins/`/ +# `apps/`, tier-defining workflows, toolchain `deno.json*`/`deno.lock`, +# or any unrecognised path). +# - `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 both regardless. -# - `desktop-native-linux` has no dedicated classifier output yet (#1152 adds -# `needs_desktop`); until then it runs whenever EITHER scaffold tier runs and -# skips only when the classifier said neither is needed (#1151). +# scaffold-runtime; `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. The @@ -79,6 +83,7 @@ jobs: run_static: ${{ steps.decide.outputs.run_static }} run_runtime: ${{ steps.decide.outputs.run_runtime }} docs_only: ${{ steps.decide.outputs.docs_only }} + needs_desktop: ${{ steps.decide.outputs.needs_desktop }} reason: ${{ steps.decide.outputs.reason }} steps: - name: Checkout @@ -112,12 +117,32 @@ jobs: git diff --name-status -M "$BASE_SHA...$HEAD_SHA" >> "$GITHUB_OUTPUT" echo "__EOF__" >> "$GITHUB_OUTPUT" + - name: Extract root deno.json (base + head, tasks-only discrimination) + id: rootcfg + if: github.event_name == 'pull_request' + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + # Missing/unreadable content classifies as `toolchain` (fail toward + # running) — hence `|| true` on both sides. + run: | + { + echo "base<<__CFG_EOF__" + git show "$BASE_SHA:deno.json" 2>/dev/null || true + echo "__CFG_EOF__" + echo "head<<__CFG_EOF__" + git show "$HEAD_SHA:deno.json" 2>/dev/null || true + echo "__CFG_EOF__" + } >> "$GITHUB_OUTPUT" + - name: Decide run/skip id: decide env: EVENT_NAME: ${{ github.event_name }} CHANGED_NAME_STATUS: ${{ steps.diff.outputs.changed }} PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} + ROOT_DENO_JSON_BASE: ${{ steps.rootcfg.outputs.base }} + ROOT_DENO_JSON_HEAD: ${{ steps.rootcfg.outputs.head }} run: deno run --allow-read --allow-write --allow-env .github/scripts/ci-classify-changes.ts scaffold-static: @@ -178,7 +203,9 @@ jobs: needs: classify # Job always starts (so its status reports); real work is guarded by the # classifier so docs-only / `ci:skip-e2e` short-circuit to SUCCESS. Heavy; - # additive until observed green. + # additive until observed green. This is the docker tier (#1152): reached + # on the classifier's docker signal — deliberately wide in v1, tightened + # only against observed green history. # 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=false`. `skipped` classify (applicability gate) keeps the old @@ -258,19 +285,19 @@ jobs: name: desktop-native-linux (deb + signed updater) needs: classify # Job always starts (so its status reports); real work is guarded by the - # classifier exactly like its two siblings (#1151). No dedicated desktop - # classifier output exists yet (#1152 adds `needs_desktop`), so the - # conservative proxy is: run whenever EITHER scaffold tier runs, skip only - # when the classifier said neither is needed. + # classifier exactly like its two siblings (#1151). The `.deb`/updater + # surface lives entirely under `packages/cli/`, so the dedicated + # `needs_desktop` output (#1152) gates it: `packages/cli/`, tier-defining + # workflows, and toolchain config changes run it; everything else skips. # 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_static=false` AND `run_runtime=false`. `skipped` classify - # (applicability gate) keeps the old skip behavior. + # `needs_desktop=false`. `skipped` classify (applicability gate) keeps the + # old skip behavior. if: ${{ !cancelled() && needs.classify.result != 'skipped' }} runs-on: ubuntu-latest timeout-minutes: 30 env: - RUN: ${{ needs.classify.result != 'success' || needs.classify.outputs.run_static == 'true' || needs.classify.outputs.run_runtime == 'true' }} + RUN: ${{ needs.classify.result != 'success' || needs.classify.outputs.needs_desktop == 'true' }} SKIP_REASON: ${{ needs.classify.outputs.reason }} steps: - name: Skipped by policy @@ -355,6 +382,7 @@ jobs: CLASSIFY_RESULT: ${{ needs.classify.result }} RUN_STATIC: ${{ needs.classify.outputs.run_static }} RUN_RUNTIME: ${{ needs.classify.outputs.run_runtime }} + RUN_DESKTOP: ${{ needs.classify.outputs.needs_desktop }} STATIC_RESULT: ${{ needs.scaffold-static.result }} RUNTIME_RESULT: ${{ needs.scaffold-runtime.result }} DESKTOP_NATIVE_LINUX_RESULT: ${{ needs.desktop-native-linux.result }} @@ -380,14 +408,7 @@ jobs: static_outcome="$(describe_scaffold_lane "$RUN_STATIC" "$STATIC_RESULT")" runtime_outcome="$(describe_scaffold_lane "$RUN_RUNTIME" "$RUNTIME_RESULT")" - # Desktop has no dedicated classifier output yet (#1152): it is - # selected whenever either scaffold tier is (#1151). - if [ "$RUN_STATIC" = "true" ] || [ "$RUN_RUNTIME" = "true" ]; then - desktop_selected="true" - else - desktop_selected="false" - fi - desktop_outcome="$(describe_scaffold_lane "$desktop_selected" "$DESKTOP_NATIVE_LINUX_RESULT")" + desktop_outcome="$(describe_scaffold_lane "$RUN_DESKTOP" "$DESKTOP_NATIVE_LINUX_RESULT")" { echo "## Scaffold CI lane visibility" From 462f0bf114766b70eef2a7c424e1dc3b44659246 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Mon, 3 Aug 2026 17:28:48 +0200 Subject: [PATCH 5/8] feat(ci): check-test and quality consult the classifier (#1152 S4) ci.yml gains its own classify job (same script, same fail-closed rule). check-test reads needs_deno; quality reads needs_deno || needs_docs so docs-only PRs still get fmt/docs-accuracy. Both required checks always start and report SUCCESS via the scaffold-static skipped-by-policy pattern; push events classify as run-everything; close-gate and deps-report stay ungated. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVEZJ1CBtRYNXzGQRZrTat --- .github/workflows/ci.yml | 159 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 155 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 729a52799..c5e7e715c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,17 @@ name: ci # # - `deps-report` — non-blocking dependency-freshness report (informational). # +# Skip policy (#1152, see `.github/scripts/ci-classify-changes.ts`): the +# `classify` job emits the capability vector; `check-test` reads `needs_deno` +# and `quality` reads `needs_deno || needs_docs`. Both jobs still START and +# report SUCCESS (they are required checks) but short-circuit to a +# "skipped-by-policy" step when the diff cannot affect them — the same +# pattern as e2e-cli.yml's scaffold tiers, and the same FAIL-CLOSED rule: a +# skip requires `classify` to SUCCEED with an explicit `needs_*=false`; a +# failed classify forces a full run. Push events (no diff) always run in +# full. `ci:full` forces everything; the skip labels do NOT extend here. +# `close-gate` and `deps-report` are never gated. +# # Still out of scope here (deferred Phase-2 "repo process automation" umbrella): # the toolchain-heavy CLI runtime e2e (`deno task e2e:cli` / scaffold.runtime, # which spawns `aspire` + `docker` + `postgres`), OIDC publish (S3), and the @@ -83,10 +94,82 @@ jobs: --pr "${{ github.event.pull_request.number }}" --pretty + classify: + name: classify changes + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + needs_deno: ${{ steps.decide.outputs.needs_deno }} + needs_docs: ${{ steps.decide.outputs.needs_docs }} + docs_only: ${{ steps.decide.outputs.docs_only }} + reason: ${{ steps.decide.outputs.reason }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: '2.9.0' + + - name: Self-check the classifier + run: >- + deno test --allow-read --allow-write --allow-env + .github/scripts/ci-classify-changes.test.ts + + - name: Compute changed files (merge-base diff, rename-aware) + id: diff + if: github.event_name == 'pull_request' + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + # `--name-status -M` so renames/copies report BOTH sides — a + # source->docs rename can never classify docs-only (see e2e-cli.yml). + run: | + echo "changed<<__EOF__" >> "$GITHUB_OUTPUT" + git diff --name-status -M "$BASE_SHA...$HEAD_SHA" >> "$GITHUB_OUTPUT" + echo "__EOF__" >> "$GITHUB_OUTPUT" + + - name: Extract root deno.json (base + head, tasks-only discrimination) + id: rootcfg + if: github.event_name == 'pull_request' + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + # Missing/unreadable content classifies as `toolchain` (fail toward + # running) — hence `|| true` on both sides. + run: | + { + echo "base<<__CFG_EOF__" + git show "$BASE_SHA:deno.json" 2>/dev/null || true + echo "__CFG_EOF__" + echo "head<<__CFG_EOF__" + git show "$HEAD_SHA:deno.json" 2>/dev/null || true + echo "__CFG_EOF__" + } >> "$GITHUB_OUTPUT" + + - name: Decide run/skip + id: decide + env: + EVENT_NAME: ${{ github.event_name }} + CHANGED_NAME_STATUS: ${{ steps.diff.outputs.changed }} + PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} + ROOT_DENO_JSON_BASE: ${{ steps.rootcfg.outputs.base }} + ROOT_DENO_JSON_HEAD: ${{ steps.rootcfg.outputs.head }} + run: deno run --allow-read --allow-write --allow-env .github/scripts/ci-classify-changes.ts + check-test: + needs: classify + # Required check: always starts and reports (see the skip-policy header). + # FAIL-CLOSED: a failed classify forces RUN=true. + if: ${{ !cancelled() }} runs-on: ubuntu-latest timeout-minutes: 30 env: + RUN: ${{ needs.classify.result != 'success' || needs.classify.outputs.needs_deno == 'true' }} + SKIP_REASON: ${{ needs.classify.outputs.reason }} NETSCRIPT_TEST_REDIS_URL: redis://127.0.0.1:6379 services: redis: @@ -99,78 +182,122 @@ jobs: --health-timeout 3s --health-retries 10 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::check-test skipped by policy. %s\n' "$SKIP_REASON" + - name: Checkout + if: env.RUN == 'true' uses: actions/checkout@v4 - name: Setup Deno + if: env.RUN == 'true' uses: denoland/setup-deno@v2 with: deno-version: '2.9.0' - name: Install workspace dependencies + if: env.RUN == 'true' run: deno install - name: Repo-wide check + if: env.RUN == 'true' run: deno task check - name: Required real-Redis regression tests + if: env.RUN == 'true' run: deno task test:redis-regression - name: Prove the pre-#1075 adapter turns the gate red + if: env.RUN == 'true' run: deno task test:redis-regression:negative-control - name: Repo-wide test + if: env.RUN == 'true' run: deno task test - name: Agentic OpenRouter preset canary + if: env.RUN == 'true' run: deno task agentic:provider-canary quality: + needs: classify + # Required check: always starts and reports. Runs for toolchain-relevant + # diffs AND docs diffs (fmt/docs-accuracy/taglines validate Markdown), so + # the gate is `needs_deno || needs_docs`. FAIL-CLOSED on classify failure. + if: ${{ !cancelled() }} runs-on: ubuntu-latest timeout-minutes: 30 + env: + RUN: ${{ needs.classify.result != 'success' || needs.classify.outputs.needs_deno == 'true' || needs.classify.outputs.needs_docs == '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::quality skipped by policy. %s\n' "$SKIP_REASON" + - name: Checkout + if: env.RUN == 'true' uses: actions/checkout@v4 - name: Setup Deno + if: env.RUN == 'true' uses: denoland/setup-deno@v2 with: deno-version: '2.9.0' - name: Install workspace dependencies + if: env.RUN == 'true' run: deno install - name: Lint + if: env.RUN == 'true' run: deno task lint - name: Format check + if: env.RUN == 'true' run: deno task fmt:check - name: JSR tagline length + if: env.RUN == 'true' run: deno task docs:tagline:check - name: Docs accuracy check + if: env.RUN == 'true' run: deno task docs:accuracy - name: Generated asset freshness + if: env.RUN == 'true' run: deno task check:assets-barrel - name: Publish asset freshness + if: env.RUN == 'true' run: deno task check:publish-assets - name: Emitted sample type-check + if: env.RUN == 'true' run: deno task check:emitted-samples - name: NetScript JSR emitted-specifier guard + if: env.RUN == 'true' run: deno task check:netscript-jsr-specifiers - name: Scaffold version guard (no preview pins) + if: env.RUN == 'true' run: deno task check:scaffold-versions - name: Publish dry-run (0 slow-types across all units) + if: env.RUN == 'true' run: deno task publish:dry-run - name: Critical advisory audit + if: env.RUN == 'true' run: deno task audit:critical deps-report: @@ -194,7 +321,7 @@ jobs: lane-visibility: name: core CI lane visibility - needs: [close-gate, check-test, quality, deps-report] + needs: [close-gate, classify, check-test, quality, deps-report] if: ${{ always() && github.event_name == 'pull_request' }} runs-on: ubuntu-latest timeout-minutes: 5 @@ -202,19 +329,43 @@ jobs: - name: Summarize core lane outcomes env: CLOSE_GATE_RESULT: ${{ needs.close-gate.result }} + CLASSIFY_RESULT: ${{ needs.classify.result }} + NEEDS_DENO: ${{ needs.classify.outputs.needs_deno }} + NEEDS_DOCS: ${{ needs.classify.outputs.needs_docs }} CHECK_TEST_RESULT: ${{ needs.check-test.result }} QUALITY_RESULT: ${{ needs.quality.result }} DEPS_REPORT_RESULT: ${{ needs.deps-report.result }} run: | + describe_core_lane() { + selected="$1" + result="$2" + if [ "$CLASSIFY_RESULT" = "success" ] && [ "$selected" != "true" ]; then + printf 'skipped by policy' + elif [ "$result" = "skipped" ]; then + printf 'not scheduled' + else + printf 'ran (%s)' "$result" + fi + } + + if [ "$NEEDS_DENO" = "true" ] || [ "$NEEDS_DOCS" = "true" ]; then + quality_selected="true" + else + quality_selected="false" + fi + check_test_outcome="$(describe_core_lane "$NEEDS_DENO" "$CHECK_TEST_RESULT")" + quality_outcome="$(describe_core_lane "$quality_selected" "$QUALITY_RESULT")" + { echo "## Core CI lane visibility" echo - echo "These lanes were scheduled for this pull request; each outcome is the terminal job result." + echo "A successful required job can be a policy skip; this table uses classifier outputs to distinguish it from real execution." echo echo "| Lane | Outcome |" echo "| --- | --- |" printf '| `close-gate` | ran (%s) |\n' "$CLOSE_GATE_RESULT" - printf '| `check-test` | ran (%s) |\n' "$CHECK_TEST_RESULT" - printf '| `quality` | ran (%s) |\n' "$QUALITY_RESULT" + printf '| `classify` | ran (%s) |\n' "$CLASSIFY_RESULT" + printf '| `check-test` | %s |\n' "$check_test_outcome" + printf '| `quality` | %s |\n' "$quality_outcome" printf '| `deps-report` | ran (%s) |\n' "$DEPS_REPORT_RESULT" } >> "$GITHUB_STEP_SUMMARY" From cea1ad5b1b64a4b8cc47cfccc6f4e6285e6e977e Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Mon, 3 Aug 2026 17:28:48 +0200 Subject: [PATCH 6/8] feat(ci): surface-diff reads needs_surface instead of a paths filter (#1152 S5) The workflow-level paths filter meant no status report on non-package PRs, blocking required-check promotion. needs_surface mirrors the old filter; the job now always starts and short-circuits to skipped-by-policy. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVEZJ1CBtRYNXzGQRZrTat --- .github/workflows/surface-diff.yml | 72 +++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/.github/workflows/surface-diff.yml b/.github/workflows/surface-diff.yml index dc30cf3a4..d3dfae9b3 100644 --- a/.github/workflows/surface-diff.yml +++ b/.github/workflows/surface-diff.yml @@ -1,27 +1,95 @@ name: public-surface-diff +# #1152: the old workflow-level `paths: packages/**` filter meant this job did +# not report at all on non-matching PRs, so it could never be promoted to a +# required check. The classifier's `needs_surface` output mirrors that filter +# exactly (any `packages/**` change, plus the conservative unrecognised-path +# escalation); the job now always starts and reports, short-circuiting to a +# skipped-by-policy step. FAIL-CLOSED: a failed classify forces a full run. + on: pull_request: - paths: - - "packages/**" permissions: contents: read jobs: + classify: + name: classify changes + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + needs_surface: ${{ steps.decide.outputs.needs_surface }} + reason: ${{ steps.decide.outputs.reason }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: "2.9.0" + + - name: Self-check the classifier + run: >- + deno test --allow-read --allow-write --allow-env + .github/scripts/ci-classify-changes.test.ts + + - name: Compute changed files (merge-base diff, rename-aware) + id: diff + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + # `--name-status -M` so renames/copies report BOTH sides (see + # e2e-cli.yml). + run: | + echo "changed<<__EOF__" >> "$GITHUB_OUTPUT" + git diff --name-status -M "$BASE_SHA...$HEAD_SHA" >> "$GITHUB_OUTPUT" + echo "__EOF__" >> "$GITHUB_OUTPUT" + + # No root deno.json extraction here: `needs_surface` is independent of + # the tasks-only discrimination (the root config never lives under + # `packages/`), and the missing-content default is the conservative one. + - name: Decide run/skip + id: decide + env: + EVENT_NAME: ${{ github.event_name }} + CHANGED_NAME_STATUS: ${{ steps.diff.outputs.changed }} + PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} + run: deno run --allow-read --allow-write --allow-env .github/scripts/ci-classify-changes.ts + surface-diff: + needs: classify + # Job always starts (so its status reports); FAIL-CLOSED on classify + # failure, same pattern as the other classifier consumers. + if: ${{ !cancelled() }} runs-on: ubuntu-latest timeout-minutes: 20 + env: + RUN: ${{ needs.classify.result != 'success' || needs.classify.outputs.needs_surface == '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::surface-diff skipped by policy. %s\n' "$SKIP_REASON" + - name: Checkout + if: env.RUN == 'true' uses: actions/checkout@v4 - name: Setup Deno + if: env.RUN == 'true' uses: denoland/setup-deno@v2 with: deno-version: "2.9.0" # Beta rollout is observational. Issue #309 owns the stable-line flip to blocking. - name: Classify public API changes + if: env.RUN == 'true' run: deno task surface:diff continue-on-error: true From e2eecba0b39915f42b3ee6dfb5ddf1184c0dcb8f Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Mon, 3 Aug 2026 17:29:16 +0200 Subject: [PATCH 7/8] chore(ci): default all version-tagged actions to Node 24 runtimes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit checkout v4->v5, upload-artifact v4->v5, download-artifact v4->v5, setup-dotnet v4->v5, upload-pages-artifact v3->v5, deploy-pages v4->v5, configure-pages v5->v6 — the smallest majors whose runtime is node24 for the release-critical actions. SHA-pinned third-party actions are left as pinned; denoland/setup-deno@v2 is already the latest major. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVEZJ1CBtRYNXzGQRZrTat --- .github/workflows/ci.yml | 10 +++++----- .github/workflows/e2e-cli-prod-local.yml | 6 +++--- .github/workflows/e2e-cli-prod.yml | 8 ++++---- .github/workflows/e2e-cli.yml | 21 +++++++++++---------- .github/workflows/jsr-settings.yml | 2 +- .github/workflows/pages.yml | 8 ++++---- .github/workflows/publish.yml | 2 +- .github/workflows/surface-diff.yml | 4 ++-- 8 files changed, 31 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5e7e715c..c15a67fcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: pull-requests: read steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Deno uses: denoland/setup-deno@v2 @@ -105,7 +105,7 @@ jobs: reason: ${{ steps.decide.outputs.reason }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -191,7 +191,7 @@ jobs: - name: Checkout if: env.RUN == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Deno if: env.RUN == 'true' @@ -244,7 +244,7 @@ jobs: - name: Checkout if: env.RUN == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Deno if: env.RUN == 'true' @@ -305,7 +305,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Deno uses: denoland/setup-deno@v2 diff --git a/.github/workflows/e2e-cli-prod-local.yml b/.github/workflows/e2e-cli-prod-local.yml index 4363af48e..43cd8ec8c 100644 --- a/.github/workflows/e2e-cli-prod-local.yml +++ b/.github/workflows/e2e-cli-prod-local.yml @@ -38,7 +38,7 @@ jobs: timeout-minutes: 75 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Deno uses: denoland/setup-deno@v2 @@ -46,7 +46,7 @@ jobs: deno-version: '2.9.0' - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: '10.0.x' @@ -90,7 +90,7 @@ jobs: - name: Upload prod-local E2E artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: e2e-cli-prod-local path: | diff --git a/.github/workflows/e2e-cli-prod.yml b/.github/workflows/e2e-cli-prod.yml index 4d22ddc7b..aa5a2992a 100644 --- a/.github/workflows/e2e-cli-prod.yml +++ b/.github/workflows/e2e-cli-prod.yml @@ -27,7 +27,7 @@ jobs: timeout-minutes: 75 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Deno uses: denoland/setup-deno@v2 @@ -35,7 +35,7 @@ jobs: deno-version: '2.9.0' - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: '10.0.x' @@ -48,7 +48,7 @@ jobs: - name: Download published version if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: netscript-published-version-${{ github.event.workflow_run.id }} run-id: ${{ github.event.workflow_run.id }} @@ -122,7 +122,7 @@ jobs: - name: Upload production E2E artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: e2e-cli-prod-${{ steps.version.outputs.version }} path: | diff --git a/.github/workflows/e2e-cli.yml b/.github/workflows/e2e-cli.yml index a567246b8..ff0c97985 100644 --- a/.github/workflows/e2e-cli.yml +++ b/.github/workflows/e2e-cli.yml @@ -43,9 +43,10 @@ name: e2e-cli # scaffold-runtime; `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. The -# cheap required trio (quality / check-test / deps-report) lives in ci.yml and is -# NOT touched by this policy. Release gates (publish, e2e-cli-prod*) have no skip. +# so they never strand a required status check if promoted to one later. ci.yml +# gates its required jobs (check-test / quality) through its own classify job +# under the same policy (#1152). Release gates (publish, e2e-cli-prod*) have no +# skip. # # FAIL-CLOSED RULE: a skip requires `classify` to SUCCEED with an explicit # `run_*=false`. If classify fails for any reason, the expensive jobs run in @@ -87,7 +88,7 @@ jobs: reason: ${{ steps.decide.outputs.reason }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -170,7 +171,7 @@ jobs: - name: Checkout if: env.RUN == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Deno if: env.RUN == 'true' @@ -226,7 +227,7 @@ jobs: - name: Checkout if: env.RUN == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Deno if: env.RUN == 'true' @@ -236,7 +237,7 @@ jobs: - name: Setup .NET if: env.RUN == 'true' - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: "10.0.x" @@ -272,7 +273,7 @@ jobs: - name: Upload E2E report artifact if: always() && env.RUN == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: e2e-cli-scaffold-runtime-report path: | @@ -309,7 +310,7 @@ jobs: - name: Checkout if: env.RUN == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Deno if: env.RUN == 'true' @@ -364,7 +365,7 @@ jobs: - name: Upload native desktop evidence if: always() && env.RUN == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: e2e-cli-desktop-native-linux-report path: .llm/tmp/desktop-native-e2e/evidence.json diff --git a/.github/workflows/jsr-settings.yml b/.github/workflows/jsr-settings.yml index 2de68a582..934f1f81f 100644 --- a/.github/workflows/jsr-settings.yml +++ b/.github/workflows/jsr-settings.yml @@ -25,7 +25,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Deno uses: denoland/setup-deno@v2 diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 6b886c06d..fb089a7d1 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Deno uses: denoland/setup-deno@v2 with: @@ -39,9 +39,9 @@ jobs: working-directory: docs/site run: deno task check:caveats - name: Configure Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@v6 - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v5 with: path: docs/site/_site @@ -54,4 +54,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d50c95447..a0e2a5b05 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -113,7 +113,7 @@ jobs: - name: Upload published version if: steps.release.outputs.dry_run != 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: netscript-published-version-${{ github.run_id }} path: version.txt diff --git a/.github/workflows/surface-diff.yml b/.github/workflows/surface-diff.yml index d3dfae9b3..580d2e2f6 100644 --- a/.github/workflows/surface-diff.yml +++ b/.github/workflows/surface-diff.yml @@ -23,7 +23,7 @@ jobs: reason: ${{ steps.decide.outputs.reason }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -80,7 +80,7 @@ jobs: - name: Checkout if: env.RUN == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Deno if: env.RUN == 'true' From c4f775dd9bb79e5425cc59557ca33cc63ccdb609 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Mon, 3 Aug 2026 17:33:26 +0200 Subject: [PATCH 8/8] chore(harness): record #1152 implementation + live verification evidence Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PVEZJ1CBtRYNXzGQRZrTat --- .../context-pack.md | 24 ++++++++--------- .../ci-scope-expensive-jobs--1152/drift.md | 15 +++++++++++ .../ci-scope-expensive-jobs--1152/worklog.md | 27 +++++++++++++++++++ 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/.llm/runs/ci-scope-expensive-jobs--1152/context-pack.md b/.llm/runs/ci-scope-expensive-jobs--1152/context-pack.md index d48b5e93a..9694893ca 100644 --- a/.llm/runs/ci-scope-expensive-jobs--1152/context-pack.md +++ b/.llm/runs/ci-scope-expensive-jobs--1152/context-pack.md @@ -1,15 +1,15 @@ # Context pack — ci-scope-expensive-jobs--1152 -**State**: plan written, awaiting PLAN-EVAL. No implementation slices landed. +**State**: implementation complete on PR #1155 (draft), live-verified by demo runs; awaiting the +PR's own full CI green, then acceptance-evidence mirror → ready-merge → merge. -- Branch `ci/scope-expensive-jobs` (base `origin/main`), worktree `/home/codex/repos/ns-ci-scope`. -- #1151 already shipped separately: PR #1153, verified by run 30825776156 (docs-only demo → - desktop skipped-by-policy). Not part of this run's remaining scope. -- Read order to resume: `supervisor.md` → `research.md` → `plan.md` (D1–D5 decisions, S1–S6 - slices, open questions 1–4). -- Next actions: (1) dispatch PLAN-EVAL on the open-model evaluator lane; (2) on PASS, implement - S1→S6 sequentially on this branch, one commit+push+PR comment per slice; (3) before/after - measurement posted on #1152. -- Hard constraints: label set frozen (`ci:full`/`ci:skip-scaffold`/`ci:skip-e2e`), skipped jobs - still report, unrecognised path ⇒ whole vector true, no routing tables in YAML, no - docker/scaffold runs from this shared machine. +- Branch `ci/scope-expensive-jobs`; PR #1155 (`Closes #1152`). #1151 already SHIPPED via PR #1153. +- Landed: classifier capability vector (needs_deno/docker/desktop/docs/surface) + #1122 precision + (tier-defining workflows only; root deno.json tasks-only discrimination), consumers in + e2e-cli.yml / ci.yml / surface-diff.yml (all skipped-by-policy pattern, fail-closed), Node-24 + action bumps. +- Evidence: 50 classifier unit tests; demo run 30827771974 (docs-only → all three expensive jobs + skipped); demo run 30827782060 (release-workflow-only → same, the #1122 replay). +- Follow-ups: post-merge observation of first real docs-only PR; needs_docker package-set + tightening against observed green history; sqlite-tier e2e idea filed as its own issue. +- Constraints: label set frozen; skipped jobs still report; unrecognised path ⇒ vector all true. diff --git a/.llm/runs/ci-scope-expensive-jobs--1152/drift.md b/.llm/runs/ci-scope-expensive-jobs--1152/drift.md index 055275df2..2af758375 100644 --- a/.llm/runs/ci-scope-expensive-jobs--1152/drift.md +++ b/.llm/runs/ci-scope-expensive-jobs--1152/drift.md @@ -6,3 +6,18 @@ - 2026-08-03 `minor` — PLAN-EVAL not yet dispatched: the evaluator lane (separate open-model session per `lane-policy.md`) is supervisor-triggered; this session stopped at the plan report as the brief requires ("Report your plan before implementing #1152"). +- 2026-08-03 `significant` — **PLAN-EVAL superseded by direct owner ratification**: the owner + reviewed the reported plan and instructed "reviewed and approved by me proceed and make the PR + pass ci green so we can merge them; release cut for 0.0.4 is waiting". Recorded here as the + owner-authorized fallback for the formal evaluator pass (release urgency). D5 (scaffold-static + IS the deno-only tier; no third suite) is ratified implicitly by the approval. +- 2026-08-03 `minor` — Slices S1 and S2 were implemented as ONE commit (the intermediate + S1-only state was throwaway); trail is otherwise per plan. +- 2026-08-03 `minor` — Scope addition by owner mid-run: bump all version-tagged actions to + Node 24 majors (checkout/upload-artifact/download-artifact/setup-dotnet v5, pages trio + latest). SHA-pinned third-party actions left pinned. +- 2026-08-03 `minor` — `quality` gate widened from plan's `needs_deno` to + `needs_deno || needs_docs`: a docs-only PR still needs fmt/docs-accuracy — the plan's + needs_docs step-level idea was dropped as unsafe (code changes can break docs accuracy too). + `needs_docs` excludes agent-context Markdown (`.llm/`, `.agents/`, `.claude/`) so the #1055 + class skips everything. diff --git a/.llm/runs/ci-scope-expensive-jobs--1152/worklog.md b/.llm/runs/ci-scope-expensive-jobs--1152/worklog.md index 7ac620300..44577906c 100644 --- a/.llm/runs/ci-scope-expensive-jobs--1152/worklog.md +++ b/.llm/runs/ci-scope-expensive-jobs--1152/worklog.md @@ -12,3 +12,30 @@ - Wrote `research.md` (baseline measurements, #1122 precision failure, required-check inventory, `.llm/tools` classification edge) and `plan.md` (capability vector D1–D5, slices S1–S6). - **Design checkpoint**: plan locked pending PLAN-EVAL. No implementation before PASS. + +## 2026-08-03 — Owner approval + implementation (PR #1155) + +- Owner ratified the plan directly and ordered proceed-to-green (see drift.md); PLAN-EVAL formal + pass superseded by owner authority. +- #1153 (=#1151) merged to main after: full green run incl. rerun of a transient npm flake + (`Failed caching npm package '@types/pg' — error reading a body from connection`), close-gate + green via evidence mirror (all 4 acceptance boxes on #1151 checked with provenance comment). + Issue #1151 CLOSED, `status:shipped`. +- Slices landed on `ci/scope-expensive-jobs`: + - S1+S2 (one commit): classifier capability vector + tier-defining-workflow and root + `deno.json` tasks-only precision; **50 unit tests green**, scoped lint 0, fmt clean. + - S3 e2e-cli.yml: desktop → `needs_desktop`; runtime documented as docker tier; root config + extraction step; lane visibility. + - S4 ci.yml: classify job + `check-test` ← `needs_deno`, `quality` ← `needs_deno || needs_docs` + with skipped-by-policy; lane visibility distinguishes policy skips. + - S5 surface-diff.yml: paths filter folded into `needs_surface`. + - chore: all version-tagged actions bumped to Node-24 majors (checkout/upload-artifact/ + download-artifact/setup-dotnet v5; pages trio latest; SHA-pinned actions untouched). + - merge commits: #1151 branch based in; main synced post-squash (conflicts resolved to the + #1152 header/desktop versions). +- **Live verification (stacked demo PRs, e2e-cli-gate label):** + - docs-only demo PR #1156 → run 30827771974: scaffold-static, scaffold-runtime AND + desktop-native-linux all `success` with ONLY "Skipped by policy" steps. + - release-workflow-only demo PR #1157 (the #1122 class) → run 30827782060: same — all three + expensive jobs skipped by policy on a `release-canary.yml`-only diff. + - The #1155 PR itself is the positive case: tier workflow edits → everything runs in full.