Implementation Plan: CLI Acceptance Scenarios & Cucumber-JVM Nightly

Branch: feat/cli-acceptance-bdd-nightly | Date: 2026-10-11 | Spec: spec.md Input: Mission specification from kitty-specs/cli-acceptance-bdd-nightly-01M4NZRJ/spec.md

Summary

Stand up a black-box acceptance surface for the Spec Kitty CLI: Gherkin scenarios in tests/scenarios/<area>/ and a nightly Cucumber-JVM runner (acceptance-jvm/, a Gradle subproject mirroring the contracts/ pinning precedent) that installs a uv build wheel into a throwaway venv and drives the CLI as an external process on Linux, macOS and Windows. The first deliverable is an ADR amendment recording the 2026-10-11 owner ruling that promise machinery (promise index, per-PR promise-tag check, sync token) belongs to spec-kitty-qa, not core — a narrowing of a never-built part of the accepted ADR. CI gains two non-gating router groups (so contributors need no JDK) and an sdist exclusion for both new trees.

Technical Context

Language/Version: Java 25 (acceptance-jvm runner, per the 2026-10-03 amendment of ADR 2026-10-01-2); Python 3.11+ (existing CLI under test, the generalized pin-guard script, the packaging test) Primary Dependencies: Gradle 8.14.5 (pinned + sha256-checked, mirrors contracts/tools/pins.json); Cucumber-JVM 7 on the JUnit Platform; allure-cucumber7-jvm; uv (builds the wheel under test). No new Python runtime dependencies. Storage: N/A — each scenario uses an ephemeral temp git repo + temp venv; nothing persisted. Testing: Cucumber-JVM/JUnit Platform for acceptance scenarios (nightly only); pytest for the generalized gradle_pin_check.py and the new packaging-safety assertion; ruff/mypy at zero. Target Platform: Linux, macOS, Windows (nightly CI runners); the runner itself is JDK 25 + the pinned Gradle on any of the three. Project Type: single project + one new JVM subproject (acceptance-jvm/) + a scenario corpus (tests/scenarios/). Performance Goals: no per-PR JDK cost (NFR-004 — both router groups gate no job); per-scenario process timeout in the driver; gating nightly run has 0 retries (NFR-005). Constraints: black-box only — steps call the SpecKittyDriver port, never import Python, never use internal Java (C-001); CLI from a uv build wheel in a temp venv, never PyPI/editable (C-002); hermetic per-scenario env (NFR-002); 100% JVM deps pinned in Gradle-generated verification-metadata.xml, no committed wrapper jar (NFR-003); sdist excludes acceptance-jvm/ + tests/scenarios/ (C-004); promise machinery out of core scope (C-003). Scale/Scope: 2 seed scenarios (one init area), 1 Gradle subproject, 1 three-OS nightly job, 2 non-gating router groups, 1 sdist-exclude + test, 1 shared-guard generalization. ~7 implementation concerns.

Supply-Chain Security (DIRECTIVE_051 / supply-chain-install-safety)

This plan adds JVM dependencies (Cucumber-JVM 7, allure-cucumber7-jvm, JUnit Platform) and a pinned Gradle distribution. Controls, each mirroring the already-vetted contracts/ precedent:

installed through the existing contracts/tools/install_tools.py (single canonical installer — reused, not copied), which refuses to unpack bytes that do not match the pin.

acceptance-jvm/gradle/verification-metadata.xml (verify-metadata=true, sha256 per artifact, origin="Generated by Gradle", never hand-edited; generated with gradle --write-verification-metadata sha256 <tasks> --no-daemon).

acceptance-jvm/ and the nightly's gradle-version (decision DM-01M4P2B3S88TYZAK2WH5HM12PZ), so pin drift fails GRADLE_PIN_MISMATCH — one canonical guard for both subprojects.

reviewed Gradle supply-chain model). The adversarial-squad challenge pass is explicitly folded into the closeout pre-PR review squad (semi-auto delivery step 8) rather than run as a separate pass now; no contested finding is dropped — any raised at closeout is dispositioned in research.md.

  • Gradle distribution: pinned by version + sha256 in acceptance-jvm/tools/pins.json,
  • JVM dependencies: all resolved artifacts pinned in a Gradle-generated
  • No committed wrapper jar / properties: CI invokes the installed gradle binary directly.
  • Drift guard: the shared contracts/tools/gradle_pin_check.py is generalized to also scan
  • No new Python deps are added, so no PyPI advisory surface changes.
  • Adversarial evidence: the dependency decisions have low novelty (they replicate an existing,

Charter Check

GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.

Charter present (.kittify/charter/). Relevant gates and this plan's posture:

reused from contracts/tools/install_tools.py, not duplicated; the pin-drift guard is the one gradle_pin_check.py, generalized rather than forked. ✅

plus minimal, additive edits to two hot CI files and one pyproject block. ✅

with same-fixture positive controls; the packaging-exclude test and the generalized pin-guard test are authored to fail before the production change. ✅

_DELIBERATELY_UNGATED_FILTER_GROUPS is an honest exemption (they genuinely gate no job); the gate's fault-injection twin still catches real orphans. ✅

  • Single canonical authority (DIRECTIVE, Quality Standing Orders): the Gradle installer is
  • Locality of change: additions are isolated new trees (acceptance-jvm/, tests/scenarios/)
  • ATDD-first / red-first (C-011): the two seed scenarios ratchet existing init behaviour
  • Supply-chain (DIRECTIVE_051): see the Supply-Chain section above. ✅
  • Terminology Canon: no feature* identifiers; scenarios/areas use neutral names. ✅
  • No retired terminology: no ceremony/status-writing; N/A to this surface. ✅
  • Architectural-gate non-vacuity: adding scenarios/acceptance_jvm to

No charter violations → Complexity Tracking left empty.

Project Structure

Documentation (this mission)

kitty-specs/cli-acceptance-bdd-nightly-01M4NZRJ/
├── plan.md              # This file
├── research.md          # Grounding note (committed) + plan decisions/supply-chain
├── data-model.md        # Phase 1 output (conceptual entities)
├── quickstart.md        # Phase 1 output (how to run the runner locally)
├── decisions/           # Decision Moment records (DM-*)
└── tasks.md             # Phase 2 output (/spec-kitty.tasks — NOT created here)

(No contracts/ directory: this mission authors no published API. meta.json carries "contracts": "none" with a rationale so strict accept waives it.)

Source Code (repository root)

acceptance-jvm/                         # NEW — Gradle subproject (nightly-only runner)
├── build.gradle                        # Cucumber-JVM 7 + JUnit Platform + allure-cucumber7-jvm
├── settings.gradle                     # names the subproject; documents "no committed wrapper jar"
├── gradle/
│   └── verification-metadata.xml       # Gradle-generated, sha256 per artifact
├── tools/
│   └── pins.json                       # Gradle distribution pin (version + sha256), mirrors contracts/
└── src/test/java/.../                  # SpecKittyDriver (port), CliProcessDriver (impl),
                                        #   step definitions, env/temp-repo isolation, JUnit runner;
                                        #   reads ../tests/scenarios in place

tests/scenarios/                        # NEW — Gherkin corpus (sdist-excluded)
└── init/
    └── init.feature                    # two bound, untagged seed scenarios (FR-003, FR-004)

contracts/tools/gradle_pin_check.py     # EDIT — generalize to also scan acceptance-jvm/
tests/contract/...                      # EDIT — extend pin-check unit tests
.github/workflows/ci-router.yml         # EDIT — add non-gating scenarios + acceptance_jvm groups
.github/workflows/ci-nightly.yml        # EDIT — add 3-OS acceptance-bdd job
tests/architectural/test_workflow_coherence.py  # EDIT — add both groups to _DELIBERATELY_UNGATED_FILTER_GROUPS
pyproject.toml                          # EDIT — sdist exclude += acceptance-jvm/**, tests/scenarios/**
tests/cross_cutting/packaging/test_packaging_safety.py  # EDIT — sdist-exclusion assertion + positive control
docs/adr/4.x/2026-10-11-3-acceptance-testing-and-bdd-support.md  # EDIT — amendment (first deliverable)
docs/changelog/CHANGELOG.md             # EDIT — changelog entry (closeout)

Structure Decision: Single project with one additive JVM subproject (acceptance-jvm/) and a scenario corpus (tests/scenarios/), both top-level and both excluded from the sdist so neither ships. Everything else is a minimal additive edit to existing authorities. The runner reads the scenario corpus in place (no copy) so one source feeds both the router and the nightly.

Complexity Tracking

No charter violations — not applicable.

Implementation Concern Map

> Concerns are NOT work packages. /spec-kitty.tasks turns these into executable WPs with explicit > dependencies. The sequencing notes below encode the one hard ordering constraint the grounding > found: the ci-router groups (IC-05) red unless the scenario + subproject files they glob are > already git-tracked (FR-003c test_every_restored_filter_glob_is_live).

IC-01 — ADR amendment (the first deliverable)

  • Purpose: Record the 2026-10-11 owner ruling so the narrowed scope is the written decision: core keeps scenarios/runner/nightly; promise index, per-PR promise-tag check and PROMISE_INDEX_SYNC_TOKEN are out of core (spec-kitty-qa owns them, and none was ever built); scenarios flips from "gates the architectural job" to "gates no job"; @promise/@defect/@qa are inert in core and only @wip/@manual affect the nightly filter.
  • Relevant requirements: FR-001; frames C-003, FR-014.
  • Affected surfaces: docs/adr/4.x/2026-10-11-3-acceptance-testing-and-bdd-support.md (an Amendment section + Status note; the original decision text preserved).
  • Sequencing/depends-on: none (can land first).
  • Risks: must faithfully reconcile the accepted ADR's four promise-machinery claims against the ruling without rewriting history — amend, don't delete.

IC-02 — Seed scenarios + corpus layout

  • Purpose: Establish tests/scenarios/<area>/ and author the two init scenarios drawn verbatim from init_help.py: re-running init is idempotent and only restores missing skill roots; init never git-inits or commits. Both bound and untagged (DM-01M4NZV51YAVWX0WHNCZGTB0CS) with same-fixture positive controls.
  • Relevant requirements: FR-002, FR-003, FR-004; SC-002.
  • Affected surfaces: tests/scenarios/init/init.feature (+ area layout).
  • Sequencing/depends-on: none structurally, but must be git-tracked before or with IC-05.
  • Risks: scenario wording must match the actual init behaviour and the driver's step bindings (IC-03); the positive controls must genuinely discriminate (non-vacuity).

IC-03 — Gradle acceptance-jvm runner + driver port

  • Purpose: The nightly runner. A pinned-Gradle subproject (mirrors contracts/: own pins.json + Gradle-generated verification-metadata.xml, no wrapper jar, JDK 25) running Cucumber-JVM 7 on the JUnit Platform, reading ../tests/scenarios in place. A SpecKittyDriver port with a CliProcessDriver (ProcessBuilder: drained streams, timeout, descendant-process kill, normalised line endings, real-path compare). Each scenario gets a temp git repo + fully redirected env (SPEC_KITTY_HOME, HOME, USERPROFILE, XDG, LOCALAPPDATA, UTF-8, NO_COLOR, fixed COLUMNS). CLI under test = uv build wheel in a temp venv.
  • Relevant requirements: FR-005, FR-006, FR-007, FR-008, FR-009; C-001, C-002, C-006; NFR-002.
  • Affected surfaces: acceptance-jvm/** (new); reuses contracts/tools/install_tools.py.
  • Sequencing/depends-on: IC-02 (scenarios to execute); must be git-tracked before/with IC-05.
  • Risks: cross-OS process + path + line-ending handling; descendant-kill on Windows; hermetic env correctness (NFR-002 positive control); supply-chain pin generation.

IC-04 — Generalize the shared Gradle pin-drift guard

  • Purpose: Extend contracts/tools/gradle_pin_check.py to also scan acceptance-jvm/ (build.gradle, pins.json, verification-metadata.xml) and the nightly acceptance-bdd job's gradle-version, keeping one canonical GRADLE_PIN_MISMATCH authority for both subprojects (DM-01M4P2B3S88TYZAK2WH5HM12PZ, NFR-003).
  • Relevant requirements: NFR-003.
  • Affected surfaces: contracts/tools/gradle_pin_check.py; its unit tests under tests/contract/.
  • Sequencing/depends-on: IC-03 (acceptance-jvm pins + metadata must exist to scan).
  • Risks: shared-gate edit — contracts/ behaviour must stay byte-identical; add focused coverage for the new scan paths (red-first).

IC-05 — Non-gating ci-router groups

  • Purpose: Add scenarios (tests/scenarios/) and acceptance_jvm (acceptance-jvm/) to the dorny filter block and the changes.outputs block, gating no per-PR job (C-005, FR-012); add both to _DELIBERATELY_UNGATED_FILTER_GROUPS in test_workflow_coherence.py; keep them out of the unmatched union (non-src). No ci_retirement_scrub.json edit (non-src groups are outside scrub).
  • Relevant requirements: FR-012; C-005; NFR-004.
  • Affected surfaces: .github/workflows/ci-router.yml; tests/architectural/test_workflow_coherence.py.
  • Sequencing/depends-on: IC-02 and IC-03 (FR-003c: both globs must match git-tracked paths).
  • Risks: hot file (54 commits/90d) — keep diff minimal/additive; must satisfy FR-003b/c and the transcription guards (test_changes_outputs_cover_every_routing_group, test_unmatched_union_covers_every_src_backed_group).

IC-06 — Nightly acceptance-bdd job (3 OSes)

  • Purpose: Add an acceptance-bdd job to ci-nightly.yml on a Linux/macOS/Windows matrix. A gating run filters not @wip and not @manual with no retries (NFR-005); a separate non-gating run executes @wip. Build the wheel (uv build), install the pinned Gradle, set up JDK 25, run the runner, upload Cucumber JSON + JUnit XML + Allure results as named artifacts for the spec-kitty-qa nightly. Decision: leave the non-gating job OUT of nightly-summary.needs to minimise the hot-file diff (verify release-readiness.yml / tests/release/test_release_ci_ownership.py do not require it).
  • Relevant requirements: FR-010, FR-011; NFR-001, NFR-005; SC-001.
  • Affected surfaces: .github/workflows/ci-nightly.yml.
  • Sequencing/depends-on: IC-03 (runner must exist to invoke).
  • Risks: hot file (40/90d); first multi-OS nightly job (others are single ubuntu-24.04); confirm the release/ownership tests don't freeze the nightly job set.

IC-07 — sdist packaging exclusion + assertion

  • Purpose: Add acceptance-jvm/ and tests/scenarios/ to the pyproject sdist exclude, and add a @pytest.mark.slow assertion in test_packaging_safety.py that both trees are absent from the built sdist, paired with a same-fixture positive control (a src/ package IS present) so the absence check is non-vacuous. Wheel tests unchanged (both trees are already outside the wheel allowlist).
  • Relevant requirements: FR-013; C-004.
  • Affected surfaces: pyproject.toml ([tool.hatch.build.targets.sdist].exclude); tests/cross_cutting/packaging/test_packaging_safety.py.
  • Sequencing/depends-on: IC-02 + IC-03 (files should exist so the built sdist meaningfully exercises the exclusion; the test + positive control are authored red-first).
  • Risks: the packaging test builds an sdist (~1 min, slow tier); keep the new assertion in the existing build_artifacts fixture, not a second build.