From 7e218935e98fc4e94d810e1398b572b850bb37ac Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 2 Oct 2026 02:26:57 +0800 Subject: [PATCH 01/12] =?UTF-8?q?docs(openspec):=20plan=20target-openspec-?= =?UTF-8?q?cli-114-line=20=E2=80=94=20pin=20v1.14.0,=20evidence=20report,?= =?UTF-8?q?=20change=20artifacts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CP0 of the OpenSpecUI 14 line: references/openspec pinned to v1.14.0 (94ca9c1e), executed-CLI evidence report, and the validated change (v14 admission window >=1.14.0 <1.15.0; version-report / change-list-inventory / status-warnings / apply-task-source-location contracts; ten registry entries with the widened shared-.agents root; fixture rotation with boundary retention). Baseline notes record the already-delivered 1132 patch line (kilo root, apply tracking, glob parity) so it is not repeated. --- .../.openspec.yaml | 2 + .../loop/checkpoints.md | 46 ++++ .../loop/implementation.md | 33 +++ .../loop/intake.md | 54 +++++ .../loop/research-plan.md | 125 ++++++++++ .../specs/openspec-cli-integration/spec.md | 216 ++++++++++++++++++ .../specs/opsx-config-center/spec.md | 155 +++++++++++++ .../specs/opsx-workflow-ui/spec.md | 67 ++++++ .../specs/projection-contract-truth/spec.md | 30 +++ references/openspec | 2 +- references/openspec-1.14.0-report.md | 166 ++++++++++++++ 11 files changed, 895 insertions(+), 1 deletion(-) create mode 100644 openspec/changes/target-openspec-cli-114-line/.openspec.yaml create mode 100644 openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md create mode 100644 openspec/changes/target-openspec-cli-114-line/loop/implementation.md create mode 100644 openspec/changes/target-openspec-cli-114-line/loop/intake.md create mode 100644 openspec/changes/target-openspec-cli-114-line/loop/research-plan.md create mode 100644 openspec/changes/target-openspec-cli-114-line/specs/openspec-cli-integration/spec.md create mode 100644 openspec/changes/target-openspec-cli-114-line/specs/opsx-config-center/spec.md create mode 100644 openspec/changes/target-openspec-cli-114-line/specs/opsx-workflow-ui/spec.md create mode 100644 openspec/changes/target-openspec-cli-114-line/specs/projection-contract-truth/spec.md create mode 100644 references/openspec-1.14.0-report.md diff --git a/openspec/changes/target-openspec-cli-114-line/.openspec.yaml b/openspec/changes/target-openspec-cli-114-line/.openspec.yaml new file mode 100644 index 00000000..61cfceb5 --- /dev/null +++ b/openspec/changes/target-openspec-cli-114-line/.openspec.yaml @@ -0,0 +1,2 @@ +schema: opsx-collab-pr-loop +created: 2026-10-02 diff --git a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md new file mode 100644 index 00000000..0925dc7c --- /dev/null +++ b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md @@ -0,0 +1,46 @@ + + +# OpenSpecUI 14 checkpoints + +## CP0 — Planning + +- [x] Worktree + submodule pin `94ca9c1e` + evidence report + change artifacts. +- [ ] Codex change review: no blocking findings (≥8/10 target). +- [ ] CP0 committed. + +## CP1 — Window and contracts (Slices 1-2) + +- [ ] Compat window rotated; boundary red recorded (stable 1.13.x → unsupported); mirrors + tests green. +- [ ] Apply-task source locations, status warnings, list archived/nested/warnings, version envelope typed + through contract + projection schemas; reds recorded; focused suites green; core typecheck. + +## CP2 — Registry (Slice 3) + +- [ ] +10 registry entries, kilo root, IBM Bob label, series `'1.14'`, generator `1.14.0`; staleness red + recorded; registry/state/server suites green. + +## CP3 — Web + fixtures (Slices 4-5) + +- [ ] Status warnings direct-plane summary row; web focused tests green. +- [ ] Pin guards rotated; `openspec-cli-114` alias with lockfile+shim evidence; full v14 positive matrix + (9 migrated + version-report + change-list-inventory) green with `--version` provenance; v12 boundary + retained; retired v13 positives deleted with proof; mutation red recorded. + +## CP4 — Alignment (Slice 6) + +- [ ] Four owner groups red/green recorded. + +## CP5 — Delivery + +- [ ] Changeset major; README law (repo en/zh + CLI package); CLAUDE.md pointer; AGENTS.md decision. +- [ ] Full gates (format/lint/typecheck/browser + explicit per-package tests); pre-existing flakes disclosed. +- [ ] PR opened; CI green; Codex final review disposition recorded. +- [ ] Owner acceptance walkthrough (Owner-only). +- [ ] Archive/release — Owner decision. +- [ ] GitHub issues handled with objective links. diff --git a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md new file mode 100644 index 00000000..f70267fb --- /dev/null +++ b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md @@ -0,0 +1,33 @@ + + +# OpenSpecUI 14 implementation state + +## Current state + +- CP0: worktree `openspecui-114` @ `target-openspec-cli-114-line` (off main `4d8c0072`); submodule pinned + `v1.14.0` (`94ca9c1e`); dependencies installed; evidence report + change artifacts written; review rounds + pending. + +## Evidence recording rule + +Every round records: slice, production owners, the precise red as it failed (or the honest statement why a +red could not be captured), the focused green command + result, and the commit. Claims without command +output are not evidence. + +## Subagent topology + +Batches follow the slice order; each subagent touches only its slice's files. Shared files (package.json +devDeps, lockfile, AGENTS.md, READMEs, `.changeset/`) are integrator-owned. Subagents never commit/push, run +focused tests via `pnpm --filter exec vitest run ` (never `test --`), report difficulties +(feedback protocol), and reclaim any process they start. + +## Review rounds + +(pending) diff --git a/openspec/changes/target-openspec-cli-114-line/loop/intake.md b/openspec/changes/target-openspec-cli-114-line/loop/intake.md new file mode 100644 index 00000000..edb67975 --- /dev/null +++ b/openspec/changes/target-openspec-cli-114-line/loop/intake.md @@ -0,0 +1,54 @@ + + +# OpenSpecUI 14 intake + +## Objective Scope + +Ship OpenSpecUI 14 for OpenSpec CLI 1.14.x: pinned reference `v1.14.0` (`94ca9c1e`), evidence report, v14 +admission window, typed contracts for the five moved JSON surfaces (version command, apply task source +locations, list archived/nested/warnings, status warnings, store edit roots), the ten-entry Agent registry +rotation (shared `.agents` root now also `amp`/`gsd`; Kilo commands → `.kilo/command/`; IBM Bob label), +generator staleness to 1.14.0, the full pinned fixture matrix with 1.13.0 boundary negatives, and README / +AGENTS / Changeset major preparation. + +## Owner-default decisions (vetoable in review) + +1. **OpenSpecUI 14, single-series window `>=1.14.0 <1.15.0`** (new major, not a 13.x widening) — five moved + JSON contract surfaces plus a ten-entry registry delta exceed any patch-bridge reading; the + one-line-per-series cadence continues; `1.15` is not pre-claimed. No 1.13 widening debt exists (the v13 + gate already admits all stable 1.13.x, including 1.13.1/1.13.2). +2. **No new capability flags.** Inside the single-series window every admitted CLI carries the new members; + contracts gain optional typed members instead (`archived?`, `nested?`, `warnings?`, task `sourcePath?`/ + `line?`), mirroring the v13 missingPrerequisites/warnings precedent. +3. **Archive list keeps the reactive-filesystem adapter as its projection owner this line.** Switching the + archive inventory to `list --archived` is an architectural follow-up (reactive-model change), explicitly + out of scope; the typed list contract still carries the new members so CLI evidence is never lossy. +4. **No UI integration for `openspec version --check` this line** — typed contract only; update UX is a + product decision. +5. **Status `warnings` render on the direct plane following the v13 summary-row law** (one collapsible + summary row, verbatim expansion, CLI attribution) — advisory, never a gate. +6. **Fixture rotation:** `openspec-cli-114` positive line; retained `openspec-cli-112` stays as the + below-admitted boundary executable (proving rejections against the v14 gate); `openspec-cli-113` alias is + retained for provenance but not required as a second boundary line (v13 suites already prove the 1.13 + line; the boundary class needs one retired executable — 1.12.0 keeps that role, cheapest reuse). + +## Constraints + +- CLI-first: no UI-side replication of upstream validate/parser/archive fixes (fixture-scoped only). +- Focused gates per package (`pnpm --filter exec vitest run `); the recursive `test:ci` aborts at + the first failing package and is not evidence of later packages. +- Header law on every touched TS/TSX file (original request dated 2026-10-02). +- GitHub issues handled after delivery with objective links only. + +## Delivery steps + +Worktree → report → change → Codex review rounds → subagent batches (A: window/contracts/registry; B: +projection/web/fixtures/alignment) → full gates → PR → CI → merge → issue sweep. Release is a separate Owner +decision, as in v13. diff --git a/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md b/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md new file mode 100644 index 00000000..2379e083 --- /dev/null +++ b/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md @@ -0,0 +1,125 @@ + + +# OpenSpecUI 14 research plan + +Evidence: `references/openspec-1.14.0-report.md` (pinned `v1.14.0` @ `94ca9c1e`; executed npm observations). + +## Spec hygiene + +Scenario titles are current-spec identity (archive refuses drops); bodies carry v14 semantics. Historical +titles ("Zed is a 1.10-line skills-only target", "SourceCraft enters at 1.12") describe introduction facts; +`minCliSeries` values never rotate while `AgentCliSeries` does. + +## Slice 1 — Compatibility window and mirrors + +Owners: `packages/core/src/openspec-compat.ts`(+test) — MAJOR 14, series `'1.14'`, ranges +`>=1.14.0 <1.15.0`, NEXT_SERIES `'1.15.0'`, tag pattern `v1.14.*`; `scripts/diagnose-cli-runner.mjs`(+test) +and `scripts/setup-example.ts` mirrors with boundary assertions (1.13 rejected / 1.14 accepted). +Red: constants-first run against current assertions (record 5+ failures incl. stable 1.13.x → unsupported). +Green: compat + diagnose focused tests; core typecheck. + +## Slice 2 — Typed contracts through the full projection chains + +Owners (complete chains, v13-B1 lesson): + +- Apply tasks: `cli-contracts/workflow.ts` `CliApplyTaskSchema` + `opsx-types.ts` `ApplyTaskSchema` gain + optional `sourcePath`/`line`; contract tests pin the executed 1.14 payload (both members present, absolute + path, 1-based line) plus an older-shape payload without them. +- Status warnings: `CliWorkflowStatusSuccessSchema` (batch + single, whichever owns the envelope) and the + opsx status input/projection schemas gain optional top-level `warnings: string[]`; tests pin the executed + `skip_design` warning verbatim. +- List members: the change-list CLI contract gains per-entry optional `archived`/`nested` and an optional + top-level `warnings` array (`nested_change_directory` finding); executed `--archived`/`--all` documents as + fixtures. +- Version command: a new typed contract for `{schemaVersion:1, version, install, update?}` with an executor + method (`version({check})`); no capability flag (admitted-window invariant). +- Store edit roots: no schema change (same array shape); fixture expectations rotate to + `[implementationRoot, projectRoot]` semantics in Slice 6 fixtures only. + +Red: schema-drop reds per surface (parse executed payload through the projection schema asserting the member +survives; fails today). Green: the three contract files + opsx-types tests; core typecheck. + +## Slice 3 — Agent registry rotation (largest slice) + +Baseline note: the 2026-09-27 `update-openspec-cli-1132` change already rotated the Kilo Code command root +to `.kilo/command/` — that physical fact carries forward unchanged. This slice rotates the series and adds +the ten 1.14 entries. + +Owners: `packages/core/src/agent-delivery-registry.ts`(+test): `AgentCliSeries = '1.14'` (minor 13 → 14 in +the resolver), provenance union retains `'1.12'`/`'1.13'`; **ten new entries** in official order — +`amp` (`.agents` root, detection `.amp`), `gsd` (`.agents` root, detection `.gsd`), `atomcode`, `codestudio` +(restart true), `dsh` (skills-only), `easycode` (TOML commands), `gigacode`, `grok` (skills-only), `veai` +(skills-only), `warp` (detection `WARP.md`); label `IBM Bob`; Devin Desktop "(formerly Windsurf)" label +note. `tool-init-state.ts`: `PINNED_AGENT_GENERATOR_VERSION = '1.14.0'`; series-aware staleness +(1.13.x-generated → stale). Server tests rotate (`agent-delivery-projection-service`, +`agent-integrations-router`, agent parts of `tool-subscription-router`). + +Red: rotate PINNED first, record the 1.13.0-generated-stays-current assertion failures. +Green: registry/state suites + server agent suites. + +## Slice 4 — Status warnings Web surface + +Owners: `packages/web/src/routes/change-view.tsx` status region + the v13 summary-row component pattern +(`apply-progress-notice.tsx` or a sibling advisory component): status `warnings` render as ONE always-visible +summary row (count + CLI attribution) with verbatim expansion; absent member degrades silently; never gates +actions. + +Red: component test asserting the row before the render change. Green: focused web tests + typecheck. + +## Slice 5 — Reference pin and the complete fixture matrix + +Owners: `scripts/prepare-openspec-reference.mjs` EXPECTED_COMMIT → `94ca9c1e...`; +`upstream-contract-regression.test.ts` pin rotation; `packages/web/scripts/w2-project-binding-playwright.ts` +pin; `packages/core/package.json` alias `"openspec-cli-114": "npm:@fission-ai/openspec@1.14.0"` (integrator +lockfile); `__tests__/official-cli-v14-fixtures.ts` helper. **Full positive matrix migrates to v14** — the +twelve v13 suites (workflow, default-store, nested-spec, show-diff, validation-full, validation-findings, +batch-status, agent-delivery, apply-readiness, nested-change, task-reading, plus any apply-tracking suite +from the 1132 line) get v14 counterparts, with apply-readiness extended to assert task +`sourcePath`/`line`, **plus two new suites**: `v14-version-report` (no-check envelope only, per no-network +fixture discipline) and `v14-change-list-inventory` (`--archived`/`--all` archived flag, nested warnings +shape). Boundary: rotate `official-cli-v12-boundary-fixtures.test.ts` role text to the v14 gate (1.12.0 +stays the retired executable — 1.13.2 remains the admitted-line provenance through the v13 suites' archive); +retire the v13 positive suites with their counterparts (deletion proof: `git diff --name-status` + orphan +scan; the v13 helper is deleted unless the boundary suite still consumes it — it consumes v12). Mutation +red: bins-map → wrong alias fails `--version` identity. + +Red: alias-resolution failure before install; pin-guard failure before rotation. +Green: joint v14 gate + boundary + upstream-contract-regression; core typecheck; lockfile diff + shim check. + +## Slice 6 — Cross-package alignment (four owner groups) + +1. Web compat copy: `cli-health-gate`(+test), settings diagnostics, `use-cli-runner` — ranges/labels derive + from shared constants; rotate literal expectations (1.13→1.14, retired window now includes 1.13). +2. Server evidence: `router.test.ts`, `change-diff-evidence-service.test.ts`, + `cli-validate-findings-router.test.ts`, `root-context-cold-start.integration.test.ts` (commit pin), + `tool-subscription-router.test.ts` compat assertion. +3. Web evidence: `change-view.test.tsx` admitted fixtures, `validation-findings-evidence`(+test), + `archived-validation-evidence`(+test). +4. CLI/scripts: `worktree-instance-manager.test.ts`, setup-example consumers. + +Red/green per group, recorded separately. + +## Slice 7 — Release preparation (integrator) + +Changeset (major: core/server/web/openspecui); README law (repo en/zh `@^13` row + archived 1.13 copies + +CLI package README scoped to v14); CLAUDE.md pointer to the 1.14 report; AGENTS.md architecture decision + +submodule pin line. Full gates: format/lint/typecheck/browser + per-package test runs (explicit list, not +recursive-only); disclose pre-existing flakes honestly (path-realpath, load-sensitive startup timing). + +## Batch topology + +- Batch A (parallel): Slices 1, 2, 3 (disjoint file sets). +- Batch B (parallel): Slices 4, 5, 6 (each depends only on Batch A constants/schemas). +- Batch C: Slice 7 integrator-only. + +## Stop-loss + +Any slice whose red cannot be produced honestly, or whose focused green fails twice for unrelated reasons, +escalates before proceeding; no unrelated defects merge into this change. diff --git a/openspec/changes/target-openspec-cli-114-line/specs/openspec-cli-integration/spec.md b/openspec/changes/target-openspec-cli-114-line/specs/openspec-cli-integration/spec.md new file mode 100644 index 00000000..3f795c31 --- /dev/null +++ b/openspec/changes/target-openspec-cli-114-line/specs/openspec-cli-integration/spec.md @@ -0,0 +1,216 @@ + + +## MODIFIED Requirements + +### Requirement: CLI Discovery and Version Enforcement + +OpenSpecUI 14 SHALL classify stable OpenSpec CLI `>=1.14.0 <1.15.0` as supported, current, and recommended. +OpenSpec CLI `<1.14.0` (including the whole 1.13.x window that OpenSpecUI 13 admitted, and every older line), +every prerelease, `>=1.15.0`, and an unparseable version SHALL be incompatible and blocked by default. +When an incompatible executable is available, the mismatch Dialog MAY expose `Skip version check`; that action +SHALL bypass only the current Web page runtime's admission gate and SHALL NOT change the detected version, +compatibility evidence, CLI payloads, downstream errors, or product support claim. + +#### Scenario: Accept the current 1.13 line + +- **GIVEN** OpenSpecUI 14 detects a stable OpenSpec CLI 1.13.x executable that OpenSpecUI 13 admitted +- **WHEN** admission is evaluated +- **THEN** the retired 1.13 line SHALL be blocked by default +- **AND** the mismatch evidence SHALL name the v14 accepted and recommended ranges `>=1.14.0 <1.15.0` + +#### Scenario: Block unsupported version forms + +- **GIVEN** OpenSpecUI 14 detects CLI 1.14.0-rc.1, 1.15.0, or an unparseable version +- **WHEN** admission is evaluated +- **THEN** the mismatch Dialog SHALL block normal interactions +- **AND** the mismatch evidence SHALL name the accepted and recommended ranges + +#### Scenario: Retire the v12 admission window + +- **GIVEN** OpenSpecUI 14 detects a stable OpenSpec CLI 1.12.x executable that OpenSpecUI 12 admitted +- **WHEN** admission is evaluated +- **THEN** the retired line SHALL be blocked by default +- **AND** the mismatch evidence SHALL name the v14 accepted and recommended ranges + +#### Scenario: Accept the current 1.12 line + +- **GIVEN** OpenSpecUI 14 detects a stable OpenSpec CLI 1.12.x executable that OpenSpecUI 12 admitted as its + current and recommended line +- **WHEN** admission is evaluated +- **THEN** the retired 1.12 line SHALL be blocked by default +- **AND** the mismatch evidence SHALL name the v14 accepted and recommended ranges + +#### Scenario: Retire the v11 admission window + +- **GIVEN** OpenSpecUI 14 detects a stable OpenSpec CLI 1.10.x or 1.11.x executable that OpenSpecUI 11 + admitted +- **WHEN** admission is evaluated +- **THEN** the retired line SHALL be blocked by default +- **AND** the mismatch evidence SHALL name the v14 accepted and recommended ranges + +#### Scenario: Accept the supported non-current 1.10 line + +- **GIVEN** OpenSpecUI 14 detects a stable OpenSpec CLI 1.10.x executable that OpenSpecUI 11 admitted as its + supported non-current line +- **WHEN** admission is evaluated +- **THEN** the retired 1.10 line SHALL be blocked by default +- **AND** the mismatch evidence SHALL name the v14 accepted and recommended ranges + +#### Scenario: Accept the current 1.11 line + +- **GIVEN** OpenSpecUI 14 detects a stable OpenSpec CLI 1.11.x executable that OpenSpecUI 11 admitted as its + current and recommended line +- **WHEN** admission is evaluated +- **THEN** the retired 1.11 line SHALL be blocked by default +- **AND** the mismatch evidence SHALL name the v14 accepted and recommended ranges + +#### Scenario: Accept the supported non-current 1.8 line + +- **GIVEN** OpenSpecUI 14 detects a stable OpenSpec CLI 1.8.x executable that OpenSpecUI 9 admitted as its + supported non-current line +- **WHEN** admission is evaluated +- **THEN** the retired 1.8 line SHALL be blocked by default +- **AND** the mismatch evidence SHALL name the v14 accepted and recommended ranges + +#### Scenario: Accept the current 1.9 line + +- **GIVEN** OpenSpecUI 14 detects a stable OpenSpec CLI 1.9.x executable that OpenSpecUI 9 admitted as its + current and recommended line +- **WHEN** admission is evaluated +- **THEN** the retired 1.9 line SHALL be blocked by default +- **AND** the mismatch evidence SHALL name the v14 accepted and recommended ranges + +#### Scenario: Accept the adapted 1.7 line + +- **GIVEN** OpenSpecUI 14 detects a stable OpenSpec CLI 1.7.x executable that OpenSpecUI 7 admitted as its + adapted line +- **WHEN** admission is evaluated +- **THEN** the retired 1.7 line SHALL remain blocked by default +- **AND** the mismatch evidence SHALL name the v14 accepted and recommended ranges + +#### Scenario: Block OpenSpec CLI 1.6 + +- **GIVEN** OpenSpecUI 14 detects an available OpenSpec CLI 1.6.x executable +- **WHEN** admission is evaluated +- **THEN** the mismatch Dialog SHALL block normal interactions +- **AND** SHALL identify the v14 accepted and recommended ranges + +#### Scenario: Bypass only the current page runtime + +- **GIVEN** an available incompatible executable is blocked +- **WHEN** the user explicitly selects `Skip version check` +- **THEN** the current Web page runtime MAY admit interactions at the user's risk +- **AND** compatibility evidence SHALL remain incompatible +- **AND** downstream protocol or execution failures SHALL remain visible +- **WHEN** the page runtime is reconstructed, refreshed, or reopened +- **THEN** the bypass SHALL be absent and the mismatch Dialog SHALL block again + +#### Scenario: Never persist the bypass + +- **WHEN** a version bypass is active +- **THEN** browser storage, Workspace state, project configuration, Server state, and exported snapshots SHALL NOT + contain it + +#### Scenario: Bypass does not admit a version-specific capability or inventory + +- **GIVEN** an unsupported, prerelease, or unparseable CLI has a page-local version bypass +- **WHEN** OpenSpecUI derives CLI capabilities or an Agent delivery inventory +- **THEN** it SHALL retain the incompatible classification +- **AND** it SHALL NOT select line-specific capability facts or a fallback inventory +- **AND** downstream execution SHALL fail through its typed availability boundary rather than a simulated + supported CLI + +## ADDED Requirements + +### Requirement: Version Report Command Contract + +OpenSpecUI 14 SHALL decode `openspec version --json` as a typed CLI contract: a `schemaVersion: 1` envelope +carrying the installed `version` and `install` facts (location, packageManager, scope), with the `update` +member conditionally present only under `--check`. The executor SHALL NOT run the registry probe unless an +explicit consumer requests it, and no UI surface is obligated by this contract. + +#### Scenario: Decode the no-check envelope + +- **GIVEN** `openspec version --json` runs without `--check` on an admitted 1.14 CLI +- **WHEN** the output crosses the typed contract +- **THEN** `schemaVersion`, `version`, and `install` SHALL be preserved as typed facts +- **AND** no `update` member SHALL be synthesized + +#### Scenario: Update metadata stays optional and probe-gated + +- **GIVEN** a consumer has not requested an update check +- **WHEN** the version contract is invoked +- **THEN** no network probe SHALL be issued by OpenSpecUI +- **AND** a payload carrying `update` SHALL parse with the member preserved verbatim + +### Requirement: Change List Inventory Contract + +The CLI change-list contract SHALL preserve the OpenSpec 1.14 inventory members: per-entry optional +`archived` (present only under `--archived`/`--all`) and `nested` (namespace-folder findings), and an +optional top-level `warnings` array (`nested_change_directory` findings). OpenSpecUI's Archive list SHALL +keep its reactive-filesystem projection owner; the CLI members are typed evidence, not a mandated data-source +switch. + +#### Scenario: Archived entries decode with the archived flag + +- **GIVEN** `openspec list --archived --json` returns an archived entry +- **WHEN** the output crosses the typed contract +- **THEN** the entry SHALL preserve `archived: true` and the shared per-entry shape + +#### Scenario: Nested findings are objective evidence + +- **GIVEN** a plain `list --json` reports a namespace folder +- **WHEN** the output crosses the typed contract +- **THEN** the entry's `nested` member and the top-level `warnings` finding SHALL be preserved verbatim +- **AND** absent members SHALL stay absent (no synthesized empty states) + +#### Scenario: Default list output remains compatible + +- **GIVEN** `openspec list --json` without the new flags on a change set with no namespace folders +- **WHEN** the output crosses the typed contract +- **THEN** parsing SHALL succeed with neither `archived` nor `warnings` fabricated + +### Requirement: Status Advisory Warnings Contract + +OpenSpecUI 14 SHALL decode the OpenSpec 1.14 `status --json` top-level `warnings` member (unrecognized +`.openspec.yaml` key advisories) as optional typed evidence preserved verbatim with CLI ownership. Warnings +SHALL NOT gate Root readiness, action availability, or Apply state. + +#### Scenario: Unrecognized metadata keys are preserved verbatim + +- **GIVEN** a change's `.openspec.yaml` carries an unrecognized key and `status --json` reports it +- **WHEN** the payload crosses the typed contract +- **THEN** the warning string SHALL be preserved exactly as the CLI provided it +- **AND** OpenSpecUI SHALL NOT synthesize or reword a warning when the member is absent + +#### Scenario: Warnings never gate actions + +- **GIVEN** a status payload carries advisory warnings +- **WHEN** action availability is evaluated +- **THEN** gating SHALL remain unchanged by the warnings + +### Requirement: Apply Task Source Locations Contract + +The Apply Instructions task contract SHALL carry the OpenSpec 1.14 `sourcePath` (absolute) and `line` +(1-based) members as optional typed facts on each task. OpenSpecUI SHALL NOT re-derive or second-guess the +locations; they are CLI-owned evidence for which file and line each checkbox lives at. + +#### Scenario: Located tasks decode with both members + +- **GIVEN** `instructions apply --json` on an admitted 1.14 CLI returns located tasks +- **WHEN** the payload crosses the typed contract and its projection chain +- **THEN** each task SHALL preserve `sourcePath` and `line` with CLI provenance + +#### Scenario: Older-shape tasks still parse + +- **GIVEN** an Apply Instructions payload whose tasks lack the location members +- **WHEN** the payload crosses the typed contract +- **THEN** parsing SHALL succeed with both members absent +- **AND** no default location SHALL be fabricated diff --git a/openspec/changes/target-openspec-cli-114-line/specs/opsx-config-center/spec.md b/openspec/changes/target-openspec-cli-114-line/specs/opsx-config-center/spec.md new file mode 100644 index 00000000..c55a117c --- /dev/null +++ b/openspec/changes/target-openspec-cli-114-line/specs/opsx-config-center/spec.md @@ -0,0 +1,155 @@ + + +## RENAMED Requirements + +- FROM: `### Requirement: OpenSpecUI 13 Agent Delivery Inventory` +- TO: `### Requirement: OpenSpecUI 14 Agent Delivery Inventory` + +## MODIFIED Requirements + +### Requirement: OpenSpecUI 14 Agent Delivery Inventory + +`/config/agents` SHALL select the official Agent inventory and command capabilities from the admitted running +OpenSpec CLI version, not from one fixed 1.14 registry. It SHALL project supported 1.14 physical delivery +metadata: current/legacy project roots, user-global skill roots, detection paths, command paths, delivery +capability, generated version, migration/cleanup evidence, and IDE restart requirements where that CLI declares +them. A tool absent from the admitted CLI line SHALL be unavailable for that line, not falsely stale or +present. + +#### Scenario: Project and global roots remain distinct + +- **GIVEN** MiniMax Code declares a global skills root and Codex declares a project skills root +- **WHEN** Agent delivery is projected +- **THEN** MiniMax evidence SHALL identify the user-global root +- **AND** Codex evidence SHALL identify the project-local root +- **AND** the UI SHALL NOT present either root as the other scope + +#### Scenario: Zed is a 1.10-line skills-only target + +- **GIVEN** the admitted OpenSpec CLI is stable 1.14.x +- **WHEN** the Agent inventory is listed +- **THEN** Zed Agent SHALL appear as a skills-only project target writing `.agents/skills` +- **AND** it SHALL NOT be offered with a command surface +- **GIVEN** the admitted OpenSpec CLI is stable 1.9.x or older +- **WHEN** the Agent inventory is listed +- **THEN** Zed Agent SHALL be unavailable for that line + +#### Scenario: Antigravity migration is objective + +- **GIVEN** the admitted OpenSpec CLI is stable 1.14.x and OpenSpec-managed Antigravity artifacts exist at + `.agent/` +- **WHEN** Agent delivery is projected +- **THEN** OpenSpecUI SHALL report `.agents/skills` and `.agents/workflows` as current roots with `.agent` as + legacy/migration evidence +- **AND** SHALL NOT delete or move the files without an explicit official CLI operation + +#### Scenario: Antigravity remains on its 1.10 root + +- **GIVEN** OpenSpec-managed Antigravity artifacts were generated by a 1.10-era CLI at `.agent/` and the + admitted OpenSpec CLI is stable 1.14.x +- **WHEN** Antigravity delivery is projected +- **THEN** `.agent` SHALL appear only as legacy/migration evidence toward the `.agents` roots +- **AND** the retired 1.10-era current-root interpretation SHALL NOT be projected for that session + +#### Scenario: Codex migration is objective + +- **GIVEN** OpenSpec-managed Codex skills exist at `.codex/skills` but not `.agents/skills` +- **WHEN** Agent delivery is projected +- **THEN** OpenSpecUI SHALL report legacy/migration evidence +- **AND** SHALL NOT report `.codex` as the current expected root +- **AND** SHALL NOT delete or move the files without an explicit official CLI operation + +#### Scenario: Shared skills-root ownership is three-valued + +- **GIVEN** the `.agents/skills` tree may be owned by `codex`, `zed`, the vendor-neutral `agents` target, or + since OpenSpec 1.14 by `amp` or `gsd` +- **WHEN** Agent delivery is projected +- **THEN** the owner evidence SHALL come from the official marker and arbitration rules +- **AND** Antigravity SHALL be identified as commands-only at that root, never the skills writer +- **AND** co-located targets SHALL be presented as sharing one physical tree + +#### Scenario: SourceCraft Code Assistant enters at 1.12 + +- **GIVEN** the admitted OpenSpec CLI is stable 1.14.x +- **WHEN** the Agent inventory is listed +- **THEN** SourceCraft Code Assistant SHALL appear with skills at `.codeassistant/skills` and commands at + `.codeassistant/commands/opsx-.md` +- **AND** its skills SHALL be presented as natural-language referenced with no slash-command surface for + skills +- **AND** it SHALL NOT declare an IDE restart requirement or any migration +- **GIVEN** the admitted OpenSpec CLI is stable 1.11.x or older +- **WHEN** the Agent inventory is listed +- **THEN** SourceCraft Code Assistant SHALL be unavailable for that line + +#### Scenario: Ten 1.14 tools enter the official inventory + +- **GIVEN** the admitted OpenSpec CLI is stable 1.14.x +- **WHEN** the Agent inventory is listed +- **THEN** Amp (`.agents` root, detection `.amp`), GSD (`.agents` root, detection `.gsd`), AtomCode + (`.atomcode`), Code Studio (`.codestudio`, IDE restart), DeepSeek Harness (skills-only `.dsh`), EasyCode + (`.easycode`, TOML commands), GigaCode (`.gigacode`), Grok Build (skills-only `.grok`), Veai (skills-only + `.veai`), and Warp (`.warp`, detection `WARP.md`) SHALL appear with their official capability and path + metadata +- **AND** the skills-only entries SHALL NOT be offered with a command surface +- **GIVEN** the admitted OpenSpec CLI is stable 1.13.x or older +- **WHEN** the Agent inventory is listed +- **THEN** no 1.14-line target SHALL appear as stale, missing, or generated for that session + +#### Scenario: Registry labels and command paths rotate as physical facts + +- **GIVEN** the admitted OpenSpec CLI is stable 1.14.x +- **WHEN** the Agent inventory is listed +- **THEN** IBM Bob SHALL appear by its full product name +- **AND** Kilo Code commands SHALL project `.kilo/command/` with the retired `.kilocode/workflows/` path as + legacy/cleanup evidence only + +#### Scenario: Select the version-specific official inventory + +- **GIVEN** the admitted OpenSpec CLI is stable 1.14.x +- **WHEN** the Agent inventory is listed +- **THEN** Zed Agent SHALL appear with the shared `.agents/skills` root and Antigravity SHALL declare the + `.agents` roots with `.agent` as legacy +- **AND** SourceCraft Code Assistant, Command Code, MiniMax Code, Rovo Dev CLI, and Shared `.agents` skills + SHALL retain their individual capability and path metadata +- **AND** an IDE restart requirement SHALL remain visible only where that line declares it for a tool it + actually writes for +- **GIVEN** the admitted OpenSpec CLI is stable 1.13.x or older +- **WHEN** the Agent inventory is listed +- **THEN** no 1.14-line target SHALL appear as stale, missing, or generated for that session + +#### Scenario: One missing adapter does not invalidate unrelated command evidence + +- **GIVEN** the admitted CLI line has no command adapter for one registry entry +- **WHEN** OpenSpecUI loads generated command evidence +- **THEN** that entry SHALL be unavailable with version-scoped evidence +- **AND** command evidence for every independently supported adapter SHALL remain available + +#### Scenario: Retained physical state keeps the selected inventory + +- **GIVEN** an admitted 1.14 Agent projection observes a filesystem or Environment replacement +- **WHEN** its retained physical states recompute +- **THEN** the replacement SHALL use the same 1.14-selected registry as the initial projection +- **AND** SHALL NOT reintroduce retired-line targets from a process-global registry + +#### Scenario: Direct Init uses the projected registry + +- **GIVEN** an admitted 1.14 Agent projection includes a tool +- **WHEN** a caller directly requests Init with a tool outside the projected registry in an explicit `tools` + array +- **THEN** the Router SHALL reject the tool before a CLI process starts +- **AND** SHALL preserve the CLI-owned literal `tools: 'all'` without rewriting it from the projected + inventory + +#### Scenario: Update repairs drifted command files through the CLI + +- **GIVEN** the admitted OpenSpec CLI is stable 1.14.x and a generated command file was hand-edited or + truncated while its skill files carry the current `generatedBy` +- **WHEN** the Agent Update action runs the official `openspec update` +- **THEN** the CLI SHALL detect and repair the drifted command file +- **AND** OpenSpecUI SHALL present the CLI's own repair evidence without introducing parallel drift detection diff --git a/openspec/changes/target-openspec-cli-114-line/specs/opsx-workflow-ui/spec.md b/openspec/changes/target-openspec-cli-114-line/specs/opsx-workflow-ui/spec.md new file mode 100644 index 00000000..f6984a5d --- /dev/null +++ b/openspec/changes/target-openspec-cli-114-line/specs/opsx-workflow-ui/spec.md @@ -0,0 +1,67 @@ + + +## MODIFIED Requirements + +### Requirement: Pinned Workflow Fixtures Are Executable + +OpenSpecUI SHALL prove each accepted workflow contract against the pinned OpenSpec 1.14.0 executable, and +SHALL prove capability-boundary rejections with a retained pinned retired executable. A hand-authored +payload alone SHALL NOT establish support for the CLI line, and a fixture for a retired line SHALL NOT be +reused as positive evidence for the current line. + +#### Scenario: Both supported lines preserve planning/task separation + +- **GIVEN** the pinned workflow fixture matrix runs against the retained pair of pinned executables + (OpenSpec 1.14.0 as the positive line and a retired executable as the boundary line) +- **WHEN** it evaluates Status and Apply Instructions on the positive line +- **THEN** the 1.14.0 executable SHALL satisfy the typed planning-completion and progress contracts +- **AND** the boundary executable SHALL NOT be consulted for positive contract evidence + +#### Scenario: Capability boundaries are executable facts + +- **GIVEN** the retained pinned retired executable is evaluated against the OpenSpecUI 14 admission gate +- **WHEN** the fixture matrix asserts the capability boundary +- **THEN** the retired line SHALL be recorded as below-admitted for the v14 window +- **AND** the 1.14.0 executable SHALL prove the accepted payloads + +#### Scenario: Positive identity is proven, not assumed + +- **GIVEN** the pinned bins map names an installed npm alias for the 1.14.0 executable +- **WHEN** the fixture matrix runs +- **THEN** the executable's `--version` identity SHALL be asserted before contract assertions +- **AND** a bins-map entry pointing at another alias SHALL fail that identity assertion + +## ADDED Requirements + +### Requirement: Status Advisory Warnings Surface + +Change Detail SHALL present CLI-owned status advisory warnings (unrecognized `.openspec.yaml` keys) when an +admitted OpenSpec CLI session provides them, following the Apply-readiness summary-row law: ONE +always-visible summary row naming the warning count and owning CLI command, with the verbatim warning text +one explicit expansion away on the same direct plane. Absent members SHALL degrade silently, and warnings +SHALL NOT gate any action. + +#### Scenario: Warning summary renders on the direct plane + +- **GIVEN** an admitted 1.14 session provides status warnings +- **WHEN** Change Detail renders its status region +- **THEN** one summary row SHALL be visible naming the warning count with CLI attribution without hover +- **AND** the verbatim warning text SHALL become visible with one explicit expansion + +#### Scenario: Degrade without warnings + +- **GIVEN** a status payload omits the warnings member +- **WHEN** Change Detail renders +- **THEN** no warning surface SHALL be synthesized + +#### Scenario: Warnings never gate actions + +- **GIVEN** status warnings are rendered +- **WHEN** Apply or Archive availability is evaluated +- **THEN** action gating SHALL remain unchanged diff --git a/openspec/changes/target-openspec-cli-114-line/specs/projection-contract-truth/spec.md b/openspec/changes/target-openspec-cli-114-line/specs/projection-contract-truth/spec.md new file mode 100644 index 00000000..3ce4e578 --- /dev/null +++ b/openspec/changes/target-openspec-cli-114-line/specs/projection-contract-truth/spec.md @@ -0,0 +1,30 @@ + + +## MODIFIED Requirements + +### Requirement: Generator Evidence Follows the Admitted CLI + +Agent generator evidence SHALL resolve the running OpenSpec CLI's live version and SHALL compare on-disk +generated state against the admitted 1.14 expectations. The pinned generator constant SHALL state the 1.14.0 +baseline and SHALL never be substituted for a live resolution. Generated-by comparisons SHALL be series-aware: +artifacts generated by the admitted series SHALL NOT be flagged stale merely for differing from the pinned +baseline constant, while below-admitted or unparseable generators SHALL be stale. + +#### Scenario: Live version is never fabricated + +- **GIVEN** an Agent projection cannot resolve the running CLI version +- **WHEN** generator evidence is computed +- **THEN** the projection SHALL report unresolved evidence +- **AND** SHALL NOT present the pinned constant as the observed version + +#### Scenario: Stale generated state is line-aware + +- **GIVEN** on-disk Agent artifacts carry `generatedBy: 1.13.2` while the admitted CLI line is 1.14 +- **WHEN** generator evidence is computed +- **THEN** the artifacts SHALL be reported as stale with the generator version as evidence +- **AND** artifacts generated by an admitted 1.14.x generator SHALL NOT be reported stale diff --git a/references/openspec b/references/openspec index 634c557b..94ca9c1e 160000 --- a/references/openspec +++ b/references/openspec @@ -1 +1 @@ -Subproject commit 634c557bd0470eec37861b46172c3f503d283c1b +Subproject commit 94ca9c1eb15d1b49c06c988419b75c3d95f8b2b5 diff --git a/references/openspec-1.14.0-report.md b/references/openspec-1.14.0-report.md new file mode 100644 index 00000000..a60fbf68 --- /dev/null +++ b/references/openspec-1.14.0-report.md @@ -0,0 +1,166 @@ + + +# OpenSpec 1.14 -> OpenSpecUI 14 adaptation report + +## Decision + +```text +OpenSpecUI 14 + adapted / supported OpenSpec CLI >=1.14.0 <1.15.0 + current / recommended OpenSpec CLI >=1.14.0 <1.15.0 + rejected <1.14.0 (including the whole 1.13 v13 window), prereleases, >=1.15.0, unparseable +``` + +The v13 report pre-declared 1.14 admission as "a separately verified decision: either a 13.x window widening +or a new major". The verified delta selects the **new major**: ten new Agent registry entries (40→50 tools, +including two new sharers of the `.agents` skills root), one new CLI command family (`openspec version`), +archived changes entering the `list` protocol, apply tasks gaining mandatory source locations, and a new +top-level `status --json` `warnings` member. Five JSON contract surfaces moved — far beyond a patch bridge — +and the one-line-per-series cadence (v7→1.7 … v13→1.13) continues. `1.15` is not pre-claimed. + +Within the 1.13 window, upstream also shipped 1.13.1/1.13.2; the v13 gate admits the whole stable 1.13 series +already, so no widening debt exists. + +## Evidence baseline + +```text +Observed 2026-10-02 Asia/Shanghai +Package @fission-ai/openspec +npm latest 1.14.0 +Reference repository references/openspec +Pinned tag v1.14.0 +Pinned commit 94ca9c1eb15d1b49c06c988419b75c3d95f8b2b5 +Previous OpenSpecUI pin v1.13.0 / 9d4e5974e5c0d9a09b9c6c1e1eb0975e80ec4461 +Current OpenSpecUI line 13.x (main @ 4d8c0072; the 2026-09-27 update-openspec-cli-1132 change already + rotated the line to CLI 1.13.2 inside the unchanged 1.13 window — Kilo commands + `.kilo/command/`, apply taskTracking evidence contract, artifact-glob parity, and + the 1.13.2 fixture matrix are delivered and are NOT repeated here) +Upstream delta 112 commits, src +5229/-885 across 93 files (three Version Packages releases: 1.13.1, 1.13.2, 1.14.0) +``` + +Sources inspected: `src/commands/workflow/instructions.ts` (LocatedTask), `src/core/list.ts` (archived/nested), +`src/core/version-check.ts` + `src/cli/index.ts` (version command), `src/core/change-status-policy.ts` +(edit roots), `src/core/config.ts` (AI_TOOLS 40→50), `src/core/command-generation/{registry,adapters/*}`, +`CHANGELOG.md` (1.14.0 + 1.13.2 sections), plus executed npm `@fission-ai/openspec@1.14.0` against a disposable +isolated fixture. + +## Verified CLI observations (executed on the npm-published 1.14.0) + +| Surface | 1.13.0 | 1.14.0 (executed) | v14 consequence | +| --- | --- | --- | --- | +| `instructions apply --json` tasks | `{id, description, done}` | each task adds **`sourcePath`** (absolute) and **`line`** (1-based), always present | Apply task contract gains the two members; projection may surface them as evidence | +| `list --archived --json` / `--all` | flag rejected | archived entries returned; every entry carries **`archived: boolean`** (present only under `--archived`/`--all`) | New list transport for archive inventory; capability-free inside the single-series window | +| `list --json` (default) | `{changes, root}` | entries may carry **`nested: string[]`** (namespace folders, #1846/#1849) and the envelope a top-level **`warnings: [{code:'nested_change_directory', name, nested, message}]`** (omitted when empty) | Change-list contract tolerates the new members; nested findings are objective CLI evidence | +| `openspec version [--check] [--json]` | no such command | `{schemaVersion:1, version, install:{location,packageManager,scope}, update?:{...status,latest?,command,canSelfUpgrade}}` (executed without `--check`; `update` absent then) | New typed command contract; `--check` performs an opt-out-able registry probe | +| `status --json` | no `warnings` | top-level **`warnings: string[]`** names unrecognized `.openspec.yaml` keys (executed: `skip_design` warning verbatim; known keys now include `goal, affected_areas, initiative, skip_specs, retire_capabilities`) | Status contract gains optional `warnings`; direct-plane advisory evidence | +| `status --json` `actionContext.allowedEditRoots` (store-backed) | store only | `[implementationRoot, projectRoot]` — the declaring repo joins the store; constraint text tells the agent to ask otherwise | Same array shape, changed semantics; no schema edit, fixture values rotate | +| Agent registry | 40 tools | **50 tools** — added `amp` (`.agents` skills root!, detection `.amp`), `gsd` (`.agents` skills root!, detection `.gsd`), `atomcode`, `codestudio` (requiresIdeRestart), `dsh` (skills-only `.dsh`), `easycode` (TOML commands `/opsx:`), `gigacode`, `grok` (skills-only `.grok`), `veai` (skills-only `.veai`), `warp` (`.warp`, detection `WARP.md`) | Registry inventory grows by ten; shared-root ownership scenarios extend beyond codex/zed/agents | +| Registry label/dir changes | — | `Bob` → **`IBM Bob`**; Kilo Code commands move to **`.kilo/command/`** (from `.kilocode/workflows/`, legacy cleanup); Devin Desktop label notes "(formerly Windsurf)" | Display-name + physical-path facts rotate; kilocode command root changes | + +Behavioral (CLI-internal, no JSON shape): archive aborts when the spec sync reports a blocking condition +(#1759); archive workflows use schema-aware task progress; Windows EPERM copy fallback; security hardening +(#1835); ~20 validate/parser correctness fixes (scenario body required, requirements outside delta sections, +case-differing requirement names, malformed RENAMED pairs, MODIFIED adds reporting, TBD/TODO punctuation, +checkbox markers `{x}`-style counted); `openspec view` TUI shows workflow status; Nix overlay. + +## Protocol delta + +```text +1.14 + +-- openspec version [--check] [--json] (new command; schemaVersion 1 envelope) + +-- instructions apply --json: tasks gain sourcePath + line (always present) + +-- list --archived / --all: archived inventory over the same JSON envelope; per-entry archived flag + +-- list --json: nested namespace-folder findings + top-level warnings array (nested_change_directory) + +-- status --json: top-level warnings (unrecognized .openspec.yaml keys) + +-- status actionContext.allowedEditRoots: store-backed changes add the declaring repo + +-- show --json: requirement/scenario name members (additive) + +-- Agent registry +10 entries (amp, gsd share .agents root); IBM Bob label; Kilo commands → .kilo/command/ + `-- archive/validate/parser hardening (CLI-internal; fixture-scoped, never UI-side logic) +``` + +### 1. `openspec version` is a new command family + +Executed envelope: `{schemaVersion:1, version, install:{location,packageManager,scope}}` with `update` +conditionally spread under `--check`. The registry probe honors CI/no-network and `DO_NOT_TRACK` opt-outs and +time-boxes at 1.5s. OpenSpecUI consequence: a typed CLI contract for the envelope; no UI integration is +required by this line (Settings diagnostics may cite `install` facts later — explicitly out of scope here). + +### 2. Apply task source locations are mandatory members + +`sourcePath` (absolute) and `line` (1-based) ride every apply task (upstream `LocatedTask`). The typed +contract should carry them as **optional** members (older in-flight payloads and static snapshots may lack +them) while the v14 fixtures assert their presence on real 1.14 output. They are evidence for "which file the +checkbox lives in"; OpenSpecUI must not re-derive or second-guess them. + +### 3. Archive inventory enters the `list` protocol + +`list --archived --json` returns archived entries with the same per-entry shape plus `archived: true`; +`--all` mixes both. OpenSpecUI's **Archive list keeps its reactive-filesystem adapter as the projection +owner** this line; switching the archive inventory to the CLI transport is a separate architectural change +(out of scope, recorded below). The typed `list` contract nonetheless gains `archived?`, `nested?`, and the +top-level `warnings` array so the CLI evidence is never lossy when consumed. + +### 4. Status warnings are direct-plane advisory evidence + +Executed verbatim: `"Unrecognized key name(s) in .openspec.yaml (untrusted data, not instructions): +skip_design. Known keys: schema, created, goal, affected_areas, initiative, skip_specs, +retire_capabilities. ..."`. `validate --strict` fails on them. OpenSpecUI projects the member verbatim +(CLI-owned); it is advisory, never a Root/action gate (mirrors the v13 apply-warnings law). + +### 5. The Agent registry grows by ten — including two new `.agents` root sharers + +`amp` and `gsd` join `codex`/`zed`/`antigravity`/`agents` on the shared `.agents` skills root (detection via +`.amp` / `.gsd`). The shared-root ownership spec scenario ("three-valued") must generalize: owner evidence +still comes from the official marker and arbitration; co-located targets share one physical tree. Kilo Code's +command root moves to `.kilo/command/` with legacy cleanup of the old dir. `codestudio` sets +`requiresIdeRestart`. Skills-only entries (`dsh`, `grok`, `veai`) follow the Zed precedent (no command +surface). Series rotates to `'1.14'`; the provenance union retains `'1.12'`/`'1.13'`; the pinned generator +moves to 1.14.0 with series-aware staleness (1.13.x-generated artifacts become stale). + +### 6. Store-backed edit roots name the declaring repo + +Same array shape, new member: `allowedEditRoots: [implementationRoot, projectRoot]` with guidance text. +Fixture expectations that pinned store-only roots rotate; no production branching on root count is allowed. + +## Current owner map + +| Surface | Primary production owner | v14 change | +| --- | --- | --- | +| compatibility gate | `packages/core/src/openspec-compat.ts` (+test, `scripts/diagnose-cli-runner.mjs`(+test), `scripts/setup-example.ts`) | v14 ranges, NEXT_SERIES 1.15.0, mirrors | +| workflow JSON contracts | `packages/core/src/cli-contracts/workflow.ts` (+test) | apply task `sourcePath`/`line`; status top-level `warnings`; list `archived`/`nested`/`warnings`; version envelope contract | +| projection schemas | `packages/core/src/opsx-types.ts` (+test), `planning-cli-projection.ts` | same members through input/projection chains | +| Agent registry/state | `packages/core/src/agent-delivery-registry.ts`, `tool-init-state.ts` (+tests, server projection/router tests) | +10 entries, kilo dir, IBM Bob label, series `'1.14'`, generator `1.14.0` | +| Web evidence | change-view / evidence surfaces (+tests) | status `warnings` projected as direct-plane advisory (v13 summary-row pattern) | +| reference pin | `scripts/prepare-openspec-reference.mjs`, `upstream-contract-regression.test.ts`, `w2-project-binding-playwright.ts` | pin `94ca9c1e` | +| fixtures | `packages/core/package.json` alias + `__tests__/official-cli-v14-fixtures.ts` + suite rotation | positive line 1.14.0; boundary 1.13.0 | +| release | Changesets + README law (repo en/zh + CLI package) | major v14 | + +## Scope boundary + +In scope: the v14 admission window; typed contracts for version/list/status/apply-task members and their +projection; the ten-entry registry rotation with shared-root scenario updates; status `warnings` direct-plane +presentation following the v13 summary-row law; pinned fixture rotation with 1.13.0 boundary negatives; +README/AGENTS/Changeset major preparation. + +Out of scope (follow-ups, not silently absorbed): + +- Switching the Archive list inventory from the reactive-filesystem adapter to the `list --archived` CLI + transport (architectural; reactive model changes). +- Any Settings/Config UI surface consuming `openspec version --check` (update UX is a product decision). +- `1.15` admission claims; supporting CLI `<1.14.0`, prereleases, `>=1.15.0`; UI-side re-implementation of + upstream validate/parser fixes. +- Publishing, PR merge beyond CI gates, archive, and final browser walkthrough (Owner-only). + +## Execution rule + +Each slice starts with one production owner, one precise red case, one green case; focused gates before +broad gates; per-package explicit test runs (the recursive `test:ci` aborts at the first failing package); +`pnpm --filter exec vitest run ` only. From 4b924e3949cf38f3a02857976b341003841a86cb Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 2 Oct 2026 02:46:55 +0800 Subject: [PATCH 02/12] docs(openspec): fold Round-A review (6.2/10 REVISE) blockers B1-B7 into the v14 plan B1 show spec names join the six-surface contract set with a Spec Requirement Names delta and show-suite fixture scope. B2 the kernel projectWorkflowStatus rebuild (single + batch) is named as the owner that strips status warnings today; red is verbatim-end-to-end through the kernel. B3 both store edit-root branches gain a normative contract and executable fixtures. B4 the boundary executable unifies to the just-retired 1.13.2 line; v12 boundary artifacts retire. B5 the matrix becomes a file-level list (11 migrate + 2 new + boundary) matching checkpoints. B6 inherited contradicting scenario titles recorded with a proposed spec-hygiene follow-up. B7 the full verbatim skip_design warning is re-captured and embedded. --- .../loop/checkpoints.md | 13 ++- .../loop/implementation.md | 30 +++++- .../loop/intake.md | 14 +-- .../loop/research-plan.md | 91 +++++++++++++------ .../specs/openspec-cli-integration/spec.md | 51 ++++++++++- references/openspec-1.14.0-report.md | 32 ++++--- 6 files changed, 176 insertions(+), 55 deletions(-) diff --git a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md index 0925dc7c..3b9b5235 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md @@ -17,8 +17,10 @@ Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下, ## CP1 — Window and contracts (Slices 1-2) - [ ] Compat window rotated; boundary red recorded (stable 1.13.x → unsupported); mirrors + tests green. -- [ ] Apply-task source locations, status warnings, list archived/nested/warnings, version envelope typed - through contract + projection schemas; reds recorded; focused suites green; core typecheck. +- [ ] Apply-task source locations, status warnings (through the kernel `projectWorkflowStatus` rebuild on + single AND batch paths — decode passthrough is stripped there today), the new list `archived` member, + show spec names, and the version envelope typed through contract + projection chains; kernel-rebuild + red recorded verbatim-end-to-end; focused suites green; core typecheck. ## CP2 — Registry (Slice 3) @@ -28,9 +30,10 @@ Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下, ## CP3 — Web + fixtures (Slices 4-5) - [ ] Status warnings direct-plane summary row; web focused tests green. -- [ ] Pin guards rotated; `openspec-cli-114` alias with lockfile+shim evidence; full v14 positive matrix - (9 migrated + version-report + change-list-inventory) green with `--version` provenance; v12 boundary - retained; retired v13 positives deleted with proof; mutation red recorded. +- [ ] Pin guards rotated; `openspec-cli-114` alias with lockfile+shim evidence; full v14 positive matrix — + 11 migrated suites + `v14-version-report` + `v14-change-list-inventory` (13 files) — green with + `--version` provenance; 1.13.2 boundary suite proves the just-retired line rejected (v12 boundary/alias + retired with proof); store edit-roots both branches asserted executably; mutation red recorded. ## CP4 — Alignment (Slice 6) diff --git a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md index f70267fb..ae09a06d 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md @@ -30,4 +30,32 @@ focused tests via `pnpm --filter exec vitest run ` (never `test --`) ## Review rounds -(pending) +### Round-A review disposition (herdr v14-change-reviewer, gpt-5.6-terra xhigh, 2026-10-02) — 6.2/10, REVISE + +Worked 16m 37s. Seven blockers, all verified against source and adopted: + +1. **B1 show names missing** → report/intake recount to six surfaces; Slice 2 owns + `CliSpecRequirementSchema` + scenario `name` through the Spec Catalog chain; new "Spec Requirement Names + Contract" delta; show-name fixture evidence assigned to the widened `show` suite (both surfaces). +2. **B2 status warnings lost at the kernel rebuild** → `opsx-kernel.ts` `projectWorkflowStatus` (the + explicit `ChangeStatusSchema.parse({...})` copy used by BOTH single and batch paths) added to Slice 2 + owners; red driven from a real 1.14 payload through the kernel to the Server-visible output, verbatim. +3. **B3 edit-roots lacked normative coverage** → new "Status Action Context Edit Roots Contract" (both + branches) + executable two-branch fixtures in the `default-store` counterpart beside the non-store + control; report documents both branches. +4. **B4 boundary executable contradiction** → unified everywhere: the just-retired **1.13.2** + (`openspec-cli-113` alias) becomes the v14 boundary line; v12 boundary suite + `openspec-cli-112` alias + + v12 helper retire; compat unit tests still cover 1.13.0/1.13.2 rejection directly. +5. **B5 matrix counts unresolvable** → file-level matrix: 11 migrate (apply-readiness keeps 1132 tracking + cases as regression + gains sourcePath/line), 2 new, boundary per B4; checkpoints match (13 files). +6. **B6 contradictory inherited scenario titles** → recorded as known legacy in Spec hygiene with a + proposed separate spec-hygiene change (verified rename process on an isolated copy; Owner decision); + new scenarios never carry contradicting titles. +7. **B7 "verbatim" warning was truncated** → complete raw string re-captured from the disposable fixture + (including the `skip_design is not a supported key; only skip_specs exists...` tail) and embedded in the + report as the fixture contract string. + +Non-blocking corrections adopted: `nested`/list-`warnings` reclassified as 1.13.1 regression items (the one +new list member is `archived`); 1132 baseline verified accurate by the reviewer (no overlap). + +Pending: Round-B re-review. diff --git a/openspec/changes/target-openspec-cli-114-line/loop/intake.md b/openspec/changes/target-openspec-cli-114-line/loop/intake.md index edb67975..c08e1f90 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/intake.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/intake.md @@ -12,8 +12,9 @@ Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下, ## Objective Scope Ship OpenSpecUI 14 for OpenSpec CLI 1.14.x: pinned reference `v1.14.0` (`94ca9c1e`), evidence report, v14 -admission window, typed contracts for the five moved JSON surfaces (version command, apply task source -locations, list archived/nested/warnings, status warnings, store edit roots), the ten-entry Agent registry +admission window, typed contracts for the six moved JSON surfaces (version command, apply task source +locations, the new list `archived` member — nested/list-warnings are 1.13.1 regression items that only +migrate fixtures, status warnings, show spec requirement/scenario names, store edit roots), the ten-entry Agent registry rotation (shared `.agents` root now also `amp`/`gsd`; Kilo commands → `.kilo/command/`; IBM Bob label), generator staleness to 1.14.0, the full pinned fixture matrix with 1.13.0 boundary negatives, and README / AGENTS / Changeset major preparation. @@ -34,10 +35,11 @@ AGENTS / Changeset major preparation. product decision. 5. **Status `warnings` render on the direct plane following the v13 summary-row law** (one collapsible summary row, verbatim expansion, CLI attribution) — advisory, never a gate. -6. **Fixture rotation:** `openspec-cli-114` positive line; retained `openspec-cli-112` stays as the - below-admitted boundary executable (proving rejections against the v14 gate); `openspec-cli-113` alias is - retained for provenance but not required as a second boundary line (v13 suites already prove the 1.13 - line; the boundary class needs one retired executable — 1.12.0 keeps that role, cheapest reuse). +6. **Fixture rotation:** `openspec-cli-114` positive line; the **just-retired 1.13.2 executable + (`openspec-cli-113` alias) becomes the below-admitted boundary line** proving the v14 gate rejects the + series it revoked (Round-A B4: a two-generations-old 1.12.0 proves only that "an old version is + rejected", not the v13 boundary). The v12 boundary suite and `openspec-cli-112` alias retire with their + helper; compat unit tests still cover 1.13.0/1.13.2 rejection and 1.14.0 acceptance directly. ## Constraints diff --git a/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md b/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md index 2379e083..703fd8d4 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md @@ -13,6 +13,14 @@ Evidence: `references/openspec-1.14.0-report.md` (pinned `v1.14.0` @ `94ca9c1e`; ## Spec hygiene +**Known legacy (recorded, not silently kept):** several inherited scenario titles read "Accept " while their bodies (correctly) specify blocked-by-default for OpenSpecUI 14. Titles are scenario +identity that archive refuses to drop, and the validator offers no verified rename path today; renaming +them speculatively risks archive rejection. Disposition: bodies remain the normative text; this change +proposes a separate spec-hygiene change to verify a rename/retirement process on an isolated copy before +touching live specs (Owner decision). New scenarios added by this change never carry a title whose meaning +their body contradicts. + Scenario titles are current-spec identity (archive refuses drops); bodies carry v14 semantics. Historical titles ("Zed is a 1.10-line skills-only target", "SourceCraft enters at 1.12") describe introduction facts; `minCliSeries` values never rotate while `AgentCliSeries` does. @@ -25,26 +33,39 @@ and `scripts/setup-example.ts` mirrors with boundary assertions (1.13 rejected / Red: constants-first run against current assertions (record 5+ failures incl. stable 1.13.x → unsupported). Green: compat + diagnose focused tests; core typecheck. -## Slice 2 — Typed contracts through the full projection chains +## Slice 2 — Typed contracts through the full projection chains (Round-A B1/B2 hardened) -Owners (complete chains, v13-B1 lesson): +Owners (complete chains, v13-B1 lesson; every "projection" claim names the physical copy site): - Apply tasks: `cli-contracts/workflow.ts` `CliApplyTaskSchema` + `opsx-types.ts` `ApplyTaskSchema` gain optional `sourcePath`/`line`; contract tests pin the executed 1.14 payload (both members present, absolute path, 1-based line) plus an older-shape payload without them. -- Status warnings: `CliWorkflowStatusSuccessSchema` (batch + single, whichever owns the envelope) and the - opsx status input/projection schemas gain optional top-level `warnings: string[]`; tests pin the executed - `skip_design` warning verbatim. -- List members: the change-list CLI contract gains per-entry optional `archived`/`nested` and an optional - top-level `warnings` array (`nested_change_directory` finding); executed `--archived`/`--all` documents as - fixtures. +- Status warnings — **full chain including the kernel rebuild**: `CliWorkflowStatusSuccessSchema` (single) and + the batch envelope schema gain optional top-level `warnings: string[]`; `opsx-types.ts` status + input/projection schemas likewise; and `packages/core/src/opsx-kernel.ts` `projectWorkflowStatus` (the + explicit `ChangeStatusSchema.parse({...})` object rebuild used by BOTH the single and batch status paths) + MUST copy `data.warnings` through — decode-time `.passthrough()` is stripped there today (Round-A B2). + Server transport (`planning-cli-projection-service`/router status procedures) carries the member by + schema, pinned by test. Red: a real 1.14 status payload (the executed `skip_design` warning, full string + in the report) driven through the Kernel single AND batch paths, asserting the verbatim string at the + Server-visible projection output — fails today; absent member stays absent (UI synthesizes nothing). +- List members: the change-list CLI contract gains the ONE new member `archived` (per-entry optional); + `nested` and top-level `warnings` are 1.13.1 members already covered by the existing nested-directory + contract — v14 only migrates their fixtures (no second ownership). +- Show spec names (Round-A B1): `cli-contracts/workflow.ts` `CliSpecRequirementSchema` and its scenario + schema gain optional `name`; the Spec Catalog chain (`CliShowSpecDocument` consumers, server spec + transports) preserves both names by CLI value. Tests: a real 1.14 `show --type spec --json` + document asserts requirement and scenario names; an old-shape document without names still parses with + the members absent; a non-string `name` is rejected rather than silently accepted. - Version command: a new typed contract for `{schemaVersion:1, version, install, update?}` with an executor method (`version({check})`); no capability flag (admitted-window invariant). -- Store edit roots: no schema change (same array shape); fixture expectations rotate to - `[implementationRoot, projectRoot]` semantics in Slice 6 fixtures only. +- Store edit roots: no schema change (same array shape) — BOTH branches are executable-fixture obligations + in Slice 5 (declaring-project branch `[implementationRoot, projectRoot]`; no-declaring-project branch + `[projectRoot]` with the ask-the-user constraint; plus the non-store control). -Red: schema-drop reds per surface (parse executed payload through the projection schema asserting the member -survives; fails today). Green: the three contract files + opsx-types tests; core typecheck. +Red: schema-drop reds per surface (parse executed payloads through the projection schema asserting the +member survives; fails today) plus the kernel-rebuild red above. Green: contract + opsx-types + kernel +focused tests; core typecheck. ## Slice 3 — Agent registry rotation (largest slice) @@ -78,20 +99,38 @@ Red: component test asserting the row before the render change. Green: focused w Owners: `scripts/prepare-openspec-reference.mjs` EXPECTED_COMMIT → `94ca9c1e...`; `upstream-contract-regression.test.ts` pin rotation; `packages/web/scripts/w2-project-binding-playwright.ts` pin; `packages/core/package.json` alias `"openspec-cli-114": "npm:@fission-ai/openspec@1.14.0"` (integrator -lockfile); `__tests__/official-cli-v14-fixtures.ts` helper. **Full positive matrix migrates to v14** — the -twelve v13 suites (workflow, default-store, nested-spec, show-diff, validation-full, validation-findings, -batch-status, agent-delivery, apply-readiness, nested-change, task-reading, plus any apply-tracking suite -from the 1132 line) get v14 counterparts, with apply-readiness extended to assert task -`sourcePath`/`line`, **plus two new suites**: `v14-version-report` (no-check envelope only, per no-network -fixture discipline) and `v14-change-list-inventory` (`--archived`/`--all` archived flag, nested warnings -shape). Boundary: rotate `official-cli-v12-boundary-fixtures.test.ts` role text to the v14 gate (1.12.0 -stays the retired executable — 1.13.2 remains the admitted-line provenance through the v13 suites' archive); -retire the v13 positive suites with their counterparts (deletion proof: `git diff --name-status` + orphan -scan; the v13 helper is deleted unless the boundary suite still consumes it — it consumes v12). Mutation -red: bins-map → wrong alias fails `--version` identity. - -Red: alias-resolution failure before install; pin-guard failure before rotation. -Green: joint v14 gate + boundary + upstream-contract-regression; core typecheck; lockfile diff + shim check. +lockfile); `__tests__/official-cli-v14-fixtures.ts` helper. + +**File-level matrix (Round-A B5 — counts resolve to exactly this list):** + +Migrate (11 existing `official-cli-v13-*.test.ts`, each becoming its v14 counterpart on the 1.14.0 +executable with `--version` provenance; `apply-readiness` KEEPS the 1132 tracking cases — +`taskTrackingConfigured`/`unavailableTrackingFiles` — as regression, not re-implementation — and additionally +asserts task `sourcePath`/`line`): `agent-delivery`, `apply-readiness`, `batch-status`, `default-store`, +`nested-change`, `nested-spec`, `show-diff`, `task-reading`, `validation-findings`, `validation-full`, +`workflow`. + +New (2): `v14-version-report` (no-check envelope only, per no-network fixture discipline) and +`v14-change-list-inventory` (`--archived` and `--all`: archived entries `archived:true`, mixed `--all` +entries `archived:false/true`; nested regression kept in `nested-change`'s counterpart). Show-name contract +evidence lives in the `show-diff` counterpart (scope widened to the `show` suite) — it runs BOTH +`show --json --diff` (existing) and `show --type spec --json` name assertions (new). + +Boundary (Round-A B4): the **1.13.2 executable (`openspec-cli-113` alias) proves the v14 gate rejects the +just-retired line** — `official-cli-v12-boundary-fixtures.test.ts` is replaced by +`official-cli-v13-boundary-fixtures.test.ts` (1.13.2 identity + below-admitted classification + the v13-only +fields absent); the v12 boundary suite, `openspec-cli-112` alias, and the v12 helper retire (deletion proof +`git diff --name-status` + orphan scan). Store edit-roots (Round-A B3): inside the `default-store` +counterpart (the suite that already builds a registered store), two executable scenarios assert the +declaring-project branch (`[implementationRoot, projectRoot]` order + the declaring-repo constraint) and the +no-declaring-project branch (`[projectRoot]` + ask-the-user constraint), beside the existing non-store +control. + +Red: alias-resolution failure before install; pin-guard failure before rotation; boundary suite fails while +the gate still admits 1.13 (record the ordering honestly — boundary lands after Slice 1). +Green: joint v14 gate (13 files) + boundary + upstream-contract-regression; core typecheck; lockfile diff + +shim check. Mutation red: bins-map → wrong alias fails `--version` identity. + ## Slice 6 — Cross-package alignment (four owner groups) diff --git a/openspec/changes/target-openspec-cli-114-line/specs/openspec-cli-integration/spec.md b/openspec/changes/target-openspec-cli-114-line/specs/openspec-cli-integration/spec.md index 3f795c31..4c788cf0 100644 --- a/openspec/changes/target-openspec-cli-114-line/specs/openspec-cli-integration/spec.md +++ b/openspec/changes/target-openspec-cli-114-line/specs/openspec-cli-integration/spec.md @@ -152,11 +152,11 @@ explicit consumer requests it, and no UI surface is obligated by this contract. ### Requirement: Change List Inventory Contract -The CLI change-list contract SHALL preserve the OpenSpec 1.14 inventory members: per-entry optional -`archived` (present only under `--archived`/`--all`) and `nested` (namespace-folder findings), and an -optional top-level `warnings` array (`nested_change_directory` findings). OpenSpecUI's Archive list SHALL -keep its reactive-filesystem projection owner; the CLI members are typed evidence, not a mandated data-source -switch. +The CLI change-list contract SHALL carry the OpenSpec 1.14 per-entry optional `archived` member (present +only under `--archived`/`--all`). The 1.13.1 `nested` and top-level `warnings` members stay covered by the +existing Change List Nested Directory contract; this line only migrates their fixtures. OpenSpecUI's Archive +list SHALL keep its reactive-filesystem projection owner; the CLI members are typed evidence, not a mandated +data-source switch. #### Scenario: Archived entries decode with the archived flag @@ -214,3 +214,44 @@ locations; they are CLI-owned evidence for which file and line each checkbox liv - **WHEN** the payload crosses the typed contract - **THEN** parsing SHALL succeed with both members absent - **AND** no default location SHALL be fabricated + + +### Requirement: Spec Requirement Names Contract + +The `show --type spec --json` contract SHALL carry the OpenSpec 1.14 optional `name` member on each +requirement and each scenario, preserved by CLI value through the spec-document contract and the Spec +Catalog projection chain. This surface is distinct from `show --json --diff`. + +#### Scenario: Names decode on a 1.14 spec document + +- **GIVEN** an admitted 1.14 CLI returns a spec document whose requirements and scenarios carry names +- **WHEN** the payload crosses the typed contract and its projection chain +- **THEN** requirement names and scenario names SHALL be preserved by CLI value with provenance + +#### Scenario: Older documents still parse without names + +- **GIVEN** a spec document whose members lack `name` +- **WHEN** the payload crosses the typed contract +- **THEN** parsing SHALL succeed with the members absent +- **AND** a non-string `name` SHALL be rejected rather than silently accepted as typed evidence + +### Requirement: Status Action Context Edit Roots Contract + +Store-backed `status --json` `actionContext.allowedEditRoots` SHALL be preserved exactly as the CLI reports +it in both branches: with a declaring project on the current path, the array lists the implementation root +and the project root with the declaring-repo constraint; without one, the array lists the project root with +the constraint telling the agent to ask which repository to edit. OpenSpecUI SHALL NOT branch production +logic on the root count. + +#### Scenario: Declaring project joins the edit roots + +- **GIVEN** a store-backed change whose store is declared by a project on the current path +- **WHEN** status is projected +- **THEN** `allowedEditRoots` SHALL preserve the CLI-reported roots and constraint verbatim + +#### Scenario: No declaring project asks the user + +- **GIVEN** a store-backed change with no declaring project on the current path +- **WHEN** status is projected +- **THEN** `allowedEditRoots` SHALL preserve the CLI-reported single root and the ask-the-user constraint +- **AND** OpenSpecUI SHALL NOT synthesize an implementation root diff --git a/references/openspec-1.14.0-report.md b/references/openspec-1.14.0-report.md index a60fbf68..e76622df 100644 --- a/references/openspec-1.14.0-report.md +++ b/references/openspec-1.14.0-report.md @@ -23,7 +23,7 @@ The v13 report pre-declared 1.14 admission as "a separately verified decision: e or a new major". The verified delta selects the **new major**: ten new Agent registry entries (40→50 tools, including two new sharers of the `.agents` skills root), one new CLI command family (`openspec version`), archived changes entering the `list` protocol, apply tasks gaining mandatory source locations, and a new -top-level `status --json` `warnings` member. Five JSON contract surfaces moved — far beyond a patch bridge — +top-level `status --json` `warnings` member. Six JSON contract surfaces moved (version command, apply task locations, list `archived`, status warnings, show names, store edit roots) — far beyond a patch bridge — and the one-line-per-series cadence (v7→1.7 … v13→1.13) continues. `1.15` is not pre-claimed. Within the 1.13 window, upstream also shipped 1.13.1/1.13.2; the v13 gate admits the whole stable 1.13 series @@ -58,7 +58,7 @@ isolated fixture. | --- | --- | --- | --- | | `instructions apply --json` tasks | `{id, description, done}` | each task adds **`sourcePath`** (absolute) and **`line`** (1-based), always present | Apply task contract gains the two members; projection may surface them as evidence | | `list --archived --json` / `--all` | flag rejected | archived entries returned; every entry carries **`archived: boolean`** (present only under `--archived`/`--all`) | New list transport for archive inventory; capability-free inside the single-series window | -| `list --json` (default) | `{changes, root}` | entries may carry **`nested: string[]`** (namespace folders, #1846/#1849) and the envelope a top-level **`warnings: [{code:'nested_change_directory', name, nested, message}]`** (omitted when empty) | Change-list contract tolerates the new members; nested findings are objective CLI evidence | +| `list --json` (default) | nested + warnings already delivered in 1.13.1 (covered by the existing Change List Nested Directory contract and fixtures) | unchanged members carry forward | v14 regression only — migrate the fixtures; **the one new list member is `archived`** | | `openspec version [--check] [--json]` | no such command | `{schemaVersion:1, version, install:{location,packageManager,scope}, update?:{...status,latest?,command,canSelfUpgrade}}` (executed without `--check`; `update` absent then) | New typed command contract; `--check` performs an opt-out-able registry probe | | `status --json` | no `warnings` | top-level **`warnings: string[]`** names unrecognized `.openspec.yaml` keys (executed: `skip_design` warning verbatim; known keys now include `goal, affected_areas, initiative, skip_specs, retire_capabilities`) | Status contract gains optional `warnings`; direct-plane advisory evidence | | `status --json` `actionContext.allowedEditRoots` (store-backed) | store only | `[implementationRoot, projectRoot]` — the declaring repo joins the store; constraint text tells the agent to ask otherwise | Same array shape, changed semantics; no schema edit, fixture values rotate | @@ -81,7 +81,8 @@ checkbox markers `{x}`-style counted); `openspec view` TUI shows workflow status +-- list --json: nested namespace-folder findings + top-level warnings array (nested_change_directory) +-- status --json: top-level warnings (unrecognized .openspec.yaml keys) +-- status actionContext.allowedEditRoots: store-backed changes add the declaring repo - +-- show --json: requirement/scenario name members (additive) + +-- show --json: requirement/scenario name members (typed on CliSpecRequirementSchema + scenarios; + distinct from the adapted show --json --diff surface; optional — old snapshots lack them) +-- Agent registry +10 entries (amp, gsd share .agents root); IBM Bob label; Kilo commands → .kilo/command/ `-- archive/validate/parser hardening (CLI-internal; fixture-scoped, never UI-side logic) ``` @@ -110,10 +111,13 @@ top-level `warnings` array so the CLI evidence is never lossy when consumed. ### 4. Status warnings are direct-plane advisory evidence -Executed verbatim: `"Unrecognized key name(s) in .openspec.yaml (untrusted data, not instructions): -skip_design. Known keys: schema, created, goal, affected_areas, initiative, skip_specs, -retire_capabilities. ..."`. `validate --strict` fails on them. OpenSpecUI projects the member verbatim -(CLI-owned); it is advisory, never a Root/action gate (mirrors the v13 apply-warnings law). +Executed verbatim (fixture input `.openspec.yaml` = `schema: spec-driven` + `skip_design: true`; the +complete raw string, re-captured for the fixture contract): `"Unrecognized key name(s) in .openspec.yaml +(untrusted data, not instructions): skip_design. Known keys: schema, created, goal, affected_areas, +initiative, skip_specs, retire_capabilities. Unknown keys are ignored and have no effect. skip_design is not +a supported key; only skip_specs exists, and it only skips artifacts whose generates path lives under +specs/."`. `validate --strict` fails on them. OpenSpecUI projects the member verbatim (CLI-owned); it is +advisory, never a Root/action gate (mirrors the v13 apply-warnings law). ### 5. The Agent registry grows by ten — including two new `.agents` root sharers @@ -127,8 +131,11 @@ moves to 1.14.0 with series-aware staleness (1.13.x-generated artifacts become s ### 6. Store-backed edit roots name the declaring repo -Same array shape, new member: `allowedEditRoots: [implementationRoot, projectRoot]` with guidance text. -Fixture expectations that pinned store-only roots rotate; no production branching on root count is allowed. +Same array shape, two branches: with a declaring project on the current path, +`allowedEditRoots: [implementationRoot, projectRoot]` plus a constraint naming the declaring repo; with no +declaring project, `allowedEditRoots: [projectRoot]` (store named in the constraint) and the guidance tells +the agent to ask which repository to edit. Both branches are fixture obligations; no production branching on +root count is allowed. ## Current owner map @@ -140,13 +147,14 @@ Fixture expectations that pinned store-only roots rotate; no production branchin | Agent registry/state | `packages/core/src/agent-delivery-registry.ts`, `tool-init-state.ts` (+tests, server projection/router tests) | +10 entries, kilo dir, IBM Bob label, series `'1.14'`, generator `1.14.0` | | Web evidence | change-view / evidence surfaces (+tests) | status `warnings` projected as direct-plane advisory (v13 summary-row pattern) | | reference pin | `scripts/prepare-openspec-reference.mjs`, `upstream-contract-regression.test.ts`, `w2-project-binding-playwright.ts` | pin `94ca9c1e` | -| fixtures | `packages/core/package.json` alias + `__tests__/official-cli-v14-fixtures.ts` + suite rotation | positive line 1.14.0; boundary 1.13.0 | +| fixtures | `packages/core/package.json` alias + `__tests__/official-cli-v14-fixtures.ts` + suite rotation | positive line 1.14.0; boundary = the just-retired 1.13.2 executable (`openspec-cli-113` alias) proving the v14 gate rejects the line it revoked; the retired `openspec-cli-112` and the v12 boundary suite are retired with their helper | | release | Changesets + README law (repo en/zh + CLI package) | major v14 | ## Scope boundary -In scope: the v14 admission window; typed contracts for version/list/status/apply-task members and their -projection; the ten-entry registry rotation with shared-root scenario updates; status `warnings` direct-plane +In scope: the v14 admission window; typed contracts for version/list/status/apply-task/show-name members +and their projection (status warnings must survive the kernel `projectWorkflowStatus` rebuild — single and +batch paths — or they are lost after decode); the ten-entry registry rotation with shared-root scenario updates; status `warnings` direct-plane presentation following the v13 summary-row law; pinned fixture rotation with 1.13.0 boundary negatives; README/AGENTS/Changeset major preparation. From 0ad24aa00f8429f3ce51c4e63c1abcdf1ff950dd Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 2 Oct 2026 03:00:49 +0800 Subject: [PATCH 03/12] =?UTF-8?q?docs(openspec):=20fold=20Round-B=20residu?= =?UTF-8?q?als=20=E2=80=94=20boundary=20semantics,=20v12=20alias=20retenti?= =?UTF-8?q?on,=20title-identity=20note?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B1: the boundary suite asserts 1.13.2 identity + unsupported + 1.14-only members absent (never v13-era absence); residual 1.13.0 wordings synced. B2: the openspec-cli-112 alias stays for its two historical generator test consumers; retirement narrows to the v12 boundary suite + helper. B3: the inherited 'Accept ...' titles get an explicit identity note in the requirement intro (bodies normative; rename mechanism proposed separately). B4: report owner map gains the show-names row; list row/count wording unified to six surfaces with archived as the one new list member. --- .../loop/checkpoints.md | 6 +++-- .../loop/implementation.md | 24 +++++++++++++++++++ .../loop/intake.md | 18 +++++++++----- .../loop/research-plan.md | 15 ++++++++---- .../specs/openspec-cli-integration/spec.md | 4 ++++ references/openspec-1.14.0-report.md | 7 +++--- 6 files changed, 58 insertions(+), 16 deletions(-) diff --git a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md index 3b9b5235..ba2ec04d 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md @@ -32,8 +32,10 @@ Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下, - [ ] Status warnings direct-plane summary row; web focused tests green. - [ ] Pin guards rotated; `openspec-cli-114` alias with lockfile+shim evidence; full v14 positive matrix — 11 migrated suites + `v14-version-report` + `v14-change-list-inventory` (13 files) — green with - `--version` provenance; 1.13.2 boundary suite proves the just-retired line rejected (v12 boundary/alias - retired with proof); store edit-roots both branches asserted executably; mutation red recorded. + `--version` provenance; the 1.13.2 boundary suite proves the just-retired line rejected (identity + + unsupported classification + 1.14-only members absent; v12 boundary suite and v12 helper retired with + proof while the `openspec-cli-112` alias stays for its two historical consumers); store edit-roots + both branches asserted executably; mutation red recorded. ## CP4 — Alignment (Slice 6) diff --git a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md index ae09a06d..1d4e0ec9 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md @@ -59,3 +59,27 @@ Non-blocking corrections adopted: `nested`/list-`warnings` reclassified as 1.13. new list member is `archived`); 1132 baseline verified accurate by the reviewer (no overlap). Pending: Round-B re-review. + +### Round-B review disposition (herdr v14-change-reviewer, gpt-5.6-terra xhigh, 2026-10-02) — 7.4/10, REVISE + +Worked 12m 27s. Round-A B2/B3/B7 confirmed closed; four residual blockers, all documentation-consistency +class, adopted: + +1. **B1 boundary text self-contradiction** — the "v13-only fields absent" assertion was semantically wrong + (1.13.2 IS a v13 CLI and legitimately emits taskTrackingConfigured etc.). Rewritten everywhere: the + boundary asserts 1.13.2 identity + unsupported classification + **1.14-only members absent** (task + sourcePath/line, list archived, show names, status warnings), never v13-era absence. Residual 1.13.0 + wordings in intake/report synced to 1.13.2. +2. **B2 v12 alias has live consumers** — `tool-init-state.test.ts` and `agent-command-content.test.ts` + resolve `openspec-cli-112` as historical generator evidence. Retirement narrowed to the v12 boundary + suite + v12 helper; the npm alias stays; pre-delete rg check named in the plan. +3. **B3 spec-hygiene disposition honesty** — the Round-A note overstated ("verified" nothing). This change + records the inherited title/body contradiction as known legacy, annotates the affected scenario BODIES + with an explicit "Historical title retained for scenario identity; this line is retired and blocked in + OpenSpecUI 14" first line (bodies are normative and editable today), and proposes the rename-mechanism + verification as a separate Owner-gated change. v14 merge does not block on it; the Owner may veto. +4. **B4 report/intake count drift** — owner map gains the show-names row; list row says "the one new member + archived (nested/warnings are 1.13.1 regression fixtures)"; intake decision 1 "five"→"six"; decision 2 + rewritten to name this line's new members vs migrated ones. + +Pending: Round-C re-review. diff --git a/openspec/changes/target-openspec-cli-114-line/loop/intake.md b/openspec/changes/target-openspec-cli-114-line/loop/intake.md index c08e1f90..ac37ac45 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/intake.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/intake.md @@ -16,18 +16,19 @@ admission window, typed contracts for the six moved JSON surfaces (version comma locations, the new list `archived` member — nested/list-warnings are 1.13.1 regression items that only migrate fixtures, status warnings, show spec requirement/scenario names, store edit roots), the ten-entry Agent registry rotation (shared `.agents` root now also `amp`/`gsd`; Kilo commands → `.kilo/command/`; IBM Bob label), -generator staleness to 1.14.0, the full pinned fixture matrix with 1.13.0 boundary negatives, and README / +generator staleness to 1.14.0, the full pinned fixture matrix with the just-retired 1.13.2 executable as the boundary negative, and README / AGENTS / Changeset major preparation. ## Owner-default decisions (vetoable in review) -1. **OpenSpecUI 14, single-series window `>=1.14.0 <1.15.0`** (new major, not a 13.x widening) — five moved +1. **OpenSpecUI 14, single-series window `>=1.14.0 <1.15.0`** (new major, not a 13.x widening) — six moved JSON contract surfaces plus a ten-entry registry delta exceed any patch-bridge reading; the one-line-per-series cadence continues; `1.15` is not pre-claimed. No 1.13 widening debt exists (the v13 gate already admits all stable 1.13.x, including 1.13.1/1.13.2). 2. **No new capability flags.** Inside the single-series window every admitted CLI carries the new members; - contracts gain optional typed members instead (`archived?`, `nested?`, `warnings?`, task `sourcePath?`/ - `line?`), mirroring the v13 missingPrerequisites/warnings precedent. + contracts gain optional typed members instead (this line's new members: list `archived?`, status + `warnings?`, task `sourcePath?`/`line?`, show spec `name?`; `nested?`/list-`warnings?` are 1.13.1 + members whose fixtures merely migrate), mirroring the v13 missingPrerequisites/warnings precedent. 3. **Archive list keeps the reactive-filesystem adapter as its projection owner this line.** Switching the archive inventory to `list --archived` is an architectural follow-up (reactive-model change), explicitly out of scope; the typed list contract still carries the new members so CLI evidence is never lossy. @@ -38,8 +39,13 @@ AGENTS / Changeset major preparation. 6. **Fixture rotation:** `openspec-cli-114` positive line; the **just-retired 1.13.2 executable (`openspec-cli-113` alias) becomes the below-admitted boundary line** proving the v14 gate rejects the series it revoked (Round-A B4: a two-generations-old 1.12.0 proves only that "an old version is - rejected", not the v13 boundary). The v12 boundary suite and `openspec-cli-112` alias retire with their - helper; compat unit tests still cover 1.13.0/1.13.2 rejection and 1.14.0 acceptance directly. + rejected", not the v13 boundary). The boundary asserts `--version` = 1.13.2 identity, the unsupported + classification, and that the **1.14-only members** (task `sourcePath`/`line`, list `archived`, show + names, status `warnings`) are absent from 1.13.2 output — it never claims v13-era members are absent. + The v12 boundary suite and the v12 fixture helper retire; the `openspec-cli-112` npm alias STAYS + installed (Round-B B2: `tool-init-state.test.ts` and `agent-command-content.test.ts` consume it as + historical generator evidence). Compat unit tests still cover 1.13.0/1.13.2 rejection and 1.14.0 + acceptance directly. ## Constraints diff --git a/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md b/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md index 703fd8d4..d1c84aaf 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md @@ -116,11 +116,16 @@ entries `archived:false/true`; nested regression kept in `nested-change`'s count evidence lives in the `show-diff` counterpart (scope widened to the `show` suite) — it runs BOTH `show --json --diff` (existing) and `show --type spec --json` name assertions (new). -Boundary (Round-A B4): the **1.13.2 executable (`openspec-cli-113` alias) proves the v14 gate rejects the -just-retired line** — `official-cli-v12-boundary-fixtures.test.ts` is replaced by -`official-cli-v13-boundary-fixtures.test.ts` (1.13.2 identity + below-admitted classification + the v13-only -fields absent); the v12 boundary suite, `openspec-cli-112` alias, and the v12 helper retire (deletion proof -`git diff --name-status` + orphan scan). Store edit-roots (Round-A B3): inside the `default-store` +Boundary (Round-A B4, Round-B B1/B2): the **1.13.2 executable (`openspec-cli-113` alias) proves the v14 +gate rejects the just-retired line** — `official-cli-v12-boundary-fixtures.test.ts` is replaced by +`official-cli-v13-boundary-fixtures.test.ts` asserting: `--version` = 1.13.2 identity, the unsupported +classification against the v14 gate, and that the **1.14-only members are absent from 1.13.2 output** +(task `sourcePath`/`line`, list `archived`, show names, status `warnings`) — it never claims v13-era members +(taskTrackingConfigured etc.) are absent, they are 1.13.2's own. The v12 boundary suite and the v12 fixture +helper retire (deletion proof `git diff --name-status` + orphan scan); **the `openspec-cli-112` npm alias +stays installed** — `tool-init-state.test.ts` and `agent-command-content.test.ts` consume it as historical +generator evidence outside this matrix (Round-B B2); pre-delete check `rg -n 'openspec-cli-112' +packages/core/src` must show only those two historical consumers. Store edit-roots (Round-A B3): inside the `default-store` counterpart (the suite that already builds a registered store), two executable scenarios assert the declaring-project branch (`[implementationRoot, projectRoot]` order + the declaring-repo constraint) and the no-declaring-project branch (`[projectRoot]` + ask-the-user constraint), beside the existing non-store diff --git a/openspec/changes/target-openspec-cli-114-line/specs/openspec-cli-integration/spec.md b/openspec/changes/target-openspec-cli-114-line/specs/openspec-cli-integration/spec.md index 4c788cf0..1663c9a0 100644 --- a/openspec/changes/target-openspec-cli-114-line/specs/openspec-cli-integration/spec.md +++ b/openspec/changes/target-openspec-cli-114-line/specs/openspec-cli-integration/spec.md @@ -14,6 +14,10 @@ Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下, OpenSpecUI 14 SHALL classify stable OpenSpec CLI `>=1.14.0 <1.15.0` as supported, current, and recommended. OpenSpec CLI `<1.14.0` (including the whole 1.13.x window that OpenSpecUI 13 admitted, and every older line), every prerelease, `>=1.15.0`, and an unparseable version SHALL be incompatible and blocked by default. + +Scenario titles below are inherited scenario identity: an "Accept ..." title names that line's historical +admission era, while every scenario body states the OpenSpecUI 14 classification (retired and blocked). +Bodies are the normative text; a separate spec-hygiene change may verify a title-rename mechanism later. When an incompatible executable is available, the mismatch Dialog MAY expose `Skip version check`; that action SHALL bypass only the current Web page runtime's admission gate and SHALL NOT change the detected version, compatibility evidence, CLI payloads, downstream errors, or product support claim. diff --git a/references/openspec-1.14.0-report.md b/references/openspec-1.14.0-report.md index e76622df..22551b5b 100644 --- a/references/openspec-1.14.0-report.md +++ b/references/openspec-1.14.0-report.md @@ -106,7 +106,7 @@ checkbox lives in"; OpenSpecUI must not re-derive or second-guess them. `list --archived --json` returns archived entries with the same per-entry shape plus `archived: true`; `--all` mixes both. OpenSpecUI's **Archive list keeps its reactive-filesystem adapter as the projection owner** this line; switching the archive inventory to the CLI transport is a separate architectural change -(out of scope, recorded below). The typed `list` contract nonetheless gains `archived?`, `nested?`, and the +(out of scope, recorded below). The typed `list` contract gains the one new member `archived?` (`nested?`/list-`warnings?` are 1.13.1 members whose fixtures migrate); the top-level `warnings` array so the CLI evidence is never lossy when consumed. ### 4. Status warnings are direct-plane advisory evidence @@ -142,7 +142,8 @@ root count is allowed. | Surface | Primary production owner | v14 change | | --- | --- | --- | | compatibility gate | `packages/core/src/openspec-compat.ts` (+test, `scripts/diagnose-cli-runner.mjs`(+test), `scripts/setup-example.ts`) | v14 ranges, NEXT_SERIES 1.15.0, mirrors | -| workflow JSON contracts | `packages/core/src/cli-contracts/workflow.ts` (+test) | apply task `sourcePath`/`line`; status top-level `warnings`; list `archived`/`nested`/`warnings`; version envelope contract | +| workflow JSON contracts | `packages/core/src/cli-contracts/workflow.ts` (+test) | apply task `sourcePath`/`line`; status top-level `warnings`; the new list member `archived` (nested/warnings are 1.13.1 regression fixtures); version envelope contract | +| show-spec names | `cli-contracts/workflow.ts` `CliSpecRequirementSchema` (+scenario) → Spec Catalog projection → server show-spec transport | requirement/scenario `name` members typed and preserved by CLI value | | projection schemas | `packages/core/src/opsx-types.ts` (+test), `planning-cli-projection.ts` | same members through input/projection chains | | Agent registry/state | `packages/core/src/agent-delivery-registry.ts`, `tool-init-state.ts` (+tests, server projection/router tests) | +10 entries, kilo dir, IBM Bob label, series `'1.14'`, generator `1.14.0` | | Web evidence | change-view / evidence surfaces (+tests) | status `warnings` projected as direct-plane advisory (v13 summary-row pattern) | @@ -155,7 +156,7 @@ root count is allowed. In scope: the v14 admission window; typed contracts for version/list/status/apply-task/show-name members and their projection (status warnings must survive the kernel `projectWorkflowStatus` rebuild — single and batch paths — or they are lost after decode); the ten-entry registry rotation with shared-root scenario updates; status `warnings` direct-plane -presentation following the v13 summary-row law; pinned fixture rotation with 1.13.0 boundary negatives; +presentation following the v13 summary-row law; pinned fixture rotation with the just-retired 1.13.2 executable as the boundary negative; README/AGENTS/Changeset major preparation. Out of scope (follow-ups, not silently absorbed): From e1210de2aa2cbecd68703e95b589f7994ad7b5e0 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 2 Oct 2026 03:07:54 +0800 Subject: [PATCH 04/12] =?UTF-8?q?docs(openspec):=20fold=20Round-C=20factua?= =?UTF-8?q?l=20corrections=20=E2=80=94=20disposition=20honesty=20and=20rep?= =?UTF-8?q?ort=20single-source=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1: the Round-B disposition now describes the real diff (requirement-level identity note; inherited bodies unchanged; rename mechanism Owner-gated and non-blocking). C2: the report protocol tree marks nested/list-warnings as 1.13.1 carry-forward (fixtures migrate only); the fixture owner map keeps the openspec-cli-112 npm alias for its two historical consumers while retiring the v12 boundary suite and helper; the broken list sentence rewritten. --- .../loop/implementation.md | 23 +++++++++++++++---- references/openspec-1.14.0-report.md | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md index 1d4e0ec9..63bd84dc 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md @@ -74,12 +74,25 @@ class, adopted: resolve `openspec-cli-112` as historical generator evidence. Retirement narrowed to the v12 boundary suite + v12 helper; the npm alias stays; pre-delete rg check named in the plan. 3. **B3 spec-hygiene disposition honesty** — the Round-A note overstated ("verified" nothing). This change - records the inherited title/body contradiction as known legacy, annotates the affected scenario BODIES - with an explicit "Historical title retained for scenario identity; this line is retired and blocked in - OpenSpecUI 14" first line (bodies are normative and editable today), and proposes the rename-mechanism - verification as a separate Owner-gated change. v14 merge does not block on it; the Owner may veto. + adds a REQUIREMENT-LEVEL identity note in the delta spec intro (titles name historical admission eras; + bodies are the normative text and already state retired/blocked for every affected scenario); the + inherited scenario bodies themselves are unchanged. The rename-mechanism verification stays a separate + Owner-gated proposal and is NOT a precondition of this change; the Owner may veto. 4. **B4 report/intake count drift** — owner map gains the show-names row; list row says "the one new member archived (nested/warnings are 1.13.1 regression fixtures)"; intake decision 1 "five"→"six"; decision 2 rewritten to name this line's new members vs migrated ones. -Pending: Round-C re-review. +### Round-C review disposition (herdr v14-change-reviewer, gpt-5.6-terra xhigh, 2026-10-02) — 7.8/10, REVISE + +Worked 6m 15s. Boundary/alias/count fixes confirmed closed; two residuals, both factual-record corrections, +adopted: + +1. **C1** — the Round-B disposition log claimed per-scenario body annotations that were never made (the + actual change was the requirement-level identity note). Disposition text corrected to describe the real + diff; inherited bodies unchanged; rename mechanism stays Owner-gated and non-blocking. +2. **C2** — report drift: the Protocol delta tree still listed `nested`/list-`warnings` as 1.14 additions + and the fixture owner map still said the `openspec-cli-112` alias retires. Both rewritten to the + single source of truth (archived is the one new list member; alias retained for its two historical + consumers); the broken parallel sentence at the list section fixed; residual audit re-run. + +Pending: Round-D narrow re-review (documentation only). diff --git a/references/openspec-1.14.0-report.md b/references/openspec-1.14.0-report.md index 22551b5b..1ddfdd43 100644 --- a/references/openspec-1.14.0-report.md +++ b/references/openspec-1.14.0-report.md @@ -148,7 +148,7 @@ root count is allowed. | Agent registry/state | `packages/core/src/agent-delivery-registry.ts`, `tool-init-state.ts` (+tests, server projection/router tests) | +10 entries, kilo dir, IBM Bob label, series `'1.14'`, generator `1.14.0` | | Web evidence | change-view / evidence surfaces (+tests) | status `warnings` projected as direct-plane advisory (v13 summary-row pattern) | | reference pin | `scripts/prepare-openspec-reference.mjs`, `upstream-contract-regression.test.ts`, `w2-project-binding-playwright.ts` | pin `94ca9c1e` | -| fixtures | `packages/core/package.json` alias + `__tests__/official-cli-v14-fixtures.ts` + suite rotation | positive line 1.14.0; boundary = the just-retired 1.13.2 executable (`openspec-cli-113` alias) proving the v14 gate rejects the line it revoked; the retired `openspec-cli-112` and the v12 boundary suite are retired with their helper | +| fixtures | `packages/core/package.json` alias + `__tests__/official-cli-v14-fixtures.ts` + suite rotation | positive line 1.14.0; boundary = the just-retired 1.13.2 executable (`openspec-cli-113` alias) proving the v14 gate rejects the line it revoked; the v12 boundary suite and its helper retire, while the `openspec-cli-112` npm alias stays for its two historical generator test consumers | | release | Changesets + README law (repo en/zh + CLI package) | major v14 | ## Scope boundary From bc633fb90735b35a425c1ae49e3e30c42404053f Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 2 Oct 2026 03:12:44 +0800 Subject: [PATCH 05/12] docs(openspec): land the lost Round-C report fixes (write-first re-apply) The Round-C correction script aborted on a later assertion before write(), so the protocol-tree carry-forward line and the list-sentence rewrite never reached the file while the disposition claimed them. Re-applied and verified in place: the 1.13.1 carry-forward note is in the delta tree, the stale list --json row is removed, and the list paragraph is a complete sentence. --- .../loop/implementation.md | 12 +++++++++++- references/openspec-1.14.0-report.md | 7 ++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md index 63bd84dc..ce907a36 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md @@ -95,4 +95,14 @@ adopted: single source of truth (archived is the one new list member; alias retained for its two historical consumers); the broken parallel sentence at the list section fixed; residual audit re-run. -Pending: Round-D narrow re-review (documentation only). +### Round-D review disposition (herdr v14-change-reviewer, gpt-5.6-terra xhigh, 2026-10-02) — 8.1/10, REVISE + +Worked 3m 57s (narrow). C1 confirmed closed. C2 partially closed — and the reviewer caught a real +integration error on our side: the Round-C correction script aborted on a later assertion before its +`write()`, so the protocol-tree carry-forward line and the list-sentence rewrite never landed even though +the Round-C disposition claimed them (batch-edit write-after-all-assertions failure; lesson already in the +global correction log). Re-applied with write-first verification (protocol tree gains the carry-forward +line, the stale `list --json` tree row is gone, the list paragraph reads as a complete sentence); the +fixture owner-map alias fix from Round-C had landed and is confirmed. + +Pending: Round-E narrow re-review of exactly these two report passages. diff --git a/references/openspec-1.14.0-report.md b/references/openspec-1.14.0-report.md index 1ddfdd43..69c5aef3 100644 --- a/references/openspec-1.14.0-report.md +++ b/references/openspec-1.14.0-report.md @@ -78,7 +78,7 @@ checkbox markers `{x}`-style counted); `openspec view` TUI shows workflow status +-- openspec version [--check] [--json] (new command; schemaVersion 1 envelope) +-- instructions apply --json: tasks gain sourcePath + line (always present) +-- list --archived / --all: archived inventory over the same JSON envelope; per-entry archived flag - +-- list --json: nested namespace-folder findings + top-level warnings array (nested_change_directory) + +-- (1.13.1 carry-forward, fixtures migrate only: list --json nested findings + warnings array) +-- status --json: top-level warnings (unrecognized .openspec.yaml keys) +-- status actionContext.allowedEditRoots: store-backed changes add the declaring repo +-- show --json: requirement/scenario name members (typed on CliSpecRequirementSchema + scenarios; @@ -106,8 +106,9 @@ checkbox lives in"; OpenSpecUI must not re-derive or second-guess them. `list --archived --json` returns archived entries with the same per-entry shape plus `archived: true`; `--all` mixes both. OpenSpecUI's **Archive list keeps its reactive-filesystem adapter as the projection owner** this line; switching the archive inventory to the CLI transport is a separate architectural change -(out of scope, recorded below). The typed `list` contract gains the one new member `archived?` (`nested?`/list-`warnings?` are 1.13.1 members whose fixtures migrate); the -top-level `warnings` array so the CLI evidence is never lossy when consumed. +(out of scope, recorded below). The typed `list` contract gains the one new member `archived?` so the CLI +evidence is never lossy when consumed; `nested?` and the top-level `warnings` array are 1.13.1 members whose +fixtures merely migrate this line. ### 4. Status warnings are direct-plane advisory evidence From 8b745a25da8a9f147a6ab77ebe18963ee7538453 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 2 Oct 2026 03:14:39 +0800 Subject: [PATCH 06/12] docs(openspec): record the Round-E GO (9.1/10) and close CP0 --- .../target-openspec-cli-114-line/loop/checkpoints.md | 5 +++-- .../loop/implementation.md | 10 +++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md index ba2ec04d..51bb1694 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md @@ -11,8 +11,9 @@ Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下, ## CP0 — Planning - [x] Worktree + submodule pin `94ca9c1e` + evidence report + change artifacts. -- [ ] Codex change review: no blocking findings (≥8/10 target). -- [ ] CP0 committed. +- [x] Codex change review: five rounds 6.2 → 7.4 → 7.8 → 8.1 → **9.1/10 GO** (blockers B1-B7, B1-B4, C1-C2, + and the lost-write catch all folded; reports /tmp/v14-change-review-round-{a..e}.md). +- [x] CP0 committed (7e218935 + review-fold commits through bc633fb9). ## CP1 — Window and contracts (Slices 1-2) diff --git a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md index ce907a36..09492c2f 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md @@ -105,4 +105,12 @@ global correction log). Re-applied with write-first verification (protocol tree line, the stale `list --json` tree row is gone, the list paragraph reads as a complete sentence); the fixture owner-map alias fix from Round-C had landed and is confirmed. -Pending: Round-E narrow re-review of exactly these two report passages. +### Round-E review disposition (herdr v14-change-reviewer, gpt-5.6-terra xhigh, 2026-10-02) — 9.1/10, GO + +Worked 1m 27s (narrow). Both re-applied fixes verified in the actual `bc633fb9` diff (not the disposition's +self-description): the stale tree row is gone, the carry-forward line is unique, the list paragraph is a +complete sentence, and the Round-D record matches the real changes. No blockers, no new non-blocking notes. +**Verdict: GO — implementation may start under the CP/slice evidence rules.** + +Review arc: Round-A 6.2 REVISE (7 blockers) → B 7.4 (4) → C 7.8 (2) → D 8.1 (1, caught our lost write) → +E 9.1 GO. From 6d556ba8cd5963c4c143ca9fe396a504ff7b9e2b Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 2 Oct 2026 03:41:07 +0800 Subject: [PATCH 07/12] feat(core): rotate OpenSpecUI 14 window, contracts, and agent registry to CLI 1.14 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Batch A of the target-openspec-cli-114-line change: - Slice 1: openspec-compat MAJOR 14 window (>=1.14.0 <1.15.0) with diagnose/setup-example mirror rotation and boundary reds recorded. - Slice 2: typed 1.14 CLI contract chain — apply-task sourcePath/line, spec requirement/scenario names, status warnings (single + batch entry shapes), list entry archived, version envelope schema; kernel projectWorkflowStatus now carries warnings through its explicit ChangeStatusSchema rebuild on both paths (decode passthrough is stripped there), with the pre-fix red captured in the new opsx-kernel-status-warnings suite. - Slice 3: agent registry series 1.14 with ten new entries (amp, gsd shared .agents root, atomcode, codestudio, dsh, easycode, gigacode, grok, veai, warp, IBM Bob label); integrator widened SHARED_AGENTS_SKILLS_OWNER_CANDIDATES to the five upstream 1.14 .agents skills writers (antigravity stays commands-only). - Server tool-subscription install assertion rotated 1.13 -> 1.14. Focused gates: core 6 files 114 passed, registry trio 93 passed, server trio 27 passed, scripts root 75 passed, per-package typecheck 0. --- .../loop/checkpoints.md | 10 +- .../loop/implementation.md | 49 ++ .../core/src/agent-command-content.test.ts | 19 +- .../core/src/agent-delivery-registry.test.ts | 389 ++++++++++++++-- packages/core/src/agent-delivery-registry.ts | 221 +++++++-- packages/core/src/cli-contracts/executor.ts | 22 +- .../core/src/cli-contracts/workflow.test.ts | 423 +++++++++++++++++- packages/core/src/cli-contracts/workflow.ts | 87 +++- packages/core/src/openspec-compat.test.ts | 89 ++-- packages/core/src/openspec-compat.ts | 43 +- .../src/opsx-kernel-status-warnings.test.ts | 234 ++++++++++ packages/core/src/opsx-kernel.ts | 12 +- packages/core/src/opsx-types.test.ts | 148 +++++- packages/core/src/opsx-types.ts | 27 +- .../core/src/planning-cli-projection.test.ts | 148 +++++- packages/core/src/tool-init-state.test.ts | 98 ++-- packages/core/src/tool-init-state.ts | 21 +- .../agent-delivery-projection-service.test.ts | 87 +++- .../src/agent-integrations-router.test.ts | 24 +- .../src/tool-subscription-router.test.ts | 14 +- scripts/diagnose-cli-runner.mjs | 7 +- scripts/diagnose-cli-runner.test.mjs | 19 +- scripts/setup-example.ts | 7 +- 23 files changed, 1939 insertions(+), 259 deletions(-) create mode 100644 packages/core/src/opsx-kernel-status-warnings.test.ts diff --git a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md index 51bb1694..a698b250 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md @@ -17,16 +17,18 @@ Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下, ## CP1 — Window and contracts (Slices 1-2) -- [ ] Compat window rotated; boundary red recorded (stable 1.13.x → unsupported); mirrors + tests green. -- [ ] Apply-task source locations, status warnings (through the kernel `projectWorkflowStatus` rebuild on +- [x] Compat window rotated; boundary red recorded (stable 1.13.x → unsupported); mirrors + tests green. +- [x] Apply-task source locations, status warnings (through the kernel `projectWorkflowStatus` rebuild on single AND batch paths — decode passthrough is stripped there today), the new list `archived` member, show spec names, and the version envelope typed through contract + projection chains; kernel-rebuild red recorded verbatim-end-to-end; focused suites green; core typecheck. ## CP2 — Registry (Slice 3) -- [ ] +10 registry entries, kilo root, IBM Bob label, series `'1.14'`, generator `1.14.0`; staleness red - recorded; registry/state/server suites green. +- [x] +10 registry entries, kilo root, IBM Bob label, series `'1.14'`, generator `1.14.0`; staleness red + recorded; registry/state/server suites green. Integrator widened + `SHARED_AGENTS_SKILLS_OWNER_CANDIDATES` to the five upstream 1.14 writers + (codex/zed/agents/amp/gsd; antigravity stays commands-only) — see implementation.md Batch A. ## CP3 — Web + fixtures (Slices 4-5) diff --git a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md index 09492c2f..ad61a465 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md @@ -114,3 +114,52 @@ complete sentence, and the Round-D record matches the real changes. No blockers, Review arc: Round-A 6.2 REVISE (7 blockers) → B 7.4 (4) → C 7.8 (2) → D 8.1 (1, caught our lost write) → E 9.1 GO. + +## Batch A implementation record (2026-10-02) + +### Slice 1 — Compat window and mirrors (CP1) + +Production: `packages/core/src/openspec-compat.ts` (MAJOR 14, series `'1.14'`, `>=1.14.0 <1.15.0`, +NEXT_SERIES `'1.15.0'`, tag `'v1.14.*'`), `scripts/diagnose-cli-runner.mjs` + `scripts/setup-example.ts` +mirrors rotated with their tests. Red evidence was recorded by the slice agent before the constant flip +(6 failure sites: compat classification, boundary labels, mirror series assertions). Green: +`openspec-compat.test.ts` inside the core run below; scripts mirror inside `pnpm test:root` +(20 files / 75 passed | 12 skipped, includes `diagnose-cli-runner.test.mjs` parity cases). + +### Slice 2 — CLI contract chain and kernel warnings (CP1) + +Production: `cli-contracts/workflow.ts` (`CliApplyTaskSchema` + `sourcePath`/`line`; +`CliSpecRequirementSchema` + scenario `name`; `CliWorkflowStatusFieldsSchema` `warnings` shared by +single and batch entry shapes, batch stays entry-level only; change-list entry `archived`; new +`CliVersionSchema`), `cli-contracts/executor.ts`, `opsx-types.ts`, and the Round-A B2 owner fix in +`opsx-kernel.ts`: `projectWorkflowStatus` now carries +`...(data.warnings !== undefined ? { warnings: data.warnings } : {})` through its explicit +`ChangeStatusSchema.parse({...})` rebuild — decode `.passthrough()` is stripped at that rebuild, so the +copy is required on BOTH the single and the shared batch path. Red evidence: new +`opsx-kernel-status-warnings.test.ts` captured the warnings loss through the real kernel rebuild before +the fix (verbatim single + batch payloads). Green: core focused run — +`pnpm --filter @openspecui/core exec vitest run src/openspec-compat.test.ts src/cli-contracts/workflow.test.ts src/opsx-types.test.ts src/opsx-kernel-status-warnings.test.ts src/planning-cli-projection.test.ts src/agent-command-content.test.ts` +→ 6 files / 114 passed; `pnpm --filter @openspecui/core exec tsc --noEmit` → 0 errors. + +### Slice 3 — Agent registry rotation (CP2) + +Production: `agent-delivery-registry.ts` (series `'1.14'`, +10 entries: amp, atomcode, codestudio, dsh, +easycode, gigacode, grok, veai, warp, IBM Bob; gsd shares the `.agents` root with amp), `tool-init-state.ts` ++ tests. Integrator decision on the slice agent's escalation: `SHARED_AGENTS_SKILLS_OWNER_CANDIDATES` +widens from three to five values `['codex','zed','agents','amp','gsd']` — upstream 1.14 arbitration pool +counts five `.agents` skills writers and the reviewed spec delta already says so; antigravity stays +commands-only (adapter-backed). The slice agent had conservatively kept the three-valued tuple and +deferred; the registry + its test were updated together in the integrator pass. Green: +`vitest run src/agent-delivery-registry.test.ts src/tool-init-state.test.ts src/agent-command-content.test.ts` +→ 93 passed. + +### Batch A integrator pass (2026-10-02) + +- Fixed one stale assertion the slice rotation missed: `packages/server/src/tool-subscription-router.test.ts` + global install expectation `@fission-ai/openspec@1.13` → `@fission-ai/openspec@1.14` (production constant + had already rotated; the assertion failed once, then passed: file 5/5). +- Server focused suites: `agent-delivery-projection-service` + `agent-integrations-router` + `tool-subscription-router` + → 22 + 5 passed across two runs (the split is the fixed assertion file re-run). +- `pnpm --filter @openspecui/server run typecheck` (10 tsconfig lanes) → 0 errors. +- Note for future rounds: multi-package `pnpm --filter A --filter B exec tsc` produces spurious TS6059 + rootDir errors in this repo; per-package invocations are authoritative. diff --git a/packages/core/src/agent-command-content.test.ts b/packages/core/src/agent-command-content.test.ts index c1cc97e8..7a847d25 100644 --- a/packages/core/src/agent-command-content.test.ts +++ b/packages/core/src/agent-command-content.test.ts @@ -14,6 +14,9 @@ * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + * — registry Slice 3: the 1.12 runner now reports the four 1.14 adapters + * (atomcode/codestudio/easycode/gigacode) as version-scoped unavailable evidence. */ import { resolve } from 'node:path' @@ -71,8 +74,20 @@ describe('loadOpenSpecAgentCommandContents', () => { expect(catalog?.opencode?.explore).toContain(OPENCODE_PROVIDED_ARGUMENTS_LINE) expect(catalog?.codex).toBeUndefined() expect(catalog?.['minimax-code']).toBeUndefined() - // The 1.12 runner ships every registry adapter, so nothing is version-unavailable. - expect(result?.unavailableTools).toEqual({}) + // The 1.12 runner ships every adapter the 1.12 registry declared; the four + // adapter-backed tools OpenSpec 1.14 introduced (atomcode, codestudio, easycode, + // gigacode) are isolated as version-scoped unavailable evidence, exactly like + // codeassistant on the retired 1.11 runner below. + expect(result?.unavailableTools).toEqual({ + atomcode: + 'Command adapter first ships with OpenSpec CLI 1.14; this runner does not declare it.', + codestudio: + 'Command adapter first ships with OpenSpec CLI 1.14; this runner does not declare it.', + easycode: + 'Command adapter first ships with OpenSpec CLI 1.14; this runner does not declare it.', + gigacode: + 'Command adapter first ships with OpenSpec CLI 1.14; this runner does not declare it.', + }) }) it('retired 1.11: isolates the missing codeassistant adapter without erasing other evidence', async () => { diff --git a/packages/core/src/agent-delivery-registry.test.ts b/packages/core/src/agent-delivery-registry.test.ts index 4292461a..b5397a64 100644 --- a/packages/core/src/agent-delivery-registry.test.ts +++ b/packages/core/src/agent-delivery-registry.test.ts @@ -1,14 +1,15 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): - * 1. Lock the complete pinned OpenSpec 1.13 Agent delivery registry at the public Core boundary. + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Lock the complete pinned OpenSpec 1.14 Agent delivery registry at the public Core boundary. * 2. Prove capability, command format/invocation, alias, detection, setup, cleanup, migration, * global skill roots, legacy roots, and IDE restart facts stay co-located. - * 3. Prove the 1.13 snapshot inherits every 1.12/1.11 physical fact (Antigravity `.agents`-current - * with `.agent` legacy/migration; zed skills-only from '1.10') and keeps SourceCraft Code - * Assistant with `minCliSeries '1.12'` (the physical 1.12 introduction fact). - * 4. Prove version-selected inventories admit only stable 1.13.x — retired 1.10.x/1.11.x/1.12.x - * lines select nothing — and declare the three-valued shared-root owner candidate set exactly - * as the pinned upstream source does. + * 3. Prove the 1.14 snapshot inherits every 1.13/1.12/1.11 physical fact (Antigravity + * `.agents`-current with `.agent` legacy/migration; zed skills-only from '1.10'; + * SourceCraft Code Assistant with `minCliSeries '1.12'`) and lands the ten 1.14 entries + * (amp, atomcode, codestudio, dsh, easycode, gigacode, grok, gsd, veai, warp). + * 4. Prove version-selected inventories admit only stable 1.14.x — retired 1.10.x/1.11.x/ + * 1.12.x/1.13.x lines select nothing — and declare the five-valued shared-root owner + * candidate set (1.14 adds the amp and gsd skills writers) as the pinned upstream source does. * 5. Provide explicit mutation-resistance evidence for every load-bearing registry dimension. * 6. Pin Kilo Code's 1.13.2 command-path rotation: `.kilo/command/opsx-{workflow}.md` is the * current template, `.kilocode/workflows/opsx-{workflow}.md` is legacy evidence, and cleanup @@ -22,6 +23,8 @@ * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-26): Slice 3 of the 1.13.2 patch rotation — the kilocode snapshot * row and dedicated legacy/cleanup assertions rotate with the pinned adapter facts. + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + * — Slice 3 rotates the admitted series to '1.14' and adds the ten new snapshot rows. */ import { describe, expect, it } from 'vitest' @@ -61,6 +64,7 @@ const OFFICIAL_REGISTRY = [ 'flat', '@', ], + ['amp', 'Amp', '.agents', 'none', null, null, null, null], [ 'antigravity', 'Antigravity', @@ -71,6 +75,16 @@ const OFFICIAL_REGISTRY = [ 'flat', '/', ], + [ + 'atomcode', + 'AtomCode', + '.atomcode', + 'adapter-backed', + '.atomcode/commands/opsx-{workflow}.md', + 'markdown', + 'flat', + '/', + ], [ 'auggie', 'Auggie (Augment CLI)', @@ -83,7 +97,7 @@ const OFFICIAL_REGISTRY = [ ], [ 'bob', - 'Bob Shell', + 'IBM Bob', '.bob', 'adapter-backed', '.bob/commands/opsx-{workflow}.md', @@ -123,6 +137,7 @@ const OFFICIAL_REGISTRY = [ ], ['codeartsagent', 'CodeArts', '.codeartsdoer', 'none', null, null, null, null], ['codex', 'Codex', '.agents', 'skills-invocable', null, null, null, null], + ['dsh', 'DeepSeek Harness', '.dsh', 'none', null, null, null, null], [ 'devin', 'Devin Desktop (formerly Windsurf)', @@ -144,6 +159,16 @@ const OFFICIAL_REGISTRY = [ 'namespaced', '/', ], + [ + 'codestudio', + 'Code Studio', + '.codestudio', + 'adapter-backed', + '.codestudio/prompts/opsx-{workflow}.prompt.md', + 'markdown', + 'flat', + '/', + ], [ 'continue', 'Continue', @@ -184,6 +209,16 @@ const OFFICIAL_REGISTRY = [ 'flat', '/', ], + [ + 'easycode', + 'EasyCode', + '.easycode', + 'adapter-backed', + '.easycode/commands/opsx/{workflow}.toml', + 'toml', + 'namespaced', + '/', + ], [ 'factory', 'Factory Droid', @@ -204,6 +239,16 @@ const OFFICIAL_REGISTRY = [ 'namespaced', '/', ], + [ + 'gigacode', + 'GigaCode', + '.gigacode', + 'adapter-backed', + '.gigacode/commands/opsx-{workflow}.md', + 'markdown', + 'flat', + '/', + ], [ 'github-copilot', 'GitHub Copilot', @@ -214,6 +259,8 @@ const OFFICIAL_REGISTRY = [ 'flat', '/', ], + ['grok', 'Grok Build', '.grok', 'none', null, null, null, null], + ['gsd', 'GSD', '.agents', 'skills-invocable', null, null, null, null], ['hermes', 'Hermes Agent', '.hermes', 'none', null, null, null, null], [ 'iflow', @@ -340,6 +387,8 @@ const OFFICIAL_REGISTRY = [ 'flat', '/', ], + ['veai', 'Veai', '.veai', 'none', null, null, null, null], + ['warp', 'Warp', '.warp', 'skills-invocable', null, null, null, null], ['zed', 'Zed Agent', '.agents', 'none', null, null, null, null], [ 'zcode', @@ -481,7 +530,7 @@ function assertPinnedRegistry(registry: readonly ToolConfig[]): void { format: 'markdown', }) ) - // 1.13 base (inherited from 1.12/1.11): Antigravity's current roots live at the shared + // 1.14 base (inherited from 1.12/1.11): Antigravity's current roots live at the shared // `.agents` root while `.agent` stays readable for detection and migrates only after // replacement generation. expect(registryEntry(registry, 'antigravity')).toEqual( @@ -519,7 +568,7 @@ function assertPinnedRegistry(registry: readonly ToolConfig[]): void { ) expect(registryEntry(registry, 'zed')?.requiresIdeRestart).toBeUndefined() // SourceCraft Code Assistant enters with 1.12 and carries forward on the admitted - // 1.13 line: description-only YAML commands under + // 1.14 line: description-only YAML commands under // the default `.codeassistant` detection root, natural-language skill references // (upstream `NATURAL_LANGUAGE_SKILL_TOOLS`, like rovodev — but with command files, // unlike rovodev), no IDE restart fact, no cleanup, no migrations. @@ -542,21 +591,182 @@ function assertPinnedRegistry(registry: readonly ToolConfig[]): void { expect(registryEntry(registry, 'codeassistant')?.cleanup).toBeUndefined() expect(registryEntry(registry, 'codeassistant')?.migrations).toBeUndefined() expect(registryEntry(registry, 'codeassistant')?.legacySkillsDirs).toBeUndefined() + // 1.14 rebrand: Bob Shell is IBM Bob with unchanged physical roots and adapter facts. + expect(registryEntry(registry, 'bob')).toEqual( + expect.objectContaining({ + name: 'IBM Bob', + successLabel: 'IBM Bob', + skillsDir: '.bob', + capability: 'adapter-backed', + command: expect.objectContaining({ + pathTemplate: '.bob/commands/opsx-{workflow}.md', + }), + }) + ) + // The ten 1.14 entries carry exactly the pinned upstream physical facts. + expect(registryEntry(registry, 'amp')).toEqual( + expect.objectContaining({ + name: 'Amp', + available: true, + successLabel: 'Amp', + skillsDir: '.agents', + detectionPaths: ['.amp', '.agents/skills'], + capability: 'none', + command: null, + minCliSeries: '1.14', + }) + ) + expect(registryEntry(registry, 'amp')?.requiresIdeRestart).toBeUndefined() + expect(registryEntry(registry, 'atomcode')).toEqual( + expect.objectContaining({ + skillsDir: '.atomcode', + capability: 'adapter-backed', + command: expect.objectContaining({ + pathTemplate: '.atomcode/commands/opsx-{workflow}.md', + format: 'markdown', + content: { + kind: 'markdown', + frontmatter: { kind: 'yaml', fields: ['name', 'description', 'args'] }, + bodyLayout: 'direct', + }, + }), + minCliSeries: '1.14', + }) + ) + expect(registryEntry(registry, 'codestudio')).toEqual( + expect.objectContaining({ + name: 'Code Studio', + successLabel: 'Code Studio', + skillsDir: '.codestudio', + requiresIdeRestart: true, + requiresIdeRestartSince: '1.14', + capability: 'adapter-backed', + command: expect.objectContaining({ + pathTemplate: '.codestudio/prompts/opsx-{workflow}.prompt.md', + }), + minCliSeries: '1.14', + }) + ) + expect(registryEntry(registry, 'dsh')).toEqual( + expect.objectContaining({ + name: 'DeepSeek Harness', + skillsDir: '.dsh', + capability: 'none', + command: null, + minCliSeries: '1.14', + }) + ) + expect(registryEntry(registry, 'easycode')).toEqual( + expect.objectContaining({ + skillsDir: '.easycode', + capability: 'adapter-backed', + command: expect.objectContaining({ + pathTemplate: '.easycode/commands/opsx/{workflow}.toml', + format: 'toml', + invocation: { style: 'namespaced', prefix: '/' }, + }), + minCliSeries: '1.14', + }) + ) + expect(registryEntry(registry, 'gigacode')).toEqual( + expect.objectContaining({ + skillsDir: '.gigacode', + capability: 'adapter-backed', + command: expect.objectContaining({ + pathTemplate: '.gigacode/commands/opsx-{workflow}.md', + }), + minCliSeries: '1.14', + }) + ) + expect(registryEntry(registry, 'grok')).toEqual( + expect.objectContaining({ + name: 'Grok Build', + skillsDir: '.grok', + capability: 'none', + command: null, + minCliSeries: '1.14', + }) + ) + // GSD shares the `.agents` root and references skills in natural language (like + // rovodev): skills-invocable, no command files, detection keyed to `.gsd`. + expect(registryEntry(registry, 'gsd')).toEqual( + expect.objectContaining({ + name: 'GSD', + skillsDir: '.agents', + detectionPaths: ['.gsd'], + capability: 'skills-invocable', + command: null, + minCliSeries: '1.14', + }) + ) + expect(registryEntry(registry, 'veai')).toEqual( + expect.objectContaining({ + name: 'Veai', + skillsDir: '.veai', + capability: 'none', + command: null, + minCliSeries: '1.14', + }) + ) + // Upstream classifies warp beside codex as skills-invocable; detection keys off + // `.warp` and the WARP.md marker file. + expect(registryEntry(registry, 'warp')).toEqual( + expect.objectContaining({ + name: 'Warp', + skillsDir: '.warp', + detectionPaths: ['.warp', 'WARP.md'], + capability: 'skills-invocable', + command: null, + minCliSeries: '1.14', + }) + ) + expect(registryEntry(registry, 'warp')?.requiresIdeRestart).toBeUndefined() + // None of the ten 1.14 tools owns cleanup, migration, or legacy-root evidence. + for (const newTool of [ + 'amp', + 'atomcode', + 'codestudio', + 'dsh', + 'easycode', + 'gigacode', + 'grok', + 'gsd', + 'veai', + 'warp', + ]) { + expect(registryEntry(registry, newTool)?.cleanup).toBeUndefined() + expect(registryEntry(registry, newTool)?.migrations).toBeUndefined() + expect(registryEntry(registry, newTool)?.legacySkillsDirs).toBeUndefined() + } } /** Assert the admitted per-series inventory against the selection boundary itself. */ function assertAdmittedSeriesInventories(): void { - const series113 = selectAgentDeliveryRegistry('1.13.0') + const series114 = selectAgentDeliveryRegistry('1.14.0') - // The admitted line ships the complete official tool set: every 1.12 tool keeps its - // physical facts and codeassistant (minCliSeries '1.12') carries forward; zed's - // provenance stays '1.10' without gating the 1.13 inventory. - expect(series113.map((tool) => tool.value)).toEqual(AI_TOOLS.map((tool) => tool.value)) - expect(series113.some((tool) => tool.value === 'codeassistant')).toBe(true) + // The admitted line ships the complete official tool set: every 1.13/1.12 tool keeps its + // physical facts, codeassistant (minCliSeries '1.12') and zed ('1.10') carry forward, and + // the ten 1.14 entries join their minCliSeries-gated inventory. + expect(series114.map((tool) => tool.value)).toEqual(AI_TOOLS.map((tool) => tool.value)) + expect(series114.some((tool) => tool.value === 'codeassistant')).toBe(true) + for (const newTool of [ + 'amp', + 'atomcode', + 'codestudio', + 'dsh', + 'easycode', + 'gigacode', + 'grok', + 'gsd', + 'veai', + 'warp', + ]) { + expect(series114.some((tool) => tool.value === newTool)).toBe(true) + } - // 1.13 inherits 1.11's Antigravity reality (carried through 1.12): current roots at the - // shared `.agents` root with `.agent` as legacy + after-generation migration evidence. - expect(registryEntry(series113, 'antigravity')).toEqual( + // 1.14 inherits 1.11's Antigravity reality (carried through 1.12/1.13): current roots at + // the shared `.agents` root with `.agent` as legacy + after-generation migration evidence. + expect(registryEntry(series114, 'antigravity')).toEqual( expect.objectContaining({ skillsDir: '.agents', legacySkillsDirs: ['.agent'], @@ -569,21 +779,40 @@ function assertAdmittedSeriesInventories(): void { ) // IDE restart facts are declared on the admitted line exactly where upstream does, - // and never on the skills-only zed / shared / codex targets or codeassistant. - expect(series113.filter((tool) => tool.requiresIdeRestart).map((tool) => tool.value)).toEqual([ + // and never on the skills-only zed / shared / codex targets, codeassistant, or the + // nine 1.14 entries that declare no restart (only codestudio does). + expect(series114.filter((tool) => tool.requiresIdeRestart).map((tool) => tool.value)).toEqual([ ...EXPECTED_RESTART_TOOLS, ]) - expect(registryEntry(series113, 'zed')?.requiresIdeRestart).toBeUndefined() - expect(registryEntry(series113, 'codeassistant')?.requiresIdeRestart).toBeUndefined() + expect(registryEntry(series114, 'zed')?.requiresIdeRestart).toBeUndefined() + expect(registryEntry(series114, 'codeassistant')?.requiresIdeRestart).toBeUndefined() + for (const noRestartTool of [ + 'amp', + 'dsh', + 'easycode', + 'gigacode', + 'grok', + 'gsd', + 'veai', + 'warp', + 'atomcode', + ]) { + expect(registryEntry(series114, noRestartTool)?.requiresIdeRestart).toBeUndefined() + } for (const candidate of SHARED_AGENTS_SKILLS_OWNER_CANDIDATES) { - expect(registryEntry(series113, candidate)?.skillsDir).toBe('.agents') + expect(registryEntry(series114, candidate)?.skillsDir).toBe('.agents') } + // amp and gsd deliver into the same shared root on the admitted line while arbitration + // stays marker/inference-driven upstream. + expect(registryEntry(series114, 'amp')?.skillsDir).toBe('.agents') + expect(registryEntry(series114, 'gsd')?.skillsDir).toBe('.agents') // Selected snapshots are plain per-series inventories: the override mechanism stays internal. - expect(series113.every((tool) => tool.perSeriesOverrides === undefined)).toBe(true) + expect(series114.every((tool) => tool.perSeriesOverrides === undefined)).toBe(true) - // Retired below-range lines select no inventory at all: the 1.10/1.11/1.12 v11/v12 + // Retired below-range lines select no inventory at all: the 1.10/1.11/1.12/1.13 v11–v13 // windows are not admitted by this release line, so they must not project a stale snapshot. + expect(selectAgentDeliveryRegistry('1.13.2')).toEqual([]) expect(selectAgentDeliveryRegistry('1.12.5')).toEqual([]) expect(selectAgentDeliveryRegistry('1.11.3')).toEqual([]) expect(selectAgentDeliveryRegistry('1.10.7')).toEqual([]) @@ -607,6 +836,7 @@ const EXPECTED_RESTART_TOOLS = [ 'antigravity', 'cline', 'devin', + 'codestudio', 'continue', 'costrict', 'cursor', @@ -620,7 +850,7 @@ const EXPECTED_RESTART_TOOLS = [ 'trae', ] as const -describe('OpenSpec 1.13 Agent delivery registry (pinned base)', () => { +describe('OpenSpec 1.14 Agent delivery registry (pinned base)', () => { it('preserves the complete pinned metadata, command format, and invocation contract', () => { assertPinnedRegistry(AI_TOOLS) }) @@ -644,19 +874,23 @@ describe('OpenSpec 1.13 Agent delivery registry (pinned base)', () => { ).toEqual({ 'amazon-q': 'yaml:description|direct', antigravity: 'yaml:description|direct', + atomcode: 'yaml:name,description,args|direct', auggie: 'yaml:description,argument-hint|direct', bob: 'yaml:description,argument-hint|direct', claude: 'yaml:name,description,allowed-tools,category,tags|direct', cline: 'none|headings', 'command-code': 'none|direct', + codestudio: 'yaml:description|direct', devin: 'yaml:name,description,category,tags|direct', codebuddy: 'yaml:name,description,argument-hint|direct', continue: 'yaml:name,description,invokable|direct', costrict: 'yaml:description,argument-hint|direct', crush: 'yaml:name,description,category,tags|direct', cursor: 'yaml:name,id,category,description|direct', + easycode: 'toml:description,prompt', factory: 'yaml:description,argument-hint|direct', gemini: 'toml:description,prompt', + gigacode: 'yaml:description|direct', 'github-copilot': 'yaml:description|direct', iflow: 'yaml:name,id,category,description|direct', junie: 'yaml:description|direct', @@ -676,32 +910,35 @@ describe('OpenSpec 1.13 Agent delivery registry (pinned base)', () => { }) }) -describe('per-series inventory selection (1.13 single-series window)', () => { - it('selects the 1.13 official inventory with codeassistant and the 1.11-inherited Antigravity roots', () => { +describe('per-series inventory selection (1.14 single-series window)', () => { + it('selects the 1.14 official inventory with the ten 1.14 entries and the 1.11-inherited Antigravity roots', () => { assertAdmittedSeriesInventories() }) it('selects no inventory for non-admitted or unparseable versions', () => { // A page-level version bypass must not manufacture an admitted inventory: prereleases, - // the next series, below-range lines (including the retired 1.10/1.11/1.12 v11/v12 + // the next series, below-range lines (including the retired 1.10/1.11/1.12/1.13 v11–v13 // windows and 1.8/1.9), and unparseable output all select zero tools. expect(selectAgentDeliveryRegistry('1.9.5')).toEqual([]) expect(selectAgentDeliveryRegistry('1.9.0')).toEqual([]) expect(selectAgentDeliveryRegistry('1.8.0')).toEqual([]) + expect(selectAgentDeliveryRegistry('1.13.0')).toEqual([]) + expect(selectAgentDeliveryRegistry('1.13.2')).toEqual([]) expect(selectAgentDeliveryRegistry('1.12.0')).toEqual([]) expect(selectAgentDeliveryRegistry('1.11.0')).toEqual([]) expect(selectAgentDeliveryRegistry('1.10.7')).toEqual([]) expect(selectAgentDeliveryRegistry('1.12.0-rc.1')).toEqual([]) expect(selectAgentDeliveryRegistry('1.13.0-rc.1')).toEqual([]) - expect(selectAgentDeliveryRegistry('1.14.0')).toEqual([]) + expect(selectAgentDeliveryRegistry('1.14.0-rc.1')).toEqual([]) + expect(selectAgentDeliveryRegistry('1.15.0')).toEqual([]) expect(selectAgentDeliveryRegistry('2.0.0')).toEqual([]) expect(selectAgentDeliveryRegistry('garbage')).toEqual([]) expect(selectAgentDeliveryRegistry(null)).toEqual([]) }) it.each([ - ['1.13.0', '1.13'], - ['1.13.4', '1.13'], + ['1.14.0', '1.14'], + ['1.14.2', '1.14'], ])('parses stable %s as the %s Agent inventory line', (cliVersion, expectedSeries) => { expect(parseOpenSpecCliSeries(cliVersion)).toBe(expectedSeries) }) @@ -713,10 +950,13 @@ describe('per-series inventory selection (1.13 single-series window)', () => { '1.10.7', '1.11.3', '1.12.5', - '1.14.0', + '1.13.0', + '1.13.2', + '1.15.0', '2.0.0', '1.12.0-rc.1', '1.13.0-rc.1', + '1.14.0-rc.1', '1.11.0-beta.1', 'garbage', '', @@ -725,19 +965,27 @@ describe('per-series inventory selection (1.13 single-series window)', () => { expect(parseOpenSpecCliSeries(cliVersion)).toBeNull() }) - it('declares the three-valued shared .agents skills-root owner candidate set', () => { + it('declares the shared .agents skills-root owner candidate set with its 1.14 writers', () => { expect(SHARED_AGENTS_SKILLS_ROOT).toBe('.agents') expect(SHARED_SKILLS_TARGET_MARKER).toBe('.openspec-target') // Upstream arbitration order: `.openspec-target` marker, then inferred owner from // generated invocation syntax, then `agents` when current skills exist, then the // `codex` fallback. The registry declares candidates only; arbitration is owned by - // the official CLI and projected by the Server Agent delivery service. - expect(SHARED_AGENTS_SKILLS_OWNER_CANDIDATES).toEqual(['codex', 'zed', 'agents']) - // Antigravity joined the shared root in 1.11 (carried forward on 1.12/1.13) but is + // the official CLI and projected by the Server Agent delivery service. OpenSpec 1.14 + // adds two skills writers to the shared root — amp (`.amp` detection) and gsd (`.gsd` + // detection, skills-invocable) — widening the candidate set to five. + expect(SHARED_AGENTS_SKILLS_OWNER_CANDIDATES).toEqual([ + 'codex', + 'zed', + 'agents', + 'amp', + 'gsd', + ]) + // Antigravity joined the shared root in 1.11 (carried forward on 1.12/1.13/1.14) but is // adapter-backed and is excluded from skills-writer candidacy; its own // `.agents/workflows` commands root is unaffected by that exclusion. expect(SHARED_AGENTS_SKILLS_OWNER_CANDIDATES).not.toContain('antigravity') - expect(registryEntry(selectAgentDeliveryRegistry('1.13.0'), 'antigravity')?.capability).toBe( + expect(registryEntry(selectAgentDeliveryRegistry('1.14.0'), 'antigravity')?.capability).toBe( 'adapter-backed' ) }) @@ -764,7 +1012,7 @@ describe('per-series inventory selection (1.13 single-series window)', () => { } }) - it('rejects stripping the Antigravity migration evidence from the 1.13 snapshot', () => { + it('rejects stripping the Antigravity migration evidence from the 1.14 snapshot', () => { const antigravity = requireRegistryEntry(AI_TOOLS, 'antigravity') const migrations = antigravity.migrations antigravity.migrations = [] @@ -802,6 +1050,65 @@ describe('registry mutation resistance', () => { 1 ), ], + [ + 'warp entry', + (registry: ToolConfig[]) => + registry.splice( + registry.findIndex((tool) => tool.value === 'warp'), + 1 + ), + ], + [ + 'amp entry', + (registry: ToolConfig[]) => + registry.splice( + registry.findIndex((tool) => tool.value === 'amp'), + 1 + ), + ], + [ + 'gsd entry', + (registry: ToolConfig[]) => + registry.splice( + registry.findIndex((tool) => tool.value === 'gsd'), + 1 + ), + ], + [ + 'codestudio command mapping', + (registry: ToolConfig[]) => { + const codestudio = registryEntry(registry, 'codestudio') + if (codestudio?.command) + codestudio.command.pathTemplate = '.codestudio/commands/opsx-{workflow}.md' + }, + ], + [ + 'easycode command format', + (registry: ToolConfig[]) => { + const easycode = registryEntry(registry, 'easycode') + if (easycode?.command) easycode.command.format = 'markdown' + }, + ], + [ + 'atomcode series gate', + (registry: ToolConfig[]) => + Object.assign(requireRegistryEntry(registry, 'atomcode'), { minCliSeries: '1.13' }), + ], + [ + 'warp detection paths', + (registry: ToolConfig[]) => + Object.assign(requireRegistryEntry(registry, 'warp'), { detectionPaths: ['.warp'] }), + ], + [ + 'gsd capability', + (registry: ToolConfig[]) => + Object.assign(requireRegistryEntry(registry, 'gsd'), { capability: 'none' }), + ], + [ + 'codestudio IDE restart requirement', + (registry: ToolConfig[]) => + Object.assign(requireRegistryEntry(registry, 'codestudio'), { requiresIdeRestart: false }), + ], [ 'capability', (registry: ToolConfig[]) => diff --git a/packages/core/src/agent-delivery-registry.ts b/packages/core/src/agent-delivery-registry.ts index 5e118804..1f2020b9 100644 --- a/packages/core/src/agent-delivery-registry.ts +++ b/packages/core/src/agent-delivery-registry.ts @@ -1,14 +1,14 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): - * 1. Preserve the complete pinned OpenSpec 1.13 Agent delivery registry in one typed physical owner. + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Preserve the complete pinned OpenSpec 1.14 Agent delivery registry in one typed physical owner. * 2. Co-locate capability, command artifact, invocation, alias, setup, cleanup, and migration metadata. * 3. Model current/legacy project roots, user-global skill roots, detection paths, and IDE restart facts. * 4. Keep the per-series override mechanism available for future widened windows while the - * admitted window is the single '1.13' line (every 1.12/1.11 physical fact carries forward). + * admitted window is the single '1.14' line (every 1.13/1.12 physical fact carries forward). * 5. Declare the shared `.agents` skills-root owner candidate set and its arbitration order as * metadata only; physical arbitration stays owned by the official CLI and the Server projection. - * 6. Select the official inventory for the admitted CLI line ('1.13') only; retired minors - * ('1.10'/'1.11'/'1.12') stay typed as provenance history and select no inventory. + * 6. Select the official inventory for the admitted CLI line ('1.14') only; retired minors + * ('1.10'/'1.11'/'1.12'/'1.13') stay typed as provenance history and select no inventory. * 7. Carry Kilo Code's 1.13.2 command-path rotation as a physical fact: `.kilo/command/` is the * live delivery target, the former `.kilocode/workflows/opsx-{workflow}.md` path is legacy * evidence, and cleanup covers exactly the old folder's two generations — never `.kilo/command/`. @@ -20,6 +20,10 @@ * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-26): Slice 3 of the 1.13.2 patch rotation — mirror upstream * kilocode adapter/legacy-cleanup facts (command path `.kilo/command/opsx-.md`). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + * — Slice 3 rotates the series to '1.14' and lands the ten 1.14 entries (amp, atomcode, + * codestudio, dsh, easycode, gigacode, grok, gsd, veai, warp); the Kilo directory rotation + * already landed with the 1132 change and is not repeated here. */ import { parseOpenSpecCliVersion } from './openspec-compat.js' @@ -110,22 +114,22 @@ export interface AIToolOption { } /** Official OpenSpec CLI lines with distinct Agent inventories admitted by this release line. */ -export type AgentCliSeries = '1.13' +export type AgentCliSeries = '1.14' /** * Historical provenance lines for registry facts that predate or retire out of the * admitted window (`minCliSeries`, `requiresIdeRestartSince`, and on-disk * `generatedBy` stamps typed at their consumers). Selection math treats them as - * ordered history: a provenance line below the admitted line never gates a v13 - * inventory, and retired admitted lines ('1.10'/'1.11'/'1.12') select no inventory - * at all. + * ordered history: a provenance line below the admitted line never gates a v14 + * inventory, and retired admitted lines ('1.10'/'1.11'/'1.12'/'1.13') select no + * inventory at all. */ -export type AgentProvenanceCliSeries = '1.9' | '1.10' | '1.11' | '1.12' | AgentCliSeries +export type AgentProvenanceCliSeries = '1.9' | '1.10' | '1.11' | '1.12' | '1.13' | AgentCliSeries /** * Per-series divergence applied when one admitted CLI line pins a different physical * reality than the newest line. The base registry always mirrors the newest admitted - * line (1.13); an override replaces the listed fields for an older admitted line + * line (1.14); an override replaces the listed fields for an older admitted line * when a future release widens the window again. A field explicitly set to * `undefined` clears the newest-line value. */ @@ -150,6 +154,8 @@ function agentCliSeriesOrder(series: AgentProvenanceCliSeries): number { return 12 case '1.13': return 13 + case '1.14': + return 14 } } @@ -167,26 +173,34 @@ export const SHARED_SKILLS_TARGET_MARKER = '.openspec-target' * service — never by this registry — and follows: the `.openspec-target` marker, then * the owner inferred from generated invocation syntax, then `agents` when current skills * already exist, then the `codex` fallback. Antigravity joined the shared root in - * 1.11 (carried forward on 1.12/1.13) but is adapter-backed and is excluded from + * 1.11 (carried forward on 1.12/1.13/1.14) but is adapter-backed and is excluded from * skills-writer candidacy; its own `.agents/workflows` commands root is unaffected - * by that exclusion. + * by that exclusion. OpenSpec 1.14 adds two more shared-root readers: `amp` + * (detection `.amp` / `.agents/skills`) and `gsd` (detection `.gsd`) both deliver + * skills into `.agents` — the shared root now hosts codex, antigravity, amp, gsd, + * zed, the vendor-neutral target, and since OpenSpec 1.14 the amp and gsd skills writers, + * while upstream arbitration stays marker- and inference-driven across that whole group + * (antigravity stays commands-only at the shared root and never joins the pool). */ -export const SHARED_AGENTS_SKILLS_OWNER_CANDIDATES: readonly ['codex', 'zed', 'agents'] = [ +export const SHARED_AGENTS_SKILLS_OWNER_CANDIDATES: readonly [ 'codex', 'zed', 'agents', -] + 'amp', + 'gsd', +] = ['codex', 'zed', 'agents', 'amp', 'gsd'] /** * Select the official Agent delivery inventory for one CLI version string. * - * Only a stable, admitted version — stable 1.13.x on this single-series line — - * selects an inventory. Unsupported forms (prereleases, >=1.14, below-range lines - * including the retired 1.10/1.11/1.12 v11/v12 windows and 1.8/1.9) and unparseable - * output select none: a page-level version bypass must not manufacture an admitted - * inventory for a CLI the release line refuses to admit. The returned snapshot - * carries the 1.13 physical reality (which inherits every 1.12/1.11 root fact; - * 1.13 adds no new Agent tool). + * Only a stable, admitted version — stable 1.14.x on this single-series line — + * selects an inventory. Unsupported forms (prereleases, >=1.15, below-range lines + * including the retired 1.10/1.11/1.12/1.13 v11–v13 windows and 1.8/1.9) and + * unparseable output select none: a page-level version bypass must not manufacture + * an admitted inventory for a CLI the release line refuses to admit. The returned + * snapshot carries the 1.14 physical reality (which inherits every 1.13/1.12 root + * fact; 1.14 adds ten Agent tools — amp, atomcode, codestudio, dsh, easycode, + * gigacode, grok, gsd, veai, warp). */ export function selectAgentDeliveryRegistry(cliVersion: string | null): ToolConfig[] { const series = parseOpenSpecCliSeries(cliVersion) @@ -219,16 +233,16 @@ export function selectAgentDeliveryRegistry(cliVersion: string | null): ToolConf * * Delegates to the single compat version parser so the same stdout that classifies a session * also selects its inventory — two parsers must never disagree at this boundary. Returns - * '1.13' only for stable 1.13.x, and null for every non-admitted form: prereleases, - * >=1.14, retired below-range lines (including the 1.12 v12 window, the 1.10/1.11 v11 - * window, and 1.9.x), or unparseable output. Retired minors stay typed as - * `AgentProvenanceCliSeries` history for registry facts and on-disk stamps; they never - * select an inventory here. + * '1.14' only for stable 1.14.x, and null for every non-admitted form: prereleases, + * >=1.15, retired below-range lines (including the 1.13 v13 window, the 1.12 v12 + * window, the 1.10/1.11 v11 window, and 1.9.x), or unparseable output. Retired minors + * stay typed as `AgentProvenanceCliSeries` history for registry facts and on-disk + * stamps; they never select an inventory here. */ export function parseOpenSpecCliSeries(cliVersion: string | null): AgentCliSeries | null { const version = parseOpenSpecCliVersion(cliVersion ?? undefined) if (!version || version.prerelease !== null) return null - if (version.major === 1 && version.minor === 13) return '1.13' + if (version.major === 1 && version.minor === 14) return '1.14' return null } @@ -300,9 +314,10 @@ const projectCleanup = (...patterns: string[]): AgentProjectCleanup => ({ }) /** - * Complete OpenSpec 1.13 Agent delivery registry in official order (the newest admitted - * line; 1.13 adds no new Agent tool, so the 1.12 physical inventory carries forward - * unchanged, itself inheriting every 1.11 root fact). + * Complete OpenSpec 1.14 Agent delivery registry in official order (the newest admitted + * line). 1.14 adds ten Agent tools — amp, atomcode, codestudio, dsh, easycode, gigacode, + * grok, gsd, veai, warp — and rebrands Bob Shell to IBM Bob; every 1.13/1.12/1.11 + * physical fact (including Kilo's `.kilo/command/` rotation) carries forward unchanged. */ export const AGENT_DELIVERY_REGISTRY: ToolConfig[] = [ { @@ -319,9 +334,24 @@ export const AGENT_DELIVERY_REGISTRY: ToolConfig[] = [ }), cleanup: projectCleanup('.amazonq/prompts/openspec-*.md'), }, + { + // Amp enters with OpenSpec 1.14 and reads the shared `.agents` skills root (see + // `SHARED_AGENTS_SKILLS_OWNER_CANDIDATES`): detection keys off `.amp` and + // `.agents/skills` — like zed — rather than the bare shared root. It ships no + // command adapter, carries no restart fact, and no legacy roots. + name: 'Amp', + value: 'amp', + minCliSeries: '1.14', + available: true, + successLabel: 'Amp', + skillsDir: '.agents', + detectionPaths: ['.amp', '.agents/skills'], + capability: 'none', + command: null, + }, { // OpenSpec 1.11 moved Antigravity workspace skills and workflows from `.agent` - // to the shared `.agents` root, and 1.12/1.13 keep that physical reality. Detection + // to the shared `.agents` root, and 1.12/1.13/1.14 keep that physical reality. Detection // keys off `.agent` and `.agents/workflows` rather than the bare shared root; // the legacy root migrates only after the replacement is generated so divergent // files are kept and reported. Antigravity itself never writes the shared skills @@ -347,6 +377,23 @@ export const AGENT_DELIVERY_REGISTRY: ToolConfig[] = [ { from: '.agent', to: '.agents', needsConsent: false, timing: 'after-generation' }, ], }, + { + // AtomCode enters with OpenSpec 1.14. Its adapter writes `.atomcode/commands/opsx-.md` + // with literal `name`/`description`/`args` frontmatter (args is `none` or `optional`; + // AtomCode's parser reads these unquoted), and injects a `**Provided arguments**: + // $ARGUMENTS` block only for workflows declaring an `**Input**` contract. + name: 'AtomCode', + value: 'atomcode', + minCliSeries: '1.14', + available: true, + successLabel: 'AtomCode', + skillsDir: '.atomcode', + capability: 'adapter-backed', + command: command( + '.atomcode/commands/opsx-{workflow}.md', + yamlMarkdown('name', 'description', 'args') + ), + }, { name: 'Auggie (Augment CLI)', value: 'auggie', @@ -361,10 +408,11 @@ export const AGENT_DELIVERY_REGISTRY: ToolConfig[] = [ cleanup: projectCleanup('.augment/commands/openspec-*.md'), }, { - name: 'Bob Shell', + // OpenSpec 1.14 rebranded Bob Shell to IBM Bob; id and physical roots stay `bob`/`.bob`. + name: 'IBM Bob', value: 'bob', available: true, - successLabel: 'Bob Shell', + successLabel: 'IBM Bob', skillsDir: '.bob', capability: 'adapter-backed', command: command( @@ -440,6 +488,18 @@ export const AGENT_DELIVERY_REGISTRY: ToolConfig[] = [ { from: '.codex', to: '.agents', needsConsent: false, timing: 'after-generation' }, ], }, + { + // DeepSeek Harness enters with OpenSpec 1.14: skills-only under the default `.dsh` + // detection root, no command adapter, no restart fact. + name: 'DeepSeek Harness', + value: 'dsh', + minCliSeries: '1.14', + available: true, + successLabel: 'DeepSeek Harness', + skillsDir: '.dsh', + capability: 'none', + command: null, + }, { name: 'Devin Desktop (formerly Windsurf)', value: 'devin', @@ -480,6 +540,21 @@ export const AGENT_DELIVERY_REGISTRY: ToolConfig[] = [ ), cleanup: projectCleanup('.codebuddy/commands/openspec'), }, + { + // Code Studio (Syncfusion) enters with OpenSpec 1.14 following its .prompt.md + // specification: description-only YAML commands under `.codestudio/prompts/`, + // loaded through an IDE/editor process that must restart. + name: 'Code Studio', + value: 'codestudio', + minCliSeries: '1.14', + available: true, + successLabel: 'Code Studio', + skillsDir: '.codestudio', + requiresIdeRestart: true, + requiresIdeRestartSince: '1.14', + capability: 'adapter-backed', + command: command('.codestudio/prompts/opsx-{workflow}.prompt.md', yamlMarkdown('description')), + }, { name: 'Continue', value: 'continue', @@ -538,6 +613,19 @@ export const AGENT_DELIVERY_REGISTRY: ToolConfig[] = [ ), cleanup: projectCleanup('.cursor/commands/openspec-*.md'), }, + { + // EasyCode enters with OpenSpec 1.14 with the Gemini-shaped TOML command format + // (description basic string + prompt multiline string) namespaced under + // `.easycode/commands/opsx/.toml`. + name: 'EasyCode', + value: 'easycode', + minCliSeries: '1.14', + available: true, + successLabel: 'EasyCode', + skillsDir: '.easycode', + capability: 'adapter-backed', + command: command('.easycode/commands/opsx/{workflow}.toml', tomlCommand), + }, { name: 'Factory Droid', value: 'factory', @@ -561,6 +649,18 @@ export const AGENT_DELIVERY_REGISTRY: ToolConfig[] = [ command: command('.gemini/commands/opsx/{workflow}.toml', tomlCommand), cleanup: projectCleanup('.gemini/commands/openspec'), }, + { + // GigaCode enters with OpenSpec 1.14: description-frontmatter markdown commands + // under `.gigacode/commands/opsx-.md`. + name: 'GigaCode', + value: 'gigacode', + minCliSeries: '1.14', + available: true, + successLabel: 'GigaCode', + skillsDir: '.gigacode', + capability: 'adapter-backed', + command: command('.gigacode/commands/opsx-{workflow}.md', yamlMarkdown('description')), + }, { name: 'GitHub Copilot', value: 'github-copilot', @@ -582,6 +682,33 @@ export const AGENT_DELIVERY_REGISTRY: ToolConfig[] = [ command: command('.github/prompts/opsx-{workflow}.prompt.md', yamlMarkdown('description')), cleanup: projectCleanup('.github/prompts/openspec-*.prompt.md'), }, + { + // Grok Build enters with OpenSpec 1.14: skills-only under the default `.grok` + // detection root, no command adapter, no restart fact. + name: 'Grok Build', + value: 'grok', + minCliSeries: '1.14', + available: true, + successLabel: 'Grok Build', + skillsDir: '.grok', + capability: 'none', + command: null, + }, + { + // GSD enters with OpenSpec 1.14 and shares the `.agents` skills root (like Amp): + // detection keys off its own `.gsd` marker. It references skills in natural + // language (upstream `NATURAL_LANGUAGE_SKILL_TOOLS`, like rovodev) and ships no + // command files. + name: 'GSD', + value: 'gsd', + minCliSeries: '1.14', + available: true, + successLabel: 'GSD', + skillsDir: '.agents', + detectionPaths: ['.gsd'], + capability: 'skills-invocable', + command: null, + }, { name: 'Hermes Agent', value: 'hermes', @@ -797,6 +924,32 @@ export const AGENT_DELIVERY_REGISTRY: ToolConfig[] = [ capability: 'adapter-backed', command: command('.trae/commands/opsx-{workflow}.md', yamlMarkdown('name', 'description')), }, + { + // Veai enters with OpenSpec 1.14: skills-only under the default `.veai` detection + // root, no command adapter, no restart fact. + name: 'Veai', + value: 'veai', + minCliSeries: '1.14', + available: true, + successLabel: 'Veai', + skillsDir: '.veai', + capability: 'none', + command: null, + }, + { + // Warp enters with OpenSpec 1.14: detection keys off `.warp` and `WARP.md`, and + // upstream classifies warp beside codex as skills-invocable — skills are its + // OpenSpec surface; it ships no command adapter and needs no IDE restart. + name: 'Warp', + value: 'warp', + minCliSeries: '1.14', + available: true, + successLabel: 'Warp', + skillsDir: '.warp', + detectionPaths: ['.warp', 'WARP.md'], + capability: 'skills-invocable', + command: null, + }, { // Zed Agent reads the shared `.agents` skills root and ships from OpenSpec 1.10. // Detection keys off `.zed` and `.agents/skills`; it has no command adapter, diff --git a/packages/core/src/cli-contracts/executor.ts b/packages/core/src/cli-contracts/executor.ts index 7cb05f16..26508446 100644 --- a/packages/core/src/cli-contracts/executor.ts +++ b/packages/core/src/cli-contracts/executor.ts @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-03 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Build root-aware read and workflow command argv. * 2. Build Store inspection and mutation argv through the official CLI surface. * 3. Build strict validate/archive argv without implicit recovery behavior. @@ -11,11 +11,15 @@ * entirely), decode findings through their own result schema, and answer a * non-admitted session with a typed refusal before any argv is composed; the CLI * owns every remaining request-combination rejection. + * 7. Own the OpenSpec 1.14 `openspec version --json` argv: `--check` (a registry probe) + * composes only when explicitly requested — the admitted-window command needs no + * capability gate, and the update UX stays out of scope. * Original request (2026-07-15): "为不同命令建立强类型适配器,不实现平行解析规则。" * Original request (2026-08-15): "v9的适配需要同时适配 1.8和1.9。" * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — version command contract (target-openspec-cli-114-line Slice 2). */ import type { z } from 'zod' import type { CliResult } from '../cli-executor.js' @@ -50,6 +54,7 @@ import { CliTemplatesSchema, CliValidateFindingsResultSchema, CliValidateSchema, + CliVersionSchema, CliWorkflowStatusSchema, type CliApplyInstructions, type CliArchive, @@ -62,6 +67,7 @@ import { type CliTemplates, type CliValidate, type CliValidateFindingsResult, + type CliVersion, type CliWorkflowStatus, } from './workflow.js' @@ -177,6 +183,20 @@ export class OpenSpecCliContractExecutor { return this.withRoot(args, options) } + /** + * Read the installed OpenSpec version envelope (OpenSpec 1.14 command family). + * + * `--check` performs a registry probe (CI/no-network and DO_NOT_TRACK opt-outs + * upstream) and composes only when explicitly requested, because the probe is an + * opt-in update check, never part of ordinary version evidence. No capability gate: + * every CLI admitted by the single-series window declares the command. + */ + async version(options: { check?: boolean } = {}): Promise> { + const args = ['version', '--json'] + if (options.check) args.push('--check') + return this.execute(args, CliVersionSchema) + } + /** List changes in the CLI-selected planning root. */ async listChanges(selector: CliRootSelector = {}): Promise> { return this.execute(this.withRoot(['list', '--json'], selector), CliChangeListSchema) diff --git a/packages/core/src/cli-contracts/workflow.test.ts b/packages/core/src/cli-contracts/workflow.test.ts index 68403fdf..5dd6b5b2 100644 --- a/packages/core/src/cli-contracts/workflow.test.ts +++ b/packages/core/src/cli-contracts/workflow.test.ts @@ -27,7 +27,17 @@ * reason and is absent when every matched file is readable, and 1.13.0/1.13.1 * payloads (also admitted by the window) decode with both members absent — never * a synthesized `false` or empty array. - + * 9. Lock the OpenSpec 1.14 typed-contract delta (references/openspec-1.14.0-report.md, + * Verified CLI observations): apply tasks gain `sourcePath` (absolute) and `line` + * (1-based) as optional members so older in-flight payloads keep decoding; the show + * Spec document gains requirement/scenario `name` members preserved by CLI value + * (non-string names rejected, old snapshots without names still decode); Status + * gains the top-level `warnings` advisory array (single success and batch healthy + * entries) verbatim with absent-when-upstream-absent semantics; the change list + * gains the per-entry `archived` boolean from `list --archived`/`--all`; and the + * new `openspec version --json` envelope decodes with `schemaVersion: 1`, install + * facts, and a passthrough `update` member that only `--check` emits. + * * Original request (2026-08-01): adapt the complete observable OpenSpec 1.7 workflow protocol. * Original request (2026-08-15): "v9的适配需要同时适配 1.8和1.9。" * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" @@ -36,6 +46,7 @@ * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — change-list nested/warnings projection (update-openspec-cli-1131 Slice 2). * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — generic diagnostics decode-regression evidence (update-openspec-cli-1131 Slice 4). * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — Apply tracking-evidence decode contract (update-openspec-cli-1132 Slice 2). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — typed contracts Slice 2 (target-openspec-cli-114-line). */ import { beforeEach, describe, expect, it, vi, type Mock, type MockInstance } from 'vitest' import { CliExecutor, type CliResult } from '../cli-executor.js' @@ -61,10 +72,12 @@ import { CliApplyInstructionsSuccessSchema, CliArchiveSchema, CliChangeListSchema, + CliShowSpecSchema, CliValidateFindingsResultSchema, CliValidateFindingsSchema, CliValidateReportSchema, CliValidateSchema, + CliVersionSchema, CliWorkflowStatusSuccessSchema, isCliValidateFindings, type CliChangeListWarning, @@ -303,9 +316,12 @@ describe('OpenSpec 1.8/1.9 workflow CLI contracts', () => { }) }) -// Admitted-line capabilities under the OpenSpecUI 13 single-series window: stable 1.13.x -// derives the 1.11-introduced batch/diff and 1.12 findings transports; the retired -// 1.10/1.11/1.12 lines derive none and prove capability-boundary rejections only. +// Admitted-line capabilities under the OpenSpecUI 14 single-series window (Slice 1 +// rotation): stable 1.14.x derives the 1.11-introduced batch/diff and 1.12 findings +// transports; every retired line (1.13/1.12/1.11/1.10) derives none and proves +// capability-boundary rejections only. Literal expectation rotation is Slice 6's +// cross-package pass; this file's fixtures rotate with the landed compat constants. +const capabilities114 = deriveOpenSpecCliCapabilities(parseOpenSpecCliVersion('1.14.0')) const capabilities113 = deriveOpenSpecCliCapabilities(parseOpenSpecCliVersion('1.13.0')) const capabilities112 = deriveOpenSpecCliCapabilities(parseOpenSpecCliVersion('1.12.0')) const capabilities111 = deriveOpenSpecCliCapabilities(parseOpenSpecCliVersion('1.11.0')) @@ -808,7 +824,7 @@ describe('OpenSpec 1.12 validate findings contract executor', () => { it('composes the findings argv only through the capability-gated findings method', async () => { const findings = await contracts.validateFindings({ target: { kind: 'scope', scope: 'all' }, - capabilities: capabilities113, + capabilities: capabilities114, }) expect(findings.kind).toBe('executed') @@ -825,12 +841,18 @@ describe('OpenSpec 1.12 validate findings contract executor', () => { expect(findingsData.report.returnedItems).toBe(1) // A non-admitted session receives the typed refusal before any argv exists: - // the retired 1.12/1.11 lines reject `--report` entirely, so no spawn may happen. + // the retired 1.13/1.12/1.11 lines reject `--report` entirely, so no spawn may happen. const refused = await contracts.validateFindings({ target: { kind: 'scope', scope: 'all' }, - capabilities: capabilities112, + capabilities: capabilities113, }) expect(refused).toEqual({ kind: 'unavailable', capability: 'findingsReport' }) + expect( + await contracts.validateFindings({ + target: { kind: 'scope', scope: 'all' }, + capabilities: capabilities112, + }) + ).toEqual({ kind: 'unavailable', capability: 'findingsReport' }) expect( await contracts.validateFindings({ target: { kind: 'scope', scope: 'all' }, @@ -1031,7 +1053,7 @@ describe('capability-gated 1.11 command argv', () => { it('builds status --all --json argv only for the admitted batch capability', async () => { const admitted = await contracts.workflowStatusAll({ - capabilities: capabilities113, + capabilities: capabilities114, schema: 'custom', store: 'shared', }) @@ -1041,10 +1063,14 @@ describe('capability-gated 1.11 command argv', () => { ['status', '--all', '--json', '--schema', 'custom', '--store', 'shared'], ]) - // Every line below the admitted 1.13 window derives no capability: the retained - // 1.12/1.11/1.10 series prove capability-boundary rejections only. - const refused = await contracts.workflowStatusAll({ capabilities: capabilities112 }) + // Every line below the admitted 1.14 window derives no capability: the retained + // 1.13/1.12/1.11/1.10 series prove capability-boundary rejections only. + const refused = await contracts.workflowStatusAll({ capabilities: capabilities113 }) expect(refused).toEqual({ kind: 'unavailable', capability: 'batchStatus' }) + expect(await contracts.workflowStatusAll({ capabilities: capabilities112 })).toEqual({ + kind: 'unavailable', + capability: 'batchStatus', + }) expect(await contracts.workflowStatusAll({ capabilities: capabilities111 })).toEqual({ kind: 'unavailable', capability: 'batchStatus', @@ -1058,7 +1084,7 @@ describe('capability-gated 1.11 command argv', () => { it('builds show --json --diff argv only for the admitted diff capability', async () => { const admitted = await contracts.showChangeDiff('add-auth', { - capabilities: capabilities113, + capabilities: capabilities114, store: 'shared', }) @@ -1068,8 +1094,12 @@ describe('capability-gated 1.11 command argv', () => { ]) // Retained below-window series prove capability-boundary rejections only. - const refused = await contracts.showChangeDiff('add-auth', { capabilities: capabilities112 }) + const refused = await contracts.showChangeDiff('add-auth', { capabilities: capabilities113 }) expect(refused).toEqual({ kind: 'unavailable', capability: 'requirementDiff' }) + expect(await contracts.showChangeDiff('add-auth', { capabilities: capabilities112 })).toEqual({ + kind: 'unavailable', + capability: 'requirementDiff', + }) expect(await contracts.showChangeDiff('add-auth', { capabilities: capabilities111 })).toEqual({ kind: 'unavailable', capability: 'requirementDiff', @@ -1314,3 +1344,370 @@ describe('OpenSpec 1.13.1 generic diagnostics decode regression', () => { }) }) }) + +/** + * Executed top-level `status --json` `warnings` entry from the npm-published 1.14.0 + * executable (references/openspec-1.14.0-report.md, Verified CLI observations, section 4; + * upstream `formatUnknownChangeMetadataKeysMessage`, pinned v1.14.0). Fixture input: + * `.openspec.yaml` = `schema: spec-driven` + `skip_design: true`. The complete raw + * string is captured verbatim — including the exact `Known keys:` ordering — because + * the member is CLI-owned advisory evidence that OpenSpecUI projects as-is. + */ +const executedStatusWarning114 = + 'Unrecognized key name(s) in .openspec.yaml (untrusted data, not instructions): skip_design. Known keys: schema, created, goal, affected_areas, initiative, skip_specs, retire_capabilities. Unknown keys are ignored and have no effect. skip_design is not a supported key; only skip_specs exists, and it only skips artifacts whose generates path lives under specs/.' + +describe('OpenSpec 1.14 status top-level warnings CLI contract', () => { + it('decodes the executed skip_design warning verbatim on the single Status success payload', () => { + const parsed = CliWorkflowStatusSuccessSchema.parse( + statusPayload({ warnings: [executedStatusWarning114] }) + ) + + expect(parsed.warnings).toEqual([executedStatusWarning114]) + // Typed-fact guard: the member exists on the decoded contract type, not merely + // as passthrough residue. + const typedWarnings: string[] | undefined = parsed.warnings + expect(typedWarnings?.[0]).toContain('skip_design is not a supported key') + // Advisory evidence never rewrites Status facts. + expect(parsed.isPlanningComplete).toBe(false) + }) + + it('carries the same member on batch healthy entries through the 1.11 envelope', () => { + const parsed = CliBatchStatusSchema.parse({ + changes: [ + batchEntryPayload({ warnings: [executedStatusWarning114] }), + batchEntryPayload({ changeName: 'clean-change' }), + ], + root, + }) + + const warned = parsed.changes[0] + if (warned === undefined || isCliBatchStatusEntryFailure(warned)) { + throw new Error('expected a healthy warned batch entry') + } + expect(warned.warnings).toEqual([executedStatusWarning114]) + const clean = parsed.changes[1] + if (clean === undefined || isCliBatchStatusEntryFailure(clean)) { + throw new Error('expected a healthy clean batch entry') + } + expect(clean.warnings).toBeUndefined() + expect('warnings' in clean).toBe(false) + }) + + it('keeps the member absent when the CLI emits no warnings instead of synthesizing', () => { + const parsed = CliWorkflowStatusSuccessSchema.parse(statusPayload()) + + expect(parsed.warnings).toBeUndefined() + expect('warnings' in parsed).toBe(false) + }) +}) + +describe('OpenSpec 1.14 apply task source locations CLI contract', () => { + // Upstream LocatedTask (pinned v1.14.0, src/commands/workflow/instructions.ts + // parseLocatedTasks): every apply task carries the absolute `sourcePath` of the file + // the checkbox lives in plus a 1-based `line`. Modeled on the executed 1.14.0 payload + // shape (references/openspec-1.14.0-report.md, Verified CLI observations): two tasks, + // both members present on each. + const executedApplyReady114 = { + ...executedApplyReady113, + tasks: [ + { + id: '1', + description: 'First task done', + done: true, + sourcePath: '/private/tmp/os114-slice2/openspec/changes/no-specs-but-tasks/tasks.md', + line: 1, + }, + { + id: '2', + description: 'Second task pending', + done: false, + sourcePath: '/private/tmp/os114-slice2/openspec/changes/no-specs-but-tasks/tasks.md', + line: 3, + }, + ], + } + + it('decodes both executed tasks with their absolute source paths and 1-based lines', () => { + const parsed = CliApplyInstructionsSuccessSchema.parse(executedApplyReady114) + + expect(parsed.tasks).toHaveLength(2) + // Typed-fact guard: source locations exist on the decoded task type, not merely + // as passthrough residue. + const first: { sourcePath: string | undefined; line: number | undefined } = parsed.tasks[0]! + expect(first.sourcePath).toBe( + '/private/tmp/os114-slice2/openspec/changes/no-specs-but-tasks/tasks.md' + ) + expect(first.sourcePath?.startsWith('/')).toBe(true) + expect(first.line).toBe(1) + expect(parsed.tasks[1]?.line).toBe(3) + // Evidence members never rewrite progress/state facts. + expect(parsed.progress).toEqual({ total: 2, complete: 1, remaining: 1 }) + expect(parsed.state).toBe('ready') + }) + + it('keeps the 1.13-era task shape decoding unchanged with both members absent', () => { + const parsed = CliApplyInstructionsSuccessSchema.parse(executedApplyReady113) + + expect(parsed.tasks[0]?.id).toBe('1') + expect(parsed.tasks[0]?.sourcePath).toBeUndefined() + expect('sourcePath' in parsed.tasks[0]!).toBe(false) + expect(parsed.tasks[0]?.line).toBeUndefined() + expect('line' in parsed.tasks[0]!).toBe(false) + }) +}) + +describe('OpenSpec 1.14 show spec requirement and scenario names CLI contract', () => { + // Upstream 1.14 show spec documents (pinned v1.14.0, src/commands/spec.ts filterSpec + + // src/core/parsers/markdown-parser.ts): every requirement carries `name` + // (normalizeRequirementName — the identity archive matches on) and every scenario + // carries `name` (scenarioNameFromHeaderText) beside the retained `text`/`rawText`. + const executedShowSpecDocument114 = { + id: 'spec-context', + title: 'Spec Context', + overview: 'The Spec Context capability owns planning context.', + requirementCount: 1, + requirements: [ + { + name: 'Planning context resolution', + text: 'The system SHALL resolve the planning context from the CLI-selected root.', + scenarios: [ + { + name: 'external active root', + rawText: '#### Scenario: external active root\n\n- WHEN the active root is external', + }, + ], + }, + ], + metadata: { version: '1.0.0', format: 'openspec' }, + root, + } + + it('decodes requirement and scenario names as typed facts preserved by CLI value', () => { + const parsed = CliShowSpecSchema.parse(executedShowSpecDocument114) + if ('status' in parsed) throw new Error('expected the successful Spec document') + + const requirement = parsed.requirements[0] + expect(requirement?.name).toBe('Planning context resolution') + expect(requirement?.text).toContain('SHALL resolve the planning context') + // Typed-fact guard: the name member exists on the decoded requirement and scenario + // types, not merely as passthrough residue. + const requirementName: string | undefined = requirement?.name + const scenarioName: string | undefined = requirement?.scenarios[0]?.name + expect([requirementName, scenarioName]).toEqual([ + 'Planning context resolution', + 'external active root', + ]) + expect(requirement?.scenarios[0]?.rawText).toContain('#### Scenario: external active root') + }) + + it('still decodes an old-shape document without names and keeps both members absent', () => { + const parsed = CliShowSpecSchema.parse({ + ...executedShowSpecDocument114, + requirements: [ + { + text: 'The system SHALL resolve the planning context from the CLI-selected root.', + scenarios: [ + { + rawText: '#### Scenario: external active root\n\n- WHEN the active root is external', + }, + ], + }, + ], + }) + if ('status' in parsed) throw new Error('expected the successful Spec document') + + const requirement = parsed.requirements[0]! + expect(requirement.name).toBeUndefined() + expect('name' in requirement).toBe(false) + expect(requirement.scenarios[0]?.name).toBeUndefined() + expect('name' in requirement.scenarios[0]!).toBe(false) + }) + + it('rejects a non-string requirement name rather than silently accepting it', () => { + const result = CliShowSpecSchema.safeParse({ + ...executedShowSpecDocument114, + requirements: [ + { + name: 42, + text: 'The system SHALL resolve the planning context.', + scenarios: [], + }, + ], + }) + + expect(result.success).toBe(false) + }) + + it('rejects a non-string scenario name rather than silently accepting it', () => { + const result = CliShowSpecSchema.safeParse({ + ...executedShowSpecDocument114, + requirements: [ + { + name: 'Planning context resolution', + text: 'The system SHALL resolve the planning context.', + scenarios: [{ rawText: '#### Scenario: external active root', name: false }], + }, + ], + }) + + expect(result.success).toBe(false) + }) +}) + +describe('OpenSpec 1.14 change-list archived inventory CLI contract', () => { + // Upstream 1.14 (pinned v1.14.0, src/core/list.ts): `list --archived --json` returns + // archived entries over the same per-entry shape, each carrying `archived: true`; + // `--all` mixes both. The flag only exists on 1.14, and default `list --json` never + // emits the member — absence stays absence. + const archivedInventoryPayload = { + changes: [ + { + name: '2026-10-02-done-change', + completedTasks: 3, + totalTasks: 3, + lastModified: '2026-10-02T00:00:00.000Z', + status: 'complete' as const, + archived: true, + }, + { + name: '2026-09-30-second-change', + completedTasks: 1, + totalTasks: 4, + lastModified: '2026-10-01T00:00:00.000Z', + status: 'in-progress' as const, + archived: true, + }, + ], + root, + status: [], + } + + it('decodes per-entry archived flags as typed facts over the shared envelope', () => { + const parsed = CliChangeListSchema.parse(archivedInventoryPayload) + + expect(parsed.changes).toHaveLength(2) + // Typed-fact guard: the archived member exists on the decoded entry type. + const archivedFlags: Array = parsed.changes.map((entry) => entry.archived) + expect(archivedFlags).toEqual([true, true]) + expect(parsed.changes[0]?.name).toBe('2026-10-02-done-change') + }) + + it('decodes the mixed --all inventory with archived false beside active entries', () => { + const parsed = CliChangeListSchema.parse({ + changes: [ + archivedInventoryPayload.changes[0], + { + name: 'active-change', + completedTasks: 0, + totalTasks: 2, + lastModified: '2026-10-02T00:00:00.000Z', + status: 'in-progress' as const, + archived: false, + }, + ], + root, + status: [], + }) + + expect(parsed.changes.map((entry) => entry.archived)).toEqual([true, false]) + }) + + it('keeps the default list decoding unchanged with archived absent', () => { + const parsed = CliChangeListSchema.parse({ + changes: [ + { + name: 'plain-change', + completedTasks: 1, + totalTasks: 1, + lastModified: '2026-10-02T00:00:00.000Z', + status: 'complete', + }, + ], + root, + status: [], + }) + + expect(parsed.changes[0]?.archived).toBeUndefined() + expect('archived' in parsed.changes[0]!).toBe(false) + }) +}) + +describe('OpenSpec 1.14 version CLI contract', () => { + // Executed `openspec version --json` envelope from the npm-published 1.14.0 + // executable without `--check` (references/openspec-1.14.0-report.md, Verified CLI + // observations, section 1): `update` is conditionally spread only under `--check`, + // so the executed no-check document carries exactly schemaVersion/version/install. + const executedVersion114 = { + schemaVersion: 1, + version: '1.14.0', + install: { + location: '/opt/homebrew/lib/node_modules/@fission-ai/openspec', + packageManager: 'npm', + scope: 'global', + }, + } + + it('decodes the executed no-check envelope with install facts and no update member', () => { + const parsed = CliVersionSchema.parse(executedVersion114) + + expect(parsed.schemaVersion).toBe(1) + expect(parsed.version).toBe('1.14.0') + expect(parsed.install).toEqual(executedVersion114.install) + expect(parsed.update).toBeUndefined() + expect('update' in parsed).toBe(false) + }) + + it('decodes a check-gated update member with passthrough tolerance for its facts', () => { + const parsed = CliVersionSchema.parse({ + ...executedVersion114, + update: { + status: 'available', + latest: '1.14.1', + command: 'npm install -g @fission-ai/openspec@latest', + canSelfUpgrade: true, + }, + }) + + expect(parsed.update?.status).toBe('available') + expect(parsed.update?.canSelfUpgrade).toBe(true) + }) + + it('decodes the unknown-install shape with every install member nullable', () => { + const parsed = CliVersionSchema.parse({ + schemaVersion: 1, + version: '1.14.0', + install: { location: null, packageManager: null, scope: null }, + }) + + expect(parsed.install).toEqual({ location: null, packageManager: null, scope: null }) + }) + + it('rejects a document with a mismatched schemaVersion literal', () => { + expect( + CliVersionSchema.safeParse({ + ...executedVersion114, + schemaVersion: 2, + }).success + ).toBe(false) + }) + + it('builds version argv without --check by default and with --check only when explicit', async () => { + const execute = vi.fn(async () => ({ + success: true, + stdout: JSON.stringify(executedVersion114), + stderr: '', + exitCode: 0, + })) + const contracts = new OpenSpecCliContractExecutor(execute) + + await contracts.version() + expect(execute.mock.calls.map(([args]) => args)).toEqual([['version', '--json']]) + + await contracts.version({ check: true }) + expect(execute.mock.calls[1]?.[0]).toEqual(['version', '--json', '--check']) + + // An explicit false stays argv-free: the registry probe is opt-in. + await contracts.version({ check: false }) + expect(execute.mock.calls[2]?.[0]).toEqual(['version', '--json']) + }) +}) diff --git a/packages/core/src/cli-contracts/workflow.ts b/packages/core/src/cli-contracts/workflow.ts index ce49066b..5bb2c92e 100644 --- a/packages/core/src/cli-contracts/workflow.ts +++ b/packages/core/src/cli-contracts/workflow.ts @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Model camelCase workflow JSON independently from Store-family JSON. * 2. Preserve strict, archived, and bulk Validate plus Archive outcomes, including failure payloads. * 3. Preserve multiline requirement bodies from `show --json`. @@ -26,6 +26,17 @@ * members: 1.13.2 always emits the boolean while 1.13.0/1.13.1 (still admitted) emit * neither, so absence stays "unknown" and is never synthesized; the array is * absent-when-everything-is-readable and both stay verbatim evidence. + * 11. Type the OpenSpec 1.14 members (references/openspec-1.14.0-report.md, Verified CLI + * observations) as additive optional facts: apply task `sourcePath` (absolute) + + * `line` (1-based, upstream LocatedTask) — older in-flight payloads may lack them and + * OpenSpecUI never re-derives them; show-Spec requirement/scenario `name` members + * (upstream normalizeRequirementName/scenarioNameFromHeaderText) preserved by CLI + * value with old snapshots still decoding; Status top-level `warnings` advisory array + * on the single success payload and every batch healthy entry (same fields schema), + * verbatim CLI evidence that never gates anything; change-list per-entry `archived` + * boolean from `list --archived`/`--all` (default list never emits it); and the new + * `openspec version --json` envelope (`schemaVersion: 1` literal, `version`, + * `install`, check-gated passthrough `update`). * * Original request (2026-07-15): "为不同命令建立强类型适配器,不实现平行解析规则。" * Original request (2026-07-26): "展开全面的接口升级和内核升级和测试升级。" @@ -35,6 +46,7 @@ * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — change-list nested/warnings projection (update-openspec-cli-1131 Slice 2). * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — Apply tracking-evidence decode contract (update-openspec-cli-1132 Slice 2). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — typed contracts Slice 2 (target-openspec-cli-114-line). */ import { z } from 'zod' import { @@ -74,6 +86,13 @@ const CliChangeListEntrySchema = z * entry ("do not treat such an entry as a change"); absent when the entry is a change. */ nested: z.array(z.string()).optional(), + /** + * OpenSpec 1.14: present only on `list --archived` / `list --all` documents, marking + * the entry as archive inventory (`true`, or `false` beside active entries under + * `--all`). The default `list --json` never emits it, so absence carries no + * information and is never synthesized. + */ + archived: z.boolean().optional(), }) .passthrough() @@ -134,11 +153,21 @@ export const CliTemplateEntrySchema = z export const CliTemplatesSchema = z.record(CliTemplateEntrySchema) -/** One requirement body shared by Spec documents and Change deltas. */ +/** + * One requirement body shared by Spec documents and Change deltas. + * + * OpenSpec 1.14 adds `name` on requirements and scenarios (upstream + * `normalizeRequirementName` / `scenarioNameFromHeaderText`): the requirement name is + * the identity archive matches on. Optional because old snapshots and in-window + * payloads may lack it; preserved strictly by CLI value, never re-derived locally. + */ export const CliSpecRequirementSchema = z .object({ + name: z.string().optional(), text: z.string(), - scenarios: z.array(z.object({ rawText: z.string() }).passthrough()), + scenarios: z.array( + z.object({ name: z.string().optional(), rawText: z.string() }).passthrough() + ), }) .passthrough() @@ -228,6 +257,14 @@ export const CliWorkflowStatusFieldsSchema = z nextSteps: z.array(z.string()), actionContext: CliActionContextSchema, artifacts: z.array(CliStatusArtifactSchema), + /** + * OpenSpec 1.14 top-level advisory warnings (unrecognized `.openspec.yaml` keys, + * upstream `formatUnknownChangeMetadataKeysMessage`). Verbatim CLI evidence, spread + * only when non-empty upstream, so absence stays absence; advisory only — never a + * gate on planning facts or actions. Shared by the single success payload and every + * batch healthy entry (the same fields schema). + */ + warnings: z.array(z.string()).optional(), }) .passthrough() @@ -286,6 +323,15 @@ const CliApplyTaskSchema = z id: z.string(), description: z.string(), done: z.boolean(), + /** + * OpenSpec 1.14 LocatedTask source locations: the absolute `sourcePath` of the file + * the checkbox lives in plus its 1-based `line`. Always present on 1.14 output, but + * optional here because older in-flight payloads and retained snapshots may lack + * them; verbatim CLI evidence ("which file the checkbox lives in") that OpenSpecUI + * never re-derives or second-guesses. + */ + sourcePath: z.string().optional(), + line: z.number().optional(), }) .passthrough() @@ -519,6 +565,40 @@ export const CliArchiveSchema = z }) .passthrough() +/** + * Typed result of the OpenSpec 1.14 `openspec version --json` command. + * + * Upstream (pinned v1.14.0, src/cli/index.ts + src/core/version-check.ts) always emits + * `schemaVersion: 1`, the running `version`, and the resolved `install` facts (every + * install member nullable — an unresolvable install reports all-null, never a guess). + * `update` is conditionally spread only under `--check` (a registry probe honoring + * CI/no-network and DO_NOT_TRACK opt-outs), so it is optional with no default and its + * facts stay passthrough evidence; the probe is opt-in and OpenSpecUI composes no + * capability gate for this admitted-window command. + */ +export const CliVersionSchema = z + .object({ + schemaVersion: z.literal(1), + version: z.string(), + install: z + .object({ + location: z.string().nullable(), + packageManager: z.enum(['npm', 'pnpm', 'bun', 'yarn', 'volta']).nullable(), + scope: z.enum(['global', 'project', 'temporary', 'source']).nullable(), + }) + .passthrough(), + update: z + .object({ + status: z.enum(['disabled', 'offline', 'available', 'current']), + latest: z.string().nullable(), + command: z.string().nullable(), + canSelfUpgrade: z.boolean(), + }) + .passthrough() + .optional(), + }) + .passthrough() + /** One CLI-reported Change row: task counts and phase straight from `openspec list`. */ export type CliChangeListEntry = z.infer @@ -547,3 +627,4 @@ export type CliValidateSummary = z.infer export type CliValidateFindings = z.infer export type CliValidateFindingsResult = z.infer export type CliArchive = z.infer +export type CliVersion = z.infer diff --git a/packages/core/src/openspec-compat.test.ts b/packages/core/src/openspec-compat.test.ts index 17dca709..1f770970 100644 --- a/packages/core/src/openspec-compat.test.ts +++ b/packages/core/src/openspec-compat.test.ts @@ -1,10 +1,11 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Prove parsing of real OpenSpec CLI version output. - * 2. Prove OpenSpecUI 13 admits only the single 1.13 series and recommends that line. - * 3. Prove older lines (including the retired 1.12 v12 window and the 1.10/1.11 v11 window), - * the future 1.14 series, prereleases, and unknown CLI versions remain blocked by default. - * 4. Prove per-command capabilities follow the admitted series law: the 1.13 target series + * 2. Prove OpenSpecUI 14 admits only the single 1.14 series and recommends that line. + * 3. Prove older lines (including the retired 1.13 v13 window, the 1.12 v12 window, and the + * 1.10/1.11 v11 window), the future 1.15 series, prereleases, and unknown CLI versions + * remain blocked by default. + * 4. Prove per-command capabilities follow the admitted series law: the 1.14 target series * grants every capability including the findings report, and a bypassed unsupported * version manufactures no command surface. * @@ -15,6 +16,7 @@ * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { describe, expect, it } from 'vitest' import { @@ -32,40 +34,40 @@ import { describe('openspec CLI compatibility law', () => { it('parses versions from raw CLI output', () => { - expect(parseOpenSpecCliVersion('1.13.0')).toEqual({ + expect(parseOpenSpecCliVersion('1.14.0')).toEqual({ major: 1, - minor: 13, + minor: 14, patch: 0, prerelease: null, }) - expect(parseOpenSpecCliVersion('openspec 1.12.0')).toEqual({ + expect(parseOpenSpecCliVersion('openspec 1.13.0')).toEqual({ major: 1, - minor: 12, + minor: 13, patch: 0, prerelease: null, }) - expect(parseOpenSpecCliVersion('openspec 1.13.0-rc.1')).toEqual({ + expect(parseOpenSpecCliVersion('openspec 1.14.0-rc.1')).toEqual({ major: 1, - minor: 13, + minor: 14, patch: 0, prerelease: 'rc.1', }) }) - it('anchors the OpenSpecUI 13 single-series range constants verbatim', () => { - expect(OPENSPECUI_TARGET_MAJOR).toBe(13) - expect(OPENSPEC_CLI_TARGET_SERIES).toBe('1.13') - expect([...OPENSPEC_CLI_SUPPORTED_SERIES]).toEqual(['1.13']) - expect(OPENSPEC_CLI_ACCEPTED_RANGE).toBe('>=1.13.0 <1.14.0') - expect(OPENSPEC_CLI_RECOMMENDED_RANGE).toBe('>=1.13.0 <1.14.0') - // The next-series boundary keeps >=1.14.0 blocked; only a separate verified decision + it('anchors the OpenSpecUI 14 single-series range constants verbatim', () => { + expect(OPENSPECUI_TARGET_MAJOR).toBe(14) + expect(OPENSPEC_CLI_TARGET_SERIES).toBe('1.14') + expect([...OPENSPEC_CLI_SUPPORTED_SERIES]).toEqual(['1.14']) + expect(OPENSPEC_CLI_ACCEPTED_RANGE).toBe('>=1.14.0 <1.15.0') + expect(OPENSPEC_CLI_RECOMMENDED_RANGE).toBe('>=1.14.0 <1.15.0') + // The next-series boundary keeps >=1.15.0 blocked; only a separate verified decision // may widen this single-series window. - expect(OPENSPEC_CLI_NEXT_SERIES_MIN_VERSION).toBe('1.14.0') - expect(OPENSPEC_CLI_REFERENCE_TAG_PATTERN).toBe('v1.13.*') + expect(OPENSPEC_CLI_NEXT_SERIES_MIN_VERSION).toBe('1.15.0') + expect(OPENSPEC_CLI_REFERENCE_TAG_PATTERN).toBe('v1.14.*') }) - it('classifies the 1.13 line as the current recommended OpenSpecUI 13 target line', () => { - for (const version of ['1.13.0', '1.13.1', 'openspec 1.13.3']) { + it('classifies the 1.14 line as the current recommended OpenSpecUI 14 target line', () => { + for (const version of ['1.14.0', '1.14.1', 'openspec 1.14.2']) { expect(classifyOpenSpecCliVersion(version)).toMatchObject({ status: 'current', supported: true, @@ -75,8 +77,8 @@ describe('openspec CLI compatibility law', () => { } }) - it('blocks the retired 1.12 v12 window with actionable v13 range copy', () => { - for (const version of ['1.12.0', '1.12.1', '1.12.5']) { + it('blocks the retired 1.13 v13 window with actionable v14 range copy', () => { + for (const version of ['1.13.0', '1.13.1', '1.13.2']) { const compatibility = classifyOpenSpecCliVersion(version) expect(compatibility).toMatchObject({ status: 'unsupported', @@ -84,14 +86,20 @@ describe('openspec CLI compatibility law', () => { recommended: false, blocksCoreInteractions: true, }) - expect(compatibility.message).toContain('>=1.13.0 <1.14.0') + expect(compatibility.message).toContain('>=1.14.0 <1.15.0') expect(compatibility.message).toContain(OPENSPEC_CLI_RECOMMENDED_RANGE) } }) - it('blocks versions below the OpenSpecUI 13 accepted range', () => { + it('blocks versions below the OpenSpecUI 14 accepted range', () => { // 1.9.x was the v9 release line's target; 1.10/1.11 were the v11 window; 1.12 was the - // v12 window; v13 blocks every older line by default. + // v12 window; 1.13 was the v13 window; v14 blocks every older line by default. + expect(classifyOpenSpecCliVersion('1.13.0')).toMatchObject({ + status: 'unsupported', + supported: false, + recommended: false, + blocksCoreInteractions: true, + }) expect(classifyOpenSpecCliVersion('1.12.0')).toMatchObject({ status: 'unsupported', supported: false, @@ -149,8 +157,8 @@ describe('openspec CLI compatibility law', () => { supported: false, blocksCoreInteractions: true, }) - // The next-series boundary: 1.14.0 is outside the accepted range and not pre-claimed. - expect(classifyOpenSpecCliVersion('1.14.0')).toMatchObject({ + // The next-series boundary: 1.15.0 is outside the accepted range and not pre-claimed. + expect(classifyOpenSpecCliVersion('1.15.0')).toMatchObject({ status: 'unsupported', supported: false, recommended: false, @@ -164,19 +172,19 @@ describe('openspec CLI compatibility law', () => { }) it('blocks every prerelease, including prereleases inside the accepted range', () => { - expect(classifyOpenSpecCliVersion('1.13.0-rc.1')).toMatchObject({ + expect(classifyOpenSpecCliVersion('1.14.0-rc.1')).toMatchObject({ status: 'unsupported', supported: false, recommended: false, blocksCoreInteractions: true, }) - expect(classifyOpenSpecCliVersion('1.13.1-beta.2')).toMatchObject({ + expect(classifyOpenSpecCliVersion('1.14.1-beta.2')).toMatchObject({ status: 'unsupported', supported: false, recommended: false, blocksCoreInteractions: true, }) - expect(classifyOpenSpecCliVersion('1.12.0-rc.1')).toMatchObject({ + expect(classifyOpenSpecCliVersion('1.13.0-rc.1')).toMatchObject({ status: 'unsupported', supported: false, blocksCoreInteractions: true, @@ -184,7 +192,7 @@ describe('openspec CLI compatibility law', () => { }) it('names the accepted and recommended ranges in mismatch evidence', () => { - const message = classifyOpenSpecCliVersion('1.12.0').message + const message = classifyOpenSpecCliVersion('1.13.0').message expect(message).toContain(OPENSPEC_CLI_ACCEPTED_RANGE) expect(message).toContain(OPENSPEC_CLI_RECOMMENDED_RANGE) expect(message).toContain(`${OPENSPECUI_TARGET_MAJOR}.x`) @@ -205,8 +213,8 @@ describe('openspec CLI compatibility law', () => { }) describe('deriveOpenSpecCliCapabilities admission boundary', () => { - it('grants every capability, including the findings report, to the admitted 1.13 series', () => { - expect(deriveOpenSpecCliCapabilities(parseOpenSpecCliVersion('1.13.0'))).toEqual({ + it('grants every capability, including the findings report, to the admitted 1.14 series', () => { + expect(deriveOpenSpecCliCapabilities(parseOpenSpecCliVersion('1.14.0'))).toEqual({ schemasRootSelector: true, archivedValidation: true, initLanguage: true, @@ -214,7 +222,7 @@ describe('deriveOpenSpecCliCapabilities admission boundary', () => { requirementDiff: true, findingsReport: true, }) - expect(deriveOpenSpecCliCapabilities(parseOpenSpecCliVersion('1.13.5'))).toEqual({ + expect(deriveOpenSpecCliCapabilities(parseOpenSpecCliVersion('1.14.5'))).toEqual({ schemasRootSelector: true, archivedValidation: true, initLanguage: true, @@ -226,15 +234,16 @@ describe('deriveOpenSpecCliCapabilities admission boundary', () => { it('grants no capabilities to bypassed unsupported or unparseable versions', () => { for (const raw of [ - '1.13.0-rc.1', - '1.13.1-beta.2', + '1.14.0-rc.1', + '1.14.1-beta.2', + '1.13.0', + '1.13.2', '1.12.0', - '1.12.5', '1.11.0', '1.10.0', '1.10.5', '1.9.0', - '1.14.0', + '1.15.0', '2.0.0', 'garbage', undefined, diff --git a/packages/core/src/openspec-compat.ts b/packages/core/src/openspec-compat.ts index 193080b2..61cbbbab 100644 --- a/packages/core/src/openspec-compat.ts +++ b/packages/core/src/openspec-compat.ts @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Encode the shipped OpenSpecUI release line's OpenSpec CLI compatibility law. * 2. Classify current, supported non-current, unsupported, and unknown CLI versions. * 3. Express the accepted range and the recommended line as separate public facts. @@ -14,8 +14,9 @@ * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ -export const OPENSPECUI_TARGET_MAJOR = 13 +export const OPENSPECUI_TARGET_MAJOR = 14 // Browser-safe contract schemas this surface re-exports for renderer evidence boundaries. // Both modules import only zod, so this subpath stays free of Node-only module graphs // (the Core barrel re-exports Node-bound values like reactive-fs and must not be imported @@ -28,33 +29,33 @@ export { isCliValidateFindings, } from './cli-contracts/workflow.js' -// OpenSpecUI 13 opens a single-series window: only stable CLI 1.13.x is published and -// fixture-verified today. Unlike v9/v11 there is no admitted non-current series; 1.14 is +// OpenSpecUI 14 opens a single-series window: only stable CLI 1.14.x is published and +// fixture-verified today. Unlike v9/v11 there is no admitted non-current series; 1.15 is // not pre-claimed, and widening to it is a separately verified decision, so -// OPENSPEC_CLI_NEXT_SERIES_MIN_VERSION keeps >=1.14.0 blocked by default. -export const OPENSPEC_CLI_TARGET_SERIES = '1.13' -export const OPENSPEC_CLI_SUPPORTED_SERIES = ['1.13'] as const -export const OPENSPEC_CLI_MIN_VERSION = '1.13.0' -export const OPENSPEC_CLI_TARGET_MIN_VERSION = '1.13.0' -export const OPENSPEC_CLI_RECOMMENDED_MIN_VERSION = '1.13.0' -export const OPENSPEC_CLI_NEXT_SERIES_MIN_VERSION = '1.14.0' -export const OPENSPEC_CLI_ACCEPTED_RANGE = '>=1.13.0 <1.14.0' -export const OPENSPEC_CLI_RECOMMENDED_RANGE = '>=1.13.0 <1.14.0' -export const OPENSPEC_CLI_REFERENCE_TAG_PATTERN = 'v1.13.*' +// OPENSPEC_CLI_NEXT_SERIES_MIN_VERSION keeps >=1.15.0 blocked by default. +export const OPENSPEC_CLI_TARGET_SERIES = '1.14' +export const OPENSPEC_CLI_SUPPORTED_SERIES = ['1.14'] as const +export const OPENSPEC_CLI_MIN_VERSION = '1.14.0' +export const OPENSPEC_CLI_TARGET_MIN_VERSION = '1.14.0' +export const OPENSPEC_CLI_RECOMMENDED_MIN_VERSION = '1.14.0' +export const OPENSPEC_CLI_NEXT_SERIES_MIN_VERSION = '1.15.0' +export const OPENSPEC_CLI_ACCEPTED_RANGE = '>=1.14.0 <1.15.0' +export const OPENSPEC_CLI_RECOMMENDED_RANGE = '>=1.14.0 <1.15.0' +export const OPENSPEC_CLI_REFERENCE_TAG_PATTERN = 'v1.14.*' /** Per-command capabilities derived from one admitted OpenSpec CLI version. */ export interface OpenSpecCliCapabilities { - /** `openspec schemas --json --store ` accepts the selector (OpenSpec 1.9+; the admitted 1.13 line). */ + /** `openspec schemas --json --store ` accepts the selector (OpenSpec 1.9+; the admitted 1.14 line). */ schemasRootSelector: boolean - /** `openspec validate --archived --json` exists (OpenSpec 1.9+; the admitted 1.13 line). */ + /** `openspec validate --archived --json` exists (OpenSpec 1.9+; the admitted 1.14 line). */ archivedValidation: boolean /** `openspec init --language ` exists (OpenSpec 1.10+). */ initLanguage: boolean - /** `openspec status --all --json` batch envelope exists (OpenSpec 1.11+; the admitted 1.13 line accepts the flag). */ + /** `openspec status --all --json` batch envelope exists (OpenSpec 1.11+; the admitted 1.14 line accepts the flag). */ batchStatus: boolean - /** `openspec show --json --diff` requirement diffs exist (OpenSpec 1.11+; the admitted 1.13 line). */ + /** `openspec show --json --diff` requirement diffs exist (OpenSpec 1.11+; the admitted 1.14 line). */ requirementDiff: boolean - /** `openspec validate --report findings --json` exists (OpenSpec 1.12+; the admitted 1.13 line; explicit bulk scope required). */ + /** `openspec validate --report findings --json` exists (OpenSpec 1.12+; the admitted 1.14 line; explicit bulk scope required). */ findingsReport: boolean } @@ -62,9 +63,9 @@ export interface OpenSpecCliCapabilities { * Derive command capabilities from one parsed CLI version. * * Only an admitted version — stable inside the accepted range — can select version-specific - * capabilities. Unsupported forms (prereleases, >=1.14, below-range) and unparseable output + * capabilities. Unsupported forms (prereleases, >=1.15, below-range) and unparseable output * keep their mismatch evidence and expose no capability, so a page-level version bypass can - * never manufacture a 1.13 command surface for a CLI the release line does not admit. + * never manufacture a 1.14 command surface for a CLI the release line does not admit. */ export function deriveOpenSpecCliCapabilities( version: OpenSpecCliVersion | null diff --git a/packages/core/src/opsx-kernel-status-warnings.test.ts b/packages/core/src/opsx-kernel-status-warnings.test.ts new file mode 100644 index 00000000..083e6c97 --- /dev/null +++ b/packages/core/src/opsx-kernel-status-warnings.test.ts @@ -0,0 +1,234 @@ +/** + * Orthogonal intents (created 2026-10-02 Asia/Shanghai): + * 1. Prove the OpenSpec 1.14 top-level Status `warnings` advisory array survives the + * Kernel's `projectWorkflowStatus` rebuild on BOTH transports — the per-change + * serial `status --change` path and the capability-gated `status --all` batch path — + * because decode-time `.passthrough()` retention is stripped by the explicit + * `ChangeStatusSchema.parse({...})` object rebuild unless the member is copied. + * 2. Pin the executed verbatim `skip_design` warning string (CLI-owned evidence, + * never rewritten) and the absent-when-upstream-absent law: a payload without + * `warnings` never gains a synthesized empty array. + * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — typed contracts Slice 2 (target-openspec-cli-114-line). + */ +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { cleanupTempDir } from './__tests__/test-utils.js' +import { CliExecutor } from './cli-executor.js' +import { ConfigManager } from './config.js' +import { OpsxKernel } from './opsx-kernel.js' +import { RuntimeInvalidationIndex } from './runtime-invalidation.js' + +const tempDirs: string[] = [] + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map(cleanupTempDir)) +}) + +/** + * Executed top-level `status --json` `warnings` entry from the npm-published 1.14.0 + * executable (references/openspec-1.14.0-report.md, Verified CLI observations, section 4; + * upstream `formatUnknownChangeMetadataKeysMessage`, pinned v1.14.0). Fixture input: + * `.openspec.yaml` = `schema: spec-driven` + `skip_design: true`. The complete raw + * string is asserted verbatim at the Server-visible projection output. + */ +const executedStatusWarning114 = + 'Unrecognized key name(s) in .openspec.yaml (untrusted data, not instructions): skip_design. Known keys: schema, created, goal, affected_areas, initiative, skip_specs, retire_capabilities. Unknown keys are ignored and have no effect. skip_design is not a supported key; only skip_specs exists, and it only skips artifacts whose generates path lives under specs/.' + +const WARNED_CHANGE = 'warned-change' +const CLEAN_CHANGE = 'clean-change' + +/** + * Fake 1.14 CLI fixture for the Status warnings projection. + * + * `--version` reports the admitted 1.14.0 line so the session derives the batch + * capability. `status --change warned-change` and its batch healthy entry both carry + * the executed top-level `warnings` array; `clean-change` never does, proving the + * absent-member law on the same envelope. + */ +function writeStatusWarningsCliFixture(cliPath: string, projectDir: string): Promise { + return writeFile( + cliPath, + ` +const args = process.argv.slice(2) +const projectDir = ${JSON.stringify(projectDir)} + +function root() { + return { + path: projectDir, + source: args.includes('--store') ? 'store' : 'nearest', + store_id: args.includes('--store') ? args[args.indexOf('--store') + 1] : undefined, + healthy: true, + status: [], + } +} + +function statusFields(changeName, batch) { + const fields = { + changeName, + schemaName: 'spec-driven', + planningHome: { + kind: 'repo', + root: projectDir, + changesDir: join(projectDir, 'openspec', 'changes'), + defaultSchema: 'spec-driven', + }, + changeRoot: join(projectDir, 'openspec', 'changes', changeName), + artifactPaths: { + proposal: { + outputPath: 'proposal.md', + resolvedOutputPath: join(projectDir, 'openspec', 'changes', changeName, 'proposal.md'), + existingOutputPaths: [], + }, + }, + isPlanningComplete: false, + isComplete: false, + applyRequires: [], + nextSteps: [], + actionContext: { + mode: 'repo-local', + sourceOfTruth: 'repo', + planningArtifacts: ['proposal'], + linkedContext: [], + allowedEditRoots: [projectDir], + requiresAffectedAreaSelection: false, + constraints: [], + }, + artifacts: [ + { id: 'proposal', outputPath: 'proposal.md', status: 'blocked', requires: [], missingDeps: ['proposal.md'] }, + ], + ...(changeName === '${WARNED_CHANGE}' + ? { warnings: [${JSON.stringify(executedStatusWarning114)}] } + : {}), + } + return batch ? fields : { ...fields, root: root() } +} + +function join(base, rest) { + return base + '/' + rest +} + +if (args.includes('--version')) { + console.log('1.14.0') + process.exit(0) +} + +if (args[0] === 'list' && args.includes('--json')) { + console.log(JSON.stringify({ + changes: ['${WARNED_CHANGE}', '${CLEAN_CHANGE}'].map((name) => ({ + name, + completedTasks: 0, + totalTasks: 1, + lastModified: '2026-10-02T00:00:00.000Z', + status: 'in-progress', + })), + root: root(), + status: [], + })) + process.exit(0) +} + +if (args[0] === 'status' && args.includes('--all') && args.includes('--json')) { + console.log(JSON.stringify({ + changes: ['${WARNED_CHANGE}', '${CLEAN_CHANGE}'].map((name) => statusFields(name, true)), + root: root(), + })) + process.exit(0) +} + +if (args[0] === 'status' && args.includes('--change')) { + const changeName = args[args.indexOf('--change') + 1] + console.log(JSON.stringify(statusFields(changeName, false))) + process.exit(0) +} + +console.error('Unsupported args:', args.join(' ')) +process.exit(1) +`.trimStart(), + 'utf8' + ) +} + +describe('OpsxKernel Status warnings projection (OpenSpec 1.14)', () => { + async function prepareKernel(): Promise<{ + kernel: OpsxKernel + executor: CliExecutor + dispose: () => Promise + }> { + const projectDir = await mkdtemp(join(tmpdir(), 'openspecui-opsx-status-warnings-')) + tempDirs.push(projectDir) + await mkdir(join(projectDir, 'openspec', 'schemas'), { recursive: true }) + await Promise.all( + [WARNED_CHANGE, CLEAN_CHANGE].map((changeId) => + mkdir(join(projectDir, 'openspec', 'changes', changeId), { recursive: true }) + ) + ) + + const cliPath = join(projectDir, 'fake-openspec.mjs') + await writeStatusWarningsCliFixture(cliPath, projectDir) + + const config = new ConfigManager(projectDir) + await config.writeConfig({ cli: { command: process.execPath, args: [cliPath] } }) + const executor = new CliExecutor(config, projectDir) + const kernel = new OpsxKernel(projectDir, executor, new RuntimeInvalidationIndex(), {}) + return { + kernel, + executor, + dispose: async () => { + kernel.dispose() + await executor.dispose() + }, + } + } + + it('keeps the executed 1.14 warnings verbatim on the single serial Status path', async () => { + const { kernel, dispose } = await prepareKernel() + try { + const status = await kernel.readStatusProjection(WARNED_CHANGE) + + expect(status.changeName).toBe(WARNED_CHANGE) + // Round-A B2: decode-time passthrough retention is stripped by the explicit + // ChangeStatusSchema.parse rebuild inside projectWorkflowStatus unless the + // member is copied — the Server-visible projection output must carry it verbatim. + expect(status.warnings).toEqual([executedStatusWarning114]) + expect(status.warnings?.[0]).toContain('skip_design is not a supported key') + // Advisory evidence never rewrites the planning-completion fact. + expect(status.isPlanningComplete).toBe(false) + } finally { + await dispose() + } + }) + + it('keeps the executed 1.14 warnings verbatim on the batch Status path', async () => { + const { kernel, dispose } = await prepareKernel() + try { + const work = await kernel.readStatusListProjection() + + const warned = work.value.find((status) => status.changeName === WARNED_CHANGE) + if (!warned) throw new Error(`expected ${WARNED_CHANGE} in the status list`) + expect(warned.warnings).toEqual([executedStatusWarning114]) + expect(warned.warnings?.[0]).toBe(executedStatusWarning114) + } finally { + await dispose() + } + }) + + it('keeps warnings absent on a payload whose CLI emitted none', async () => { + const { kernel, dispose } = await prepareKernel() + try { + const single = await kernel.readStatusProjection(CLEAN_CHANGE) + expect(single.warnings).toBeUndefined() + expect('warnings' in single).toBe(false) + + const work = await kernel.readStatusListProjection() + const clean = work.value.find((status) => status.changeName === CLEAN_CHANGE) + if (!clean) throw new Error(`expected ${CLEAN_CHANGE} in the status list`) + expect(clean.warnings).toBeUndefined() + expect('warnings' in clean).toBe(false) + } finally { + await dispose() + } + }) +}) diff --git a/packages/core/src/opsx-kernel.ts b/packages/core/src/opsx-kernel.ts index 45455d00..24b048c7 100644 --- a/packages/core/src/opsx-kernel.ts +++ b/packages/core/src/opsx-kernel.ts @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-03 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Maintain reactive CLI-backed projections and their direct typed Projection Work readers. * 2. Share and release per-entity streams through one planning-root kernel lifecycle. * 3. Keep OpenSpec configuration ownership outside the workflow cache while tracking both YAML filename variants. @@ -17,6 +17,10 @@ * `nested` are namespace folders and are excluded from `entries` and the compat `value` * at this boundary, the structural namespace-name set is derived once here, and the CLI's * top-level `warnings` ride through as display evidence (absent when the CLI omitted them). + * 9. Copy the OpenSpec 1.14 top-level Status `warnings` advisory array through the shared + * `projectWorkflowStatus` rebuild (single serial and batch transports): the member is + * verbatim CLI evidence that decode-side `.passthrough()` retention alone would strip + * at this explicit object rebuild; absent stays absent and it never gates planning facts. * * Original request (2026-07-15): "Planning-root adapters and services consume the CLI-resolved root." * Original request (2026-07-31): "系统性地进行修复,因为List页面也有类似的问题。所有可能其它页面都有类似的问题。" @@ -24,6 +28,7 @@ * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — change-list nested/warnings projection (update-openspec-cli-1131 Slice 2). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — Status warnings kernel copy (target-openspec-cli-114-line Slice 2). */ import { join, matchesGlob, relative, resolve, sep } from 'node:path' import { z } from 'zod' @@ -363,6 +368,11 @@ function projectWorkflowStatus( isPlanningComplete: data.isPlanningComplete, applyRequires: data.applyRequires, artifacts: data.artifacts, + // OpenSpec 1.14 top-level advisory warnings (unrecognized `.openspec.yaml` keys): + // copied explicitly because the decode-side `.passthrough()` retention is stripped + // by this object rebuild. Absent stays absent — never a synthesized empty array — + // and the member never gates planning facts. + ...(data.warnings !== undefined ? { warnings: data.warnings } : {}), provenance: { kind: 'cli', planningHome: data.planningHome, diff --git a/packages/core/src/opsx-types.test.ts b/packages/core/src/opsx-types.test.ts index b3b3eecd..35ffd299 100644 --- a/packages/core/src/opsx-types.test.ts +++ b/packages/core/src/opsx-types.test.ts @@ -15,14 +15,26 @@ * OpenSpec 1.13.2 (`references/openspec/src/core/artifact-graph/outputs.ts`): original * wildcards plus brace expansions and extglob groups after POSIX normalization, while * literal filenames stay literal — watcher-granularity only. + * 8. Preserve the OpenSpec 1.14 Apply task source locations (`sourcePath` absolute, + * `line` 1-based) through the input/projection schemas verbatim: optional everywhere, + * absent-when-upstream-absent, never re-derived or second-guessed locally. + * 9. Preserve the OpenSpec 1.14 Status top-level `warnings` advisory array through the + * retained `ChangeStatusSchema`: verbatim CLI evidence, absent when the CLI omitted + * it, never a gate on planning facts. * * Original request (2026-07-15): "Preserve CLI-provided paths, action context, References, and diagnostics end to end." * Original request (2026-07-23): "OPSX Status 不应等待完整 Kernel warmup,且必须保留 CLI evidence。" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — tracking-evidence projection + glob-recognition parity (update-openspec-cli-1132 Slice 2). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — typed contracts Slice 2 (target-openspec-cli-114-line). */ import { describe, expect, it } from 'vitest' -import { ApplyInstructionsSchema, ArtifactInstructionsSchema, isGlobPattern } from './opsx-types.js' +import { + ApplyInstructionsSchema, + ArtifactInstructionsSchema, + ChangeStatusSchema, + isGlobPattern, +} from './opsx-types.js' const referenceIndex = [ { @@ -272,6 +284,140 @@ describe('ApplyInstructionsSchema', () => { expect(parsed.unavailableTrackingFiles).toBeUndefined() expect('unavailableTrackingFiles' in parsed).toBe(false) }) + + it('preserves OpenSpec 1.14 apply task source locations verbatim through the projection', () => { + // Upstream LocatedTask (pinned v1.14.0): every apply task carries the absolute + // `sourcePath` of the file the checkbox lives in plus a 1-based `line`. Both ride + // the input schema and survive the transformed projection untouched — OpenSpecUI + // never re-derives or second-guesses them. + const parsed = ApplyInstructionsSchema.parse({ + ...baseApplyInstructions, + contextFiles: {}, + tasks: [ + { + id: '1', + description: 'First task done', + done: true, + sourcePath: '/repo/openspec/changes/add-example/tasks.md', + line: 1, + }, + { + id: '2', + description: 'Second task pending', + done: false, + sourcePath: '/repo/openspec/changes/add-example/tasks.md', + line: 3, + }, + ], + }) + + const first: { sourcePath: string | undefined; line: number | undefined } = parsed.tasks[0]! + expect(first.sourcePath).toBe('/repo/openspec/changes/add-example/tasks.md') + expect(first.line).toBe(1) + expect(parsed.tasks[1]?.line).toBe(3) + // Source locations never touch the progress authority. + expect(parsed.applyInstructionProgress).toMatchObject({ + total: 1, + complete: 0, + remaining: 1, + state: 'ready', + }) + }) + + it('keeps apply task source locations absent on pre-1.14 payloads without synthesizing', () => { + const parsed = ApplyInstructionsSchema.parse({ + ...baseApplyInstructions, + contextFiles: {}, + }) + + expect(parsed.tasks[0]?.sourcePath).toBeUndefined() + expect('sourcePath' in parsed.tasks[0]!).toBe(false) + expect(parsed.tasks[0]?.line).toBeUndefined() + expect('line' in parsed.tasks[0]!).toBe(false) + }) +}) + +describe('ChangeStatusSchema', () => { + /** + * Executed top-level `status --json` `warnings` entry from the npm-published 1.14.0 + * executable (references/openspec-1.14.0-report.md, Verified CLI observations, + * section 4): fixture input `.openspec.yaml` = `schema: spec-driven` + + * `skip_design: true`. Verbatim, CLI-owned advisory evidence. + */ + const executedStatusWarning114 = + 'Unrecognized key name(s) in .openspec.yaml (untrusted data, not instructions): skip_design. Known keys: schema, created, goal, affected_areas, initiative, skip_specs, retire_capabilities. Unknown keys are ignored and have no effect. skip_design is not a supported key; only skip_specs exists, and it only skips artifacts whose generates path lives under specs/.' + + const cliStatusValue = { + changeName: 'add-example', + schemaName: 'spec-driven', + isPlanningComplete: false, + applyRequires: ['tasks'], + artifacts: [ + { + id: 'proposal', + outputPath: 'proposal.md', + status: 'done' as const, + requires: [], + }, + ], + provenance: { + kind: 'cli' as const, + planningHome: { + kind: 'repo' as const, + root: '/repo', + changesDir: '/repo/openspec/changes', + defaultSchema: 'spec-driven', + }, + changeRoot: '/repo/openspec/changes/add-example', + artifactPaths: { + proposal: { + outputPath: 'proposal.md', + resolvedOutputPath: '/repo/openspec/changes/add-example/proposal.md', + existingOutputPaths: ['/repo/openspec/changes/add-example/proposal.md'], + }, + }, + nextSteps: [], + actionContext: { + mode: 'repo-local' as const, + sourceOfTruth: 'repo' as const, + planningArtifacts: ['proposal'], + linkedContext: [], + allowedEditRoots: ['/repo'], + requiresAffectedAreaSelection: false, + constraints: [], + }, + root: { path: '/repo', source: 'nearest' as const }, + evidence: { + command: 'status' as const, + success: true, + stdout: '{}', + stderr: '', + exitCode: 0, + payload: {}, + diagnostics: [], + selector: {}, + root: { path: '/repo', source: 'nearest' as const }, + }, + }, + } + + it('preserves the executed 1.14 warnings member verbatim on the retained projection', () => { + const parsed = ChangeStatusSchema.parse({ + ...cliStatusValue, + warnings: [executedStatusWarning114], + }) + + expect(parsed.warnings).toEqual([executedStatusWarning114]) + // Advisory evidence never rewrites the planning-completion fact. + expect(parsed.isPlanningComplete).toBe(false) + }) + + it('keeps the member absent when the CLI omitted it instead of synthesizing', () => { + const parsed = ChangeStatusSchema.parse(cliStatusValue) + + expect(parsed.warnings).toBeUndefined() + expect('warnings' in parsed).toBe(false) + }) }) describe('ArtifactInstructionsSchema', () => { diff --git a/packages/core/src/opsx-types.ts b/packages/core/src/opsx-types.ts index 030c9fd8..aea04aaa 100644 --- a/packages/core/src/opsx-types.ts +++ b/packages/core/src/opsx-types.ts @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Publish runtime schemas for final Projection Work payloads, including transformed Apply progress. * 2. Keep isPlanningComplete as the authoritative planning fact over any local tracked alias. * 3. Carry CLI status evidence, diagnostics, and per-artifact requires as typed facts. @@ -15,11 +15,20 @@ * recognition (OpenSpec 1.13.2, references/openspec/src/core/artifact-graph/outputs.ts): * original wildcards plus extglob groups and brace expansions after POSIX separator * normalization — watcher-granularity only, so dependency watching never forks upstream. + * 9. Project the OpenSpec 1.14 Apply task source locations (`sourcePath` absolute, + * `line` 1-based) through the shared ApplyTaskSchema verbatim: optional, never + * re-derived, absent-when-upstream-absent, never inputs to progress authority. + * 10. Project the OpenSpec 1.14 Status top-level `warnings` advisory array through the + * retained ChangeStatusSchema: verbatim CLI evidence (unrecognized `.openspec.yaml` + * keys), optional with no empty-array default, never a gate on planning facts — and + * explicitly copied through the Kernel's projectWorkflowStatus rebuild so decode-time + * passthrough retention is not lost there. * * Original request (2026-07-15): "为内核载荷建立强类型。" * Original request (2026-08-15): "v9的适配需要同时适配 1.8和1.9。" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — tracking-evidence projection + glob-recognition watcher parity (update-openspec-cli-1132 Slice 2). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — typed contracts Slice 2 (target-openspec-cli-114-line). */ import { z } from 'zod' import type { CliJsonValue } from './cli-contracts/command-result.js' @@ -145,6 +154,14 @@ export const ChangeStatusSchema = z.object({ isPlanningComplete: z.boolean(), applyRequires: z.array(z.string()), artifacts: z.array(ArtifactStatusSchema), + /** + * OpenSpec 1.14 top-level advisory warnings (unrecognized `.openspec.yaml` keys). + * Verbatim CLI evidence, absent when the CLI omitted it — never a synthesized empty + * array and never a gate on planning facts or actions. The Kernel's + * `projectWorkflowStatus` rebuild copies this member explicitly because the + * decode-side `.passthrough()` retention does not survive the object rebuild. + */ + warnings: z.array(z.string()).optional(), provenance: z.discriminatedUnion('kind', [ z.object({ kind: z.literal('cli'), @@ -176,6 +193,14 @@ export const ApplyTaskSchema = z.object({ id: z.string(), description: z.string(), done: z.boolean(), + /** + * OpenSpec 1.14 LocatedTask source locations: the absolute `sourcePath` of the file + * the checkbox lives in plus its 1-based `line`. Optional because older payloads may + * lack them; verbatim CLI evidence, never re-derived or second-guessed, and never an + * input to `createApplyInstructionProgress`. + */ + sourcePath: z.string().optional(), + line: z.number().optional(), }) export type ApplyTask = z.infer diff --git a/packages/core/src/planning-cli-projection.test.ts b/packages/core/src/planning-cli-projection.test.ts index 531e441c..4f50c6be 100644 --- a/packages/core/src/planning-cli-projection.test.ts +++ b/packages/core/src/planning-cli-projection.test.ts @@ -1,5 +1,5 @@ /** - * Orthogonal intents (created 2026-09-12 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Pin that the Planning-root CLI Projection Work contract passes the OpenSpec 1.13 * Apply `warnings` and `missingPrerequisites` members through the shared * `ApplyInstructionsProjectionSchema` value without any projection-specific branch: @@ -8,9 +8,15 @@ * carries the structural namespace-name set derived at the kernel boundary, and * passes the CLI's top-level change-list `warnings` through as display evidence * (absent when the CLI omitted them, never a synthesized empty array). + * 3. Pin the OpenSpec 1.14 members through the same shared schemas: apply task + * `sourcePath`/`line` source locations survive the apply projection verbatim, and + * the retained Status projection (`opsx-status`, `opsx-status-list`) carries the + * 1.14 top-level `warnings` advisory array verbatim — the server boundary parse + * neither drops nor synthesizes it. * * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — change-list nested/warnings projection (update-openspec-cli-1131 Slice 2). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — typed contracts Slice 2 (target-openspec-cli-114-line). */ import { describe, expect, it } from 'vitest' import { PlanningCliProjectionDataSchema } from './planning-cli-projection.js' @@ -95,6 +101,146 @@ describe('PlanningCliProjectionDataSchema opsx-apply-instructions passthrough', expect(parsed.value.warnings).toBeUndefined() expect(parsed.value.missingPrerequisites).toBeUndefined() }) + + it('preserves OpenSpec 1.14 apply task source locations through the shared projection', () => { + // Upstream LocatedTask (pinned v1.14.0): each task carries an absolute `sourcePath` + // plus a 1-based `line`. The planning projection passes them through untouched — + // OpenSpecUI never re-derives "which file the checkbox lives in". + const parsed = PlanningCliProjectionDataSchema.parse({ + kind: 'opsx-apply-instructions', + value: { + ...readyProjectionValue, + tasks: [ + { + id: '1', + description: 'First task done', + done: true, + sourcePath: + '/private/tmp/os113-slice2.SykHfF/openspec/changes/no-specs-but-tasks/tasks.md', + line: 1, + }, + { + id: '2', + description: 'Second task pending', + done: false, + sourcePath: + '/private/tmp/os113-slice2.SykHfF/openspec/changes/no-specs-but-tasks/tasks.md', + line: 3, + }, + ], + }, + }) + + if (parsed.kind !== 'opsx-apply-instructions') throw new Error('expected apply projection') + const first: { sourcePath: string | undefined; line: number | undefined } = + parsed.value.tasks[0]! + expect(first.sourcePath).toBe( + '/private/tmp/os113-slice2.SykHfF/openspec/changes/no-specs-but-tasks/tasks.md' + ) + expect(first.line).toBe(1) + expect(parsed.value.tasks[1]?.line).toBe(3) + }) +}) + +describe('PlanningCliProjectionDataSchema opsx-status warnings passthrough (OpenSpec 1.14)', () => { + /** + * Executed top-level `status --json` `warnings` entry from the npm-published 1.14.0 + * executable (references/openspec-1.14.0-report.md, Verified CLI observations, + * section 4): fixture input `.openspec.yaml` = `schema: spec-driven` + + * `skip_design: true`. Verbatim CLI-owned advisory evidence. + */ + const executedStatusWarning114 = + 'Unrecognized key name(s) in .openspec.yaml (untrusted data, not instructions): skip_design. Known keys: schema, created, goal, affected_areas, initiative, skip_specs, retire_capabilities. Unknown keys are ignored and have no effect. skip_design is not a supported key; only skip_specs exists, and it only skips artifacts whose generates path lives under specs/.' + + const statusEvidence = { + command: 'status', + success: true, + stdout: '{}', + stderr: '', + exitCode: 0, + payload: {}, + diagnostics: [], + selector: {}, + root: { path: '/repo', source: 'nearest' }, + } as const + + function statusValue(overrides: Record = {}) { + return { + changeName: 'add-example', + schemaName: 'spec-driven', + isPlanningComplete: false, + applyRequires: ['tasks'], + artifacts: [{ id: 'proposal', outputPath: 'proposal.md', status: 'done', requires: [] }], + provenance: { + kind: 'cli', + planningHome: { + kind: 'repo', + root: '/repo', + changesDir: '/repo/openspec/changes', + defaultSchema: 'spec-driven', + }, + changeRoot: '/repo/openspec/changes/add-example', + artifactPaths: { + proposal: { + outputPath: 'proposal.md', + resolvedOutputPath: '/repo/openspec/changes/add-example/proposal.md', + existingOutputPaths: ['/repo/openspec/changes/add-example/proposal.md'], + }, + }, + nextSteps: [], + actionContext: { + mode: 'repo-local', + sourceOfTruth: 'repo', + planningArtifacts: ['proposal'], + linkedContext: [], + allowedEditRoots: ['/repo'], + requiresAffectedAreaSelection: false, + constraints: [], + }, + root: { path: '/repo', source: 'nearest' }, + evidence: statusEvidence, + }, + ...overrides, + } + } + + it('carries the verbatim 1.14 warnings member on one opsx-status projection', () => { + const parsed = PlanningCliProjectionDataSchema.parse({ + kind: 'opsx-status', + value: statusValue({ warnings: [executedStatusWarning114] }), + }) + + if (parsed.kind !== 'opsx-status') throw new Error('expected status projection') + expect(parsed.value.warnings).toEqual([executedStatusWarning114]) + expect(parsed.value.isPlanningComplete).toBe(false) + }) + + it('carries the verbatim 1.14 warnings member on the opsx-status-list aggregate', () => { + const parsed = PlanningCliProjectionDataSchema.parse({ + kind: 'opsx-status-list', + value: [ + statusValue({ warnings: [executedStatusWarning114] }), + statusValue({ changeName: 'clean-change' }), + ], + evidence: statusEvidence, + }) + + if (parsed.kind !== 'opsx-status-list') throw new Error('expected status-list projection') + expect(parsed.value[0]?.warnings).toEqual([executedStatusWarning114]) + expect(parsed.value[1]?.warnings).toBeUndefined() + expect('warnings' in parsed.value[1]!).toBe(false) + }) + + it('keeps the member absent on pre-1.14 statuses without synthesizing', () => { + const parsed = PlanningCliProjectionDataSchema.parse({ + kind: 'opsx-status', + value: statusValue(), + }) + + if (parsed.kind !== 'opsx-status') throw new Error('expected status projection') + expect(parsed.value.warnings).toBeUndefined() + expect('warnings' in parsed.value).toBe(false) + }) }) describe('PlanningCliProjectionDataSchema opsx-change-list namespace/warnings', () => { diff --git a/packages/core/src/tool-init-state.test.ts b/packages/core/src/tool-init-state.test.ts index a864e8f4..7b40b5b4 100644 --- a/packages/core/src/tool-init-state.test.ts +++ b/packages/core/src/tool-init-state.test.ts @@ -2,11 +2,11 @@ * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): * 1. Preserve Tool initialization projection semantics across delivery modes and physical scopes. * 2. Bound Tool artifact observation fanout at directory-inventory scale across recomputes. - * 3. Project OpenSpec 1.13 capability, generated-version, migration, cleanup, global-root, and unavailable states. + * 3. Project OpenSpec 1.14 capability, generated-version, migration, cleanup, global-root, and unavailable states. * 4. Require exact official command contents before a commands-only install can be current, * tolerating only the OpenCode generator-owned provided-arguments injection line. - * 5. Judge generated-by staleness series-aware: only the admitted 1.13.x line is current; - * retired 1.12.x/1.11.x/1.10.x and older generators are stale. + * 5. Judge generated-by staleness series-aware: only the admitted 1.14.x line is current; + * retired 1.13.x/1.12.x/1.11.x/1.10.x and older generators are stale. * 6. Pin the rotated Kilo Code entry's runtime cleanup boundary: the legacy * `.kilocode/workflows/opsx-*` generation is ambiguity-skipped as a pattern and retired * per workflow artifact, the `openspec-*` wildcard enumerates matches as evidence @@ -24,6 +24,8 @@ * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-26): Slice 3 of the 1.13.2 patch rotation — characterize the * runtime cleanup boundary of Kilo's rotated command path at the public projection entry. + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + * — Slice 3 rotates generated-by currency to the admitted 1.14.x line. * Note: openspec-cli-112 generator fixtures below are historical/boundary evidence for staleness and * legacy-compat classification; positive v13 acceptance lives in the official-cli-v13 suite. */ @@ -127,11 +129,11 @@ describe('getToolInitStates', () => { it('reports initialized when expected skills and commands exist for delivery=both', async () => { await writeGeneratedSkill( join(tempDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md'), - '1.13.0' + '1.14.0' ) await writeGeneratedSkill( join(tempDir, '.claude', 'skills', 'openspec-apply-change', 'SKILL.md'), - '1.13.0' + '1.14.0' ) await writeArtifact(join(tempDir, '.claude', 'commands', 'opsx', 'explore.md')) await writeArtifact(join(tempDir, '.claude', 'commands', 'opsx', 'apply.md')) @@ -155,7 +157,7 @@ describe('getToolInitStates', () => { it('treats skills-only delivery as initialized without command files', async () => { await writeGeneratedSkill( join(tempDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md'), - '1.13.0' + '1.14.0' ) const states = await getToolInitStates(tempDir, { @@ -173,7 +175,7 @@ describe('getToolInitStates', () => { it('reports partial when expected command artifacts are missing', async () => { await writeGeneratedSkill( join(tempDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md'), - '1.13.0' + '1.14.0' ) const states = await getToolInitStates(tempDir, { @@ -190,11 +192,11 @@ describe('getToolInitStates', () => { it('reports partial when stale workflows are still present', async () => { await writeGeneratedSkill( join(tempDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md'), - '1.13.0' + '1.14.0' ) await writeGeneratedSkill( join(tempDir, '.claude', 'skills', 'openspec-apply-change', 'SKILL.md'), - '1.13.0' + '1.14.0' ) const states = await getToolInitStates(tempDir, { @@ -210,7 +212,7 @@ describe('getToolInitStates', () => { it('projects Codex skills at the shared .agents root with .codex as legacy migration evidence', async () => { await writeGeneratedSkill( join(tempDir, '.agents', 'skills', 'openspec-explore', 'SKILL.md'), - '1.13.0' + '1.14.0' ) const states = await getToolInitStates(tempDir, { @@ -231,7 +233,7 @@ describe('getToolInitStates', () => { it('reports Codex .codex skills as migration evidence without treating .codex as current', async () => { await writeGeneratedSkill( join(tempDir, '.codex', 'skills', 'openspec-explore', 'SKILL.md'), - '1.13.0' + '1.14.0' ) const states = await getToolInitStates(tempDir, { @@ -254,7 +256,7 @@ describe('getToolInitStates', () => { process.env.CODEX_HOME = codexHome await writeGeneratedSkill( join(tempDir, '.agents', 'skills', 'openspec-explore', 'SKILL.md'), - '1.13.0' + '1.14.0' ) await writeArtifact(join(codexHome, 'prompts', 'opsx-explore.md')) @@ -267,7 +269,7 @@ describe('getToolInitStates', () => { expect(state?.status).toBe('cleanup-needed') expect(state?.expectedSkillCount).toBe(1) expect(state?.expectedCommandCount).toBe(0) - expect(state).toHaveProperty('generatedByVersion', '1.13.0') + expect(state).toHaveProperty('generatedByVersion', '1.14.0') expect(state).toHaveProperty('cleanup.required', true) expect(state).toHaveProperty('cleanup.workflows', ['explore']) }) @@ -282,7 +284,7 @@ describe('getToolInitStates', () => { try { await writeGeneratedSkill( join(tempDir, '.minimax', 'skills', 'openspec-explore', 'SKILL.md'), - '1.13.0' + '1.14.0' ) const states = await getToolInitStates(tempDir, { @@ -347,47 +349,47 @@ describe('getToolInitStates', () => { await writeGeneratedSkill( join(tempDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md'), - '1.13.3' + '1.14.3' ) const runtimeMatched = await getToolInitStates(tempDir, { delivery: 'skills', workflows: ['explore'], - generatorVersion: '1.13.1', + generatorVersion: '1.14.1', }) expect(runtimeMatched.find((entry) => entry.toolId === 'claude')).toMatchObject({ readiness: 'initialized', status: 'initialized', - generatedByVersion: '1.13.3', + generatedByVersion: '1.14.3', issues: [], }) }) - it('treats every stable 1.13.x generator stamp as current regardless of the runtime patch', async () => { + it('treats every stable 1.14.x generator stamp as current regardless of the runtime patch', async () => { await writeGeneratedSkill( join(tempDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md'), - '1.13.2' + '1.14.2' ) await writeArtifact(join(tempDir, '.claude', 'commands', 'opsx', 'explore.md')) const states = await getToolInitStates(tempDir, { delivery: 'both', workflows: ['explore'], - generatorVersion: '1.13.0', + generatorVersion: '1.14.0', }) const state = states.find((entry) => entry.toolId === 'claude') expect(state).toMatchObject({ readiness: 'initialized', status: 'initialized', - generatedByVersion: '1.13.2', + generatedByVersion: '1.14.2', issues: [], }) }) it('flags below-admitted generator versions stale inside an admitted session', async () => { - // The retired 1.10/1.11/1.12 v11/v12 windows are below-admitted: trees those lines - // generated must read stale under a 1.13 session, exactly like older 1.9-era trees. - for (const belowAdmitted of ['1.12.0', '1.11.0', '1.10.2', '1.9.0'] as const) { + // The retired 1.10/1.11/1.12/1.13 v11–v13 windows are below-admitted: trees those lines + // generated must read stale under a 1.14 session, exactly like older 1.9-era trees. + for (const belowAdmitted of ['1.13.2', '1.12.0', '1.11.0', '1.10.2', '1.9.0'] as const) { await writeGeneratedSkill( join(tempDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md'), belowAdmitted @@ -396,7 +398,7 @@ describe('getToolInitStates', () => { const states = await getToolInitStates(tempDir, { delivery: 'skills', workflows: ['explore'], - generatorVersion: '1.13.0', + generatorVersion: '1.14.0', }) const state = states.find((entry) => entry.toolId === 'claude') @@ -435,7 +437,7 @@ describe('getToolInitStates', () => { it('reports Windsurf artifacts as a consent-gated Devin migration without deleting them', async () => { await writeGeneratedSkill( join(tempDir, '.windsurf', 'skills', 'openspec-explore', 'SKILL.md'), - '1.13.0' + '1.14.0' ) await writeArtifact(join(tempDir, '.windsurf', 'workflows', 'opsx-explore.md')) @@ -459,7 +461,7 @@ describe('getToolInitStates', () => { it('uses Qwen Markdown command artifacts and projects the shared .agents target physically', async () => { await writeGeneratedSkill( join(tempDir, '.qwen', 'skills', 'openspec-explore', 'SKILL.md'), - '1.13.0' + '1.14.0' ) await writeArtifact(join(tempDir, '.qwen', 'commands', 'opsx-explore.md')) @@ -592,7 +594,7 @@ describe('getToolInitStates', () => { readiness: 'initialized', status: 'initialized', // The pinned fallback displays only because no runtime version was supplied. - generatedByVersion: '1.13.0', + generatedByVersion: '1.14.0', issues: [], }) }) @@ -603,7 +605,7 @@ describe('getToolInitStates', () => { const states = await getToolInitStates(tempDir, { delivery: 'commands', workflows: ['explore'], - generatorVersion: '1.13.0', + generatorVersion: '1.14.0', commandContents: officialCommandContents, }) const state = states.find((entry) => entry.toolId === 'qwen') @@ -626,7 +628,7 @@ describe('getToolInitStates', () => { await writeGeneratedSkill( join(tempDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md'), - '1.13.0' + '1.14.0' ) await writeArtifact(join(tempDir, '.claude', 'commands', 'opsx', 'explore.md')) @@ -639,8 +641,8 @@ describe('getToolInitStates', () => { }) it('retains the selected registry across replacement emissions', async () => { - const registry113 = selectAgentDeliveryRegistry('1.13.2') - expect(registry113).toHaveLength(40) + const registry114 = selectAgentDeliveryRegistry('1.14.2') + expect(registry114).toHaveLength(50) const environment = new ReactiveObservationEnvironment() const releaseRoot = await environment.acquireRoot(tempDir) const context = new ReactiveContext() @@ -648,16 +650,17 @@ describe('getToolInitStates', () => { createToolInitStateProjection(tempDir, { delivery: 'skills', workflows: ['update'], - registry: registry113, + registry: registry114, }) ) try { const initial = await projection.next() expect(initial.done).toBe(false) - expect(initial.value).toHaveLength(40) - // The 1.13 snapshot inherits Antigravity's shared-root reality from 1.11, keeps - // zed, and ships the codeassistant entry the 1.12 line introduced. + expect(initial.value).toHaveLength(50) + // The 1.14 snapshot inherits Antigravity's shared-root reality from 1.11, keeps + // zed, ships the codeassistant entry the 1.12 line introduced, and projects the + // shared-root gsd entry the 1.14 line added. const antigravity = initial.value.find( (entry: ToolInitState) => entry.toolId === 'antigravity' ) @@ -670,16 +673,20 @@ describe('getToolInitStates', () => { expect(initial.value.some((entry: ToolInitState) => entry.toolId === 'codeassistant')).toBe( true ) + expect( + initial.value.find((entry: ToolInitState) => entry.toolId === 'gsd')?.skillsScope + ).toEqual({ kind: 'project', skillsDir: '.agents' }) + expect(initial.value.some((entry: ToolInitState) => entry.toolId === 'warp')).toBe(true) // A filesystem mutation forces a replacement emission through the rebuilt options; the - // 1.13 selection must survive it rather than reverting to the global newest inventory. + // 1.14 selection must survive it rather than reverting to the global newest inventory. const mutatedArtifact = join(tempDir, '.claude', 'skills', 'openspec-update', 'SKILL.md') await mkdir(dirname(mutatedArtifact), { recursive: true }) await writeFile(mutatedArtifact, 'probe', 'utf8') await settleReactivePathMutation(mutatedArtifact) const replacement = await projection.next() expect(replacement.done).toBe(false) - expect(replacement.value).toHaveLength(40) + expect(replacement.value).toHaveLength(50) expect( replacement.value.find((entry: ToolInitState) => entry.toolId === 'antigravity') ?.skillsScope @@ -688,6 +695,7 @@ describe('getToolInitStates', () => { expect( replacement.value.some((entry: ToolInitState) => entry.toolId === 'codeassistant') ).toBe(true) + expect(replacement.value.some((entry: ToolInitState) => entry.toolId === 'gsd')).toBe(true) } finally { await projection.return(undefined) await releaseRoot() @@ -697,7 +705,7 @@ describe('getToolInitStates', () => { it('projects Antigravity current roots at .agents with .agent as after-generation migration evidence', async () => { // A legacy `.agent` tree last current on the 1.10 line stays migration evidence for - // the 1.13 snapshot (the 1.11 root move carries forward). + // the 1.14 snapshot (the 1.11 root move carries forward). await writeGeneratedSkill( join(tempDir, '.agent', 'skills', 'openspec-explore', 'SKILL.md'), '1.10.3' @@ -706,7 +714,7 @@ describe('getToolInitStates', () => { const states = await getToolInitStates(tempDir, { delivery: 'both', workflows: ['explore'], - generatorVersion: '1.13.0', + generatorVersion: '1.14.0', }) const state = states.find((entry) => entry.toolId === 'antigravity') @@ -729,7 +737,7 @@ describe('getToolInitStates', () => { }) it('projects no per-tool states for retired below-range series selections', async () => { - // The retired 1.10/1.11/1.12 v11/v12 windows select no inventory, so a projection + // The retired 1.10/1.11/1.12/1.13 v11–v13 windows select no inventory, so a projection // bound to that selection observes zero tools instead of a stale per-series snapshot. await writeGeneratedSkill( join(tempDir, '.agent', 'skills', 'openspec-explore', 'SKILL.md'), @@ -737,7 +745,7 @@ describe('getToolInitStates', () => { ) await writeArtifact(join(tempDir, '.agent', 'workflows', 'opsx-explore.md')) - for (const retired of ['1.12.1', '1.10.1', '1.11.2'] as const) { + for (const retired of ['1.13.1', '1.12.1', '1.10.1', '1.11.2'] as const) { const states = await getToolInitStates(tempDir, { delivery: 'both', workflows: ['explore'], @@ -846,7 +854,7 @@ describe('getToolInitStates', () => { const skillStart = emissions.length await writeGeneratedSkill( join(tempDir, '.agents', 'skills', 'openspec-update-change', 'SKILL.md'), - '1.13.0' + '1.14.0' ) // Slow watchers can emit intermediate projections before the skill pair settles; // wait for the terminal status itself, not merely for a new emission count. @@ -869,12 +877,12 @@ describe('getToolInitStates', () => { it('detects OpenSpec 1.6 update skills and commands for Oh My Pi and Trae', async () => { await writeGeneratedSkill( join(tempDir, '.omp', 'skills', 'openspec-update-change', 'SKILL.md'), - '1.13.0' + '1.14.0' ) await writeArtifact(join(tempDir, '.omp', 'commands', 'opsx-update.md')) await writeGeneratedSkill( join(tempDir, '.trae', 'skills', 'openspec-update-change', 'SKILL.md'), - '1.13.0' + '1.14.0' ) await writeArtifact(join(tempDir, '.trae', 'commands', 'opsx-update.md')) diff --git a/packages/core/src/tool-init-state.ts b/packages/core/src/tool-init-state.ts index e7692cac..c22d6eee 100644 --- a/packages/core/src/tool-init-state.ts +++ b/packages/core/src/tool-init-state.ts @@ -1,19 +1,20 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): - * 1. Project the unified OpenSpec 1.13 Agent registry into exact skill and command artifact state. + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Project the unified OpenSpec 1.14 Agent registry into exact skill and command artifact state. * 2. Report partial, stale-version, cleanup-needed, migration-required, and unavailable states from physical evidence. * 3. Observe user-global skill roots (e.g. MiniMax Code) without ever cleaning or migrating them here. * 4. Preserve bounded reactive directory observation and fresh one-shot cache invalidation. * 5. Expose Codex managed-global-prompt observation without treating those prompts as current commands. - * 6. Judge generated-by staleness series-aware: only the admitted line (stable 1.13.x) is - * current; below-admitted generators (1.12.x, 1.11.x, 1.10.x, and older) and unparseable - * stamps are stale. + * 6. Judge generated-by staleness series-aware: only the admitted line (stable 1.14.x) is + * current; below-admitted generators (1.13.x, 1.12.x, 1.11.x, 1.10.x, and older) and + * unparseable stamps are stale. * * Original request (2026-08-01): adapt the complete OpenSpec 1.7 Agent delivery protocol for OpenSpecUI 7. * Original request (2026-08-15): "v9的适配需要同时适配 1.8和1.9。" * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { homedir } from 'node:os' @@ -53,7 +54,7 @@ export type ToolInitReadiness = 'unavailable' | 'uninitialized' | 'partial' | 'i export type ToolInitIssue = 'stale-version' | 'cleanup-needed' | 'migration-required' /** Pinned source version used when a runtime CLI version is not supplied by the Server owner. */ -export const PINNED_AGENT_GENERATOR_VERSION = '1.13.0' +export const PINNED_AGENT_GENERATOR_VERSION = '1.14.0' /** Physical delivery scope of one tool's skills inventory. */ export type ToolSkillsScope = @@ -422,9 +423,9 @@ async function areExpectedCommandContentsCurrent( /** * Series-aware generator currency. * - * Artifacts whose `generatedBy` stamp names the admitted line (stable 1.13.x) are - * current; every below-admitted generator (the retired 1.10.x/1.11.x/1.12.x windows - * and older) plus unparseable or future stamps is stale. The pinned constant never + * Artifacts whose `generatedBy` stamp names the admitted line (stable 1.14.x) are + * current; every below-admitted generator (the retired 1.10.x/1.11.x/1.12.x/1.13.x + * windows and older) plus unparseable or future stamps is stale. The pinned constant never * feeds this comparison — it is fallback display only and never fabricates a * live version. */ @@ -532,7 +533,7 @@ async function collectCleanup( : 'project-artifacts', paths, workflows, - replacementLabel: globalCleanup?.replacementLabel ?? 'OpenSpec 1.13 Agent delivery', + replacementLabel: globalCleanup?.replacementLabel ?? 'OpenSpec 1.14 Agent delivery', } } diff --git a/packages/server/src/agent-delivery-projection-service.test.ts b/packages/server/src/agent-delivery-projection-service.test.ts index e5144b9a..e87434aa 100644 --- a/packages/server/src/agent-delivery-projection-service.test.ts +++ b/packages/server/src/agent-delivery-projection-service.test.ts @@ -5,10 +5,11 @@ * 3. Prove explicit refresh and dispose own deterministic replacement and retirement boundaries. * 4. Prove version-selected inventories including unavailable-CLI sessions. * 5. Keep the initial real-registry reactive wait ahead of shared CI runners. - * 6. Lock the admitted 1.13 series snapshot: every 1.12/1.11 physical fact carries forward, - * Zed stays skills-only, Antigravity keeps its `.agents` root plus `.agent` legacy-migration - * evidence, codeassistant joins with `.codeassistant`, and retired sessions - * (1.10/1.11/1.12/1.9) select none. + * 6. Lock the admitted 1.14 series snapshot: every 1.13/1.12/1.11 physical fact carries + * forward, Zed stays skills-only, Antigravity keeps its `.agents` root plus `.agent` + * legacy-migration evidence, codeassistant joins with `.codeassistant`, the ten 1.14 + * entries join the admitted inventory, and retired sessions (1.10/1.11/1.12/1.13/1.9) + * select none. * * Original request (2026-08-01): "新增 Agent delivery projection service 及 checked tests。" @@ -17,8 +18,9 @@ * snapshot out ~1 in 2 full-suite runs (same class the opsx-kernel reactive budget was raised * for on 2026-08-14); local quiet-machine runs stay green, so the wait budget is the defect. * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" - * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" + * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { @@ -44,7 +46,7 @@ import type { ProjectionWorkSubscription } from './projection-work/index.js' const REACTIVE_MISSING_PATH_FALLBACK_MS = 1_000 const cliExecutor = { - checkAvailability: async () => ({ available: true, version: '1.13.0' }), + checkAvailability: async () => ({ available: true, version: '1.14.0' }), } const cliCommandAuthority = { getCliCommand: async () => ['not-an-importable-openspec-runner'], @@ -159,7 +161,7 @@ async function writeGeneratedSkill( await mkdir(dirname(path), { recursive: true }) await writeFile( path, - `---\nname: ${skillDirectory}\nmetadata:\n generatedBy: 1.13.0\n---\n`, + `---\nname: ${skillDirectory}\nmetadata:\n generatedBy: 1.14.0\n---\n`, 'utf8' ) return path @@ -221,7 +223,7 @@ describe('AgentDeliveryProjectionService', () => { delivery: 'skills', workflows: ['update'], }) - expect(current.registry).toHaveLength(40) + expect(current.registry).toHaveLength(50) expect(current.registry.find((tool) => tool.value === 'agents')).toMatchObject({ available: true, skillsDir: '.agents', @@ -251,9 +253,10 @@ describe('AgentDeliveryProjectionService', () => { aliases: ['windsurf'], migrations: [{ from: '.windsurf', to: '.devin', needsConsent: true }], }) - // The admitted 1.13 line projects Zed beside the shared root and Antigravity's + // The admitted 1.14 line projects Zed beside the shared root and Antigravity's // migrated `.agents` root with its legacy `.agent` evidence; codeassistant joins - // as the SourceCraft Code Assistant target introduced on the 1.12 line. + // as the SourceCraft Code Assistant target introduced on the 1.12 line, and the + // shared-root readers amp and gsd join with the 1.14 entries. expect(current.registry.find((tool) => tool.value === 'zed')).toMatchObject({ skillsDir: '.agents', detectionPaths: ['.zed', '.agents/skills'], @@ -268,7 +271,7 @@ describe('AgentDeliveryProjectionService', () => { command: { pathTemplate: '.agents/workflows/opsx-{workflow}.md' }, }) // codeassistant entered the inventory on the 1.12 line and carries forward on the - // admitted 1.13 line: SourceCraft roots, adapter-backed capability, and no + // admitted 1.14 line: SourceCraft roots, adapter-backed capability, and no // IDE-restart fact. expect(current.registry.find((tool) => tool.value === 'codeassistant')).toMatchObject({ available: true, @@ -279,9 +282,31 @@ describe('AgentDeliveryProjectionService', () => { expect( current.registry.find((tool) => tool.value === 'codeassistant')?.requiresIdeRestart ).toBeUndefined() + // The 1.14 entries project on the admitted line: shared-root amp/gsd, the + // skills-invocable warp target, and the codestudio adapter with its restart fact. + expect(current.registry.find((tool) => tool.value === 'amp')).toMatchObject({ + skillsDir: '.agents', + detectionPaths: ['.amp', '.agents/skills'], + capability: 'none', + }) + expect(current.registry.find((tool) => tool.value === 'gsd')).toMatchObject({ + skillsDir: '.agents', + detectionPaths: ['.gsd'], + capability: 'skills-invocable', + }) + expect(current.registry.find((tool) => tool.value === 'warp')).toMatchObject({ + skillsDir: '.warp', + detectionPaths: ['.warp', 'WARP.md'], + capability: 'skills-invocable', + }) + expect(current.registry.find((tool) => tool.value === 'codestudio')).toMatchObject({ + skillsDir: '.codestudio', + requiresIdeRestart: true, + command: { pathTemplate: '.codestudio/prompts/opsx-{workflow}.prompt.md' }, + }) expect(findToolState(current, 'claude')).toMatchObject({ status: 'initialized', - generatedByVersion: '1.13.0', + generatedByVersion: '1.14.0', installedSkillWorkflows: ['update'], }) @@ -323,7 +348,7 @@ describe('AgentDeliveryProjectionService', () => { try { const current = await service.getCurrent() - expect(current.registry).toHaveLength(40) + expect(current.registry).toHaveLength(50) expect(findToolState(current, 'claude')?.status).toBe('uninitialized') expect(getCliCommand).not.toHaveBeenCalled() } finally { @@ -355,7 +380,7 @@ describe('AgentDeliveryProjectionService', () => { try { const current = await service.getCurrent() - // No live CLI means no admitted inventory: the pinned 1.13.0 generator version must not + // No live CLI means no admitted inventory: the pinned 1.14.0 generator version must not // fabricate one. expect(current.registry).toEqual([]) expect(current.states).toEqual([]) @@ -433,6 +458,40 @@ describe('AgentDeliveryProjectionService', () => { } }) + it('selects an empty inventory for a retired 1.13 session and matches the Core selector', async () => { + clearCache() + const projectDir = await mkdtemp(join(tmpdir(), 'openspecui-agent-delivery-113-')) + const environment = new EnvironmentAuthorityFixture( + environmentProjection({ delivery: 'skills', workflows: ['update'] }) + ) + const observationEnvironment = new ReactiveObservationEnvironment() + const service = new AgentDeliveryProjectionService({ + projectDir, + environmentGlobalProjectionService: environment, + observationEnvironment, + cliExecutor: { + ...cliExecutor, + checkAvailability: async () => ({ available: true, version: '1.13.2' }), + }, + cliCommandAuthority, + }) + + try { + await writeGeneratedSkill(projectDir, 'update') + const current = await service.getCurrent() + + // The v13 window retired with the v14 single-series admission: a bypassed 1.13 + // session must not inherit an admitted inventory, and the service projection + // agrees exactly with the Core series selector over the same detected version. + expect(selectAgentDeliveryRegistry('1.13.2')).toEqual([]) + expect(current.registry).toEqual([]) + expect(current.states).toEqual([]) + } finally { + await service.dispose() + await rm(projectDir, { recursive: true, force: true }) + } + }) + it('selects an empty inventory for a retired 1.9 session and matches the Core selector', async () => { clearCache() const projectDir = await mkdtemp(join(tmpdir(), 'openspecui-agent-delivery-19-')) diff --git a/packages/server/src/agent-integrations-router.test.ts b/packages/server/src/agent-integrations-router.test.ts index 101cbd0d..0d5bb804 100644 --- a/packages/server/src/agent-integrations-router.test.ts +++ b/packages/server/src/agent-integrations-router.test.ts @@ -12,6 +12,7 @@ * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { @@ -105,7 +106,7 @@ async function createFixture() { }) vi.spyOn(server.cliExecutor, 'checkAvailability').mockResolvedValue({ available: true, - version: '1.13.0', + version: '1.14.0', }) disposals.push(async () => { vi.restoreAllMocks() @@ -137,7 +138,7 @@ describe('agentIntegrationsRouter', () => { .createCaller(fixture.server.createContext()) .agentIntegrations.get() - expect(projection.registry).toHaveLength(40) + expect(projection.registry).toHaveLength(50) expect(projection.policy).toEqual({ profile: 'core', delivery: 'both', @@ -153,12 +154,23 @@ describe('agentIntegrationsRouter', () => { available: true, capability: 'skills-invocable', }) - // The admitted 1.13 line projects the SourceCraft Code Assistant inventory entry - // (introduced on the 1.12 line). + // The admitted 1.14 line projects the SourceCraft Code Assistant inventory entry + // (introduced on the 1.12 line) and the ten 1.14 entries (amp, atomcode, codestudio, + // dsh, easycode, gigacode, grok, gsd, veai, warp). expect(projection.registry.find((tool) => tool.value === 'codeassistant')).toMatchObject({ available: true, skillsDir: '.codeassistant', }) + expect(projection.registry.find((tool) => tool.value === 'gsd')).toMatchObject({ + available: true, + skillsDir: '.agents', + detectionPaths: ['.gsd'], + }) + expect(projection.registry.find((tool) => tool.value === 'warp')).toMatchObject({ + available: true, + skillsDir: '.warp', + capability: 'skills-invocable', + }) }) it('delegates explicit refresh and returns the complete replacement projection', async () => { @@ -251,9 +263,9 @@ describe('agentIntegrationsRouter', () => { const handle = { settled: terminal.promise, cancel } satisfies CliStreamHandle const initStream = vi.spyOn(fixture.server.cliExecutor, 'initStream').mockReturnValue(handle) vi.spyOn(fixture.server.agentDeliveryProjectionService, 'getCurrent').mockResolvedValue({ - // The admitted 1.13 fixture must offer the requested tool: explicit Init tools are + // The admitted 1.14 fixture must offer the requested tool: explicit Init tools are // validated against the projection registry before any CLI spawn. - registry: selectAgentDeliveryRegistry('1.13.0'), + registry: selectAgentDeliveryRegistry('1.14.0'), policy: { profile: 'custom', delivery: 'commands', workflows: ['verify'] }, states: [], }) diff --git a/packages/server/src/tool-subscription-router.test.ts b/packages/server/src/tool-subscription-router.test.ts index f5ecf88e..ebaf2790 100644 --- a/packages/server/src/tool-subscription-router.test.ts +++ b/packages/server/src/tool-subscription-router.test.ts @@ -15,11 +15,13 @@ * Original request (2026-08-15): "v9的适配需要同时适配 1.8和1.9。" * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * - * The pinned fallback spec, generated-by expectations, and the versioned install-series - * assertion follow the v13 1.13 series from the shared compat constants. + * The pinned fallback spec and generated-by expectations follow the v14 1.14 series; + * the versioned install-series assertion tracks the shared compat constants (rotated + * with the compat window, Slice 6 ownership). * Original request (2026-08-28, issue #258): the global install stream installs the admitted * versioned series instead of an unversioned spec the admission gate can block. * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { clearCache, @@ -80,7 +82,7 @@ async function writeArtifact(filePath: string, content = '# external fixture\n') async function writeGeneratedSkill(filePath: string): Promise { await mkdir(dirname(filePath), { recursive: true }) - await writeFile(filePath, '---\nmetadata:\n generatedBy: "1.13.0"\n---\n', 'utf8') + await writeFile(filePath, '---\nmetadata:\n generatedBy: "1.14.0"\n---\n', 'utf8') } async function prepareCachedRunnerReplacement( @@ -169,7 +171,7 @@ async function createRouterFixture( const releaseLaunchObservation = await server.observationEnvironment.acquireRoot(launchRoot) vi.spyOn(server.cliExecutor, 'checkAvailability').mockResolvedValue({ available: true, - version: '1.13.0', + version: '1.14.0', }) const environment = { kind: 'environment-global', @@ -454,7 +456,7 @@ describe('public tool subscriptions', { timeout: SERVER_FIXTURE_TEST_TIMEOUT_MS expect(findToolState(initialized, 'claude')).toMatchObject({ status: 'initialized', readiness: 'initialized', - generatedByVersion: '1.13.0', + generatedByVersion: '1.14.0', issues: [], }) @@ -671,7 +673,7 @@ describe( await expect(Promise.all(runnerAtExit)).resolves.toMatchObject([{ version: 'runner-b' }]) expect(events).toEqual([{ type: 'exit', exitCode: 0 }]) expect(executeCommandStream).toHaveBeenCalledWith( - ['npm', 'install', '-g', '@fission-ai/openspec@1.13'], + ['npm', 'install', '-g', '@fission-ai/openspec@1.14'], expect.any(Function) ) } finally { diff --git a/scripts/diagnose-cli-runner.mjs b/scripts/diagnose-cli-runner.mjs index 91a78101..cf190328 100755 --- a/scripts/diagnose-cli-runner.mjs +++ b/scripts/diagnose-cli-runner.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Reproduce the production CLI-runner resolution path and report every candidate attempt (no short-circuit). * 2. Surface Windows npm-global extension-less shim defects that `spawn({shell:false})` cannot execute. * 3. Emit machine-readable JSON via --json so users can paste objective evidence into bug reports. @@ -25,6 +25,7 @@ * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { execFile, spawn } from 'node:child_process' import { arch, homedir, platform } from 'node:os' @@ -40,8 +41,8 @@ const SHELL_RESOLVE_TIMEOUT_MS = 5_000 // Mirror of packages/core OPENSPEC_CLI_TARGET_SERIES: fallback probes must resolve the series // the release line admits, never an unversioned @latest the gate can block. -// Keep in sync with packages/core/src/openspec-compat.ts (OpenSpecUI 13 -> 1.13). -const OPENSPEC_CLI_TARGET_SERIES = '1.13' +// Keep in sync with packages/core/src/openspec-compat.ts (OpenSpecUI 14 -> 1.14). +const OPENSPEC_CLI_TARGET_SERIES = '1.14' const OPENSPEC_CLI_FALLBACK_SPEC = `@fission-ai/openspec@${OPENSPEC_CLI_TARGET_SERIES}` const PACKAGE_MANAGER_RUNNERS = [ diff --git a/scripts/diagnose-cli-runner.test.mjs b/scripts/diagnose-cli-runner.test.mjs index 3b824f10..48339fbc 100644 --- a/scripts/diagnose-cli-runner.test.mjs +++ b/scripts/diagnose-cli-runner.test.mjs @@ -1,12 +1,12 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Prove the plain-Node Windows diagnostic resolves an npm-style CLI shim and retires its timed-out tree. * 2. Distinguish a complete diagnostic report from an early child-process failure before JSON parsing. * 3. Hide fixture subprocess console windows (`windowsHide`) for uniform hidden-console execution on Windows. * 4. Compare `where.exe` evidence through canonical paths because hosted runners mix 8.3 short * forms (RUNNER~1) with long forms in TEMP-derived fixture roots. * 5. Keep the diagnostic's fallback candidate set pinned to the Core-admitted CLI series. - * 6. Prove the rotated mirror rejects the retired 1.12 series while the 1.13 fallback spec + * 6. Prove the rotated mirror rejects the retired 1.13 series while the 1.14 fallback spec * resolves an admitted series, and keep the setup-example runner mirror on the same * rotated constant. * @@ -16,6 +16,7 @@ * out-of-range @latest as a working runner. * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { spawnSync } from 'node:child_process' import { @@ -57,7 +58,7 @@ describe('CLI runner diagnostic candidate parity', () => { expect(source).not.toMatch(/'@fission-ai\/openspec'/) }) - it('rejects a retired 1.12 probe resolution while the rotated 1.13 fallback spec stays admitted', async () => { + it('rejects a retired 1.13 probe resolution while the rotated 1.14 fallback spec stays admitted', async () => { const { readFile } = await import('node:fs/promises') const source = await readFile(DIAGNOSTIC_SCRIPT, 'utf8') const { classifyOpenSpecCliVersion, OPENSPEC_CLI_TARGET_SERIES } = await import( @@ -65,17 +66,17 @@ describe('CLI runner diagnostic candidate parity', () => { ) // The rotated mirror builds its fallback spec from the admitted series only; a stale - // 1.12 mirror spec must not survive the constants rotation. - expect(OPENSPEC_CLI_TARGET_SERIES).toBe('1.13') + // 1.13 mirror spec must not survive the constants rotation. + expect(OPENSPEC_CLI_TARGET_SERIES).toBe('1.14') expect(source).toContain( 'const OPENSPEC_CLI_FALLBACK_SPEC = `@fission-ai/openspec@${OPENSPEC_CLI_TARGET_SERIES}`' ) - expect(source).not.toContain('@fission-ai/openspec@1.12') - expect(source).not.toContain("'1.12'") + expect(source).not.toContain('@fission-ai/openspec@1.13') + expect(source).not.toContain("'1.13'") - // A probe that resolves the retired 1.12 series reports a version the gate blocks, + // A probe that resolves the retired 1.13 series reports a version the gate blocks, // while the pinned fallback resolves the admitted current series. - expect(classifyOpenSpecCliVersion('1.12.5')).toMatchObject({ + expect(classifyOpenSpecCliVersion('1.13.5')).toMatchObject({ status: 'unsupported', blocksCoreInteractions: true, }) diff --git a/scripts/setup-example.ts b/scripts/setup-example.ts index f80b8237..41cb9132 100644 --- a/scripts/setup-example.ts +++ b/scripts/setup-example.ts @@ -1,6 +1,6 @@ #!/usr/bin/env tsx /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Scaffold the E2E example project (openspec/ structure, sample specs and changes, * .openspecui.json) for `pnpm example:setup [--clean]`. * 2. Pin the example's npx runner to the admitted CLI series instead of an unversioned @@ -11,6 +11,7 @@ * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { existsSync } from 'fs' @@ -238,8 +239,8 @@ const SAMPLE_AGENTS_MD = `# AI Agent Instructions // Mirror of packages/core OPENSPEC_CLI_TARGET_SERIES: the example runner must resolve the // series this release line admits, never an unversioned @latest the gate can block. -// Keep in sync with packages/core/src/openspec-compat.ts (OpenSpecUI 13 -> 1.13). -const OPENSPEC_CLI_TARGET_SERIES = '1.13' +// Keep in sync with packages/core/src/openspec-compat.ts (OpenSpecUI 14 -> 1.14). +const OPENSPEC_CLI_TARGET_SERIES = '1.14' const CONFIG = { cli: { From 57e55e028541f82e43562a87871c10c48de2e605 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 2 Oct 2026 04:16:37 +0800 Subject: [PATCH 08/12] test(core,web,server): complete the CLI 1.14 fixture matrix, status warnings surface, and cross-package alignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Batch B of the target-openspec-cli-114-line change: - Slice 4: status warnings render on the Change Detail direct plane — collapsed advisory summary row (role=status, openspec status attribution) with in-place verbatim expansion; absent field degrades with zero synthesis and never gates actions; static mode shares the projection and renders nothing. - Slice 5: pin guards rotated to v1.14.0 (94ca9c1e) with recorded red; openspec-cli-114 alias added (shim 1.14.0, lockfile +23 lines) while openspec-cli-112 stays for its two historical consumers; 11 v13 suites migrated (apply keeps 1132 tracking regressions and adds sourcePath/line; default-store adds store edit-roots both branches; show adds spec requirement/scenario names) plus version-report and change-list-inventory suites; 1.13.2 boundary suite proves the retired line rejected with 1.14-only members absent; v13/v12 helpers and v12 boundary suite retired with clean orphan scan; mutation red recorded and restored. - Slice 6: four alignment groups rotated (web compat copy, server evidence, web evidence, cli/scripts) — real reds recorded where literals rotated, constant-follow recorded elsewhere; production logic untouched. - Lane hygiene: Batch A typed-fact annotations corrected to optional members; opsx-kernel-schemas-root fixture migrated off the retired v13 helper. Integrator gates: core 18 files/91 tests, web 6 files/65 tests (cross-slice change-view fixpoint closed), server 4 files, core+web typecheck clean. --- .../loop/checkpoints.md | 6 +- .../loop/implementation.md | 61 ++++ packages/core/package.json | 1 + .../__tests__/official-cli-v13-fixtures.ts | 148 --------- ...xtures.ts => official-cli-v14-fixtures.ts} | 41 ++- .../core/src/agent-command-content.test.ts | 2 +- .../core/src/cli-contracts/workflow.test.ts | 13 +- ...official-cli-v12-boundary-fixtures.test.ts | 125 -------- ...official-cli-v13-boundary-fixtures.test.ts | 226 +++++++++++++ ...ial-cli-v13-default-store-fixtures.test.ts | 119 ------- ...l-cli-v14-agent-delivery-fixtures.test.ts} | 30 +- ...-cli-v14-apply-readiness-fixtures.test.ts} | 60 ++-- ...ial-cli-v14-batch-status-fixtures.test.ts} | 21 +- ...v14-change-list-inventory-fixtures.test.ts | 129 ++++++++ ...ial-cli-v14-default-store-fixtures.test.ts | 206 ++++++++++++ ...al-cli-v14-nested-change-fixtures.test.ts} | 23 +- ...cial-cli-v14-nested-spec-fixtures.test.ts} | 28 +- ...=> official-cli-v14-show-fixtures.test.ts} | 95 +++++- ...ial-cli-v14-task-reading-fixtures.test.ts} | 23 +- ...-v14-validation-findings-fixtures.test.ts} | 37 ++- ...-cli-v14-validation-full-fixtures.test.ts} | 27 +- ...al-cli-v14-version-report-fixtures.test.ts | 63 ++++ ...fficial-cli-v14-workflow-fixtures.test.ts} | 33 +- .../opsx-kernel-schemas-root.fixtures.test.ts | 19 +- packages/core/src/opsx-types.test.ts | 8 +- packages/core/src/tool-init-state.test.ts | 2 +- .../src/upstream-contract-regression.test.ts | 11 +- .../core/tsconfig.agent-delivery-tests.json | 2 +- .../core/tsconfig.default-store-tests.json | 2 +- packages/core/tsconfig.nested-spec-tests.json | 2 +- .../tsconfig.workflow-contract-tests.json | 20 +- .../src/change-diff-evidence-service.test.ts | 29 +- .../src/cli-validate-findings-router.test.ts | 17 +- ...oot-context-cold-start.integration.test.ts | 11 +- packages/server/src/router.test.ts | 5 +- .../scripts/w2-project-binding-playwright.ts | 10 +- .../archived-validation-evidence.test.tsx | 7 +- .../src/components/cli-health-gate.test.tsx | 61 ++-- .../web/src/components/cli-health-gate.tsx | 7 +- .../openspec-settings-diagnostics.tsx | 5 +- .../status-warnings-notice.test.tsx | 76 +++++ .../src/components/status-warnings-notice.tsx | 93 ++++++ .../validation-findings-evidence.test.tsx | 15 +- packages/web/src/lib/use-cli-runner.tsx | 3 +- .../change-view-status-warnings.test.tsx | 297 ++++++++++++++++++ packages/web/src/routes/change-view.test.tsx | 9 +- packages/web/src/routes/change-view.tsx | 15 +- packages/web/src/routes/settings.tsx | 7 +- pnpm-lock.yaml | 23 ++ scripts/prepare-openspec-reference.mjs | 9 +- 50 files changed, 1613 insertions(+), 669 deletions(-) delete mode 100644 packages/core/src/__tests__/official-cli-v13-fixtures.ts rename packages/core/src/__tests__/{official-cli-v12-fixtures.ts => official-cli-v14-fixtures.ts} (75%) delete mode 100644 packages/core/src/official-cli-v12-boundary-fixtures.test.ts create mode 100644 packages/core/src/official-cli-v13-boundary-fixtures.test.ts delete mode 100644 packages/core/src/official-cli-v13-default-store-fixtures.test.ts rename packages/core/src/{official-cli-v13-agent-delivery-fixtures.test.ts => official-cli-v14-agent-delivery-fixtures.test.ts} (89%) rename packages/core/src/{official-cli-v13-apply-readiness-fixtures.test.ts => official-cli-v14-apply-readiness-fixtures.test.ts} (88%) rename packages/core/src/{official-cli-v13-batch-status-fixtures.test.ts => official-cli-v14-batch-status-fixtures.test.ts} (90%) create mode 100644 packages/core/src/official-cli-v14-change-list-inventory-fixtures.test.ts create mode 100644 packages/core/src/official-cli-v14-default-store-fixtures.test.ts rename packages/core/src/{official-cli-v13-nested-change-fixtures.test.ts => official-cli-v14-nested-change-fixtures.test.ts} (90%) rename packages/core/src/{official-cli-v13-nested-spec-fixtures.test.ts => official-cli-v14-nested-spec-fixtures.test.ts} (75%) rename packages/core/src/{official-cli-v13-show-diff-fixtures.test.ts => official-cli-v14-show-fixtures.test.ts} (64%) rename packages/core/src/{official-cli-v13-task-reading-fixtures.test.ts => official-cli-v14-task-reading-fixtures.test.ts} (84%) rename packages/core/src/{official-cli-v13-validation-findings-fixtures.test.ts => official-cli-v14-validation-findings-fixtures.test.ts} (93%) rename packages/core/src/{official-cli-v13-validation-full-fixtures.test.ts => official-cli-v14-validation-full-fixtures.test.ts} (94%) create mode 100644 packages/core/src/official-cli-v14-version-report-fixtures.test.ts rename packages/core/src/{official-cli-v13-workflow-fixtures.test.ts => official-cli-v14-workflow-fixtures.test.ts} (90%) create mode 100644 packages/web/src/components/status-warnings-notice.test.tsx create mode 100644 packages/web/src/components/status-warnings-notice.tsx create mode 100644 packages/web/src/routes/change-view-status-warnings.test.tsx diff --git a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md index a698b250..db80efff 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md @@ -32,8 +32,8 @@ Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下, ## CP3 — Web + fixtures (Slices 4-5) -- [ ] Status warnings direct-plane summary row; web focused tests green. -- [ ] Pin guards rotated; `openspec-cli-114` alias with lockfile+shim evidence; full v14 positive matrix — +- [x] Status warnings direct-plane summary row; web focused tests green. +- [x] Pin guards rotated; `openspec-cli-114` alias with lockfile+shim evidence; full v14 positive matrix — 11 migrated suites + `v14-version-report` + `v14-change-list-inventory` (13 files) — green with `--version` provenance; the 1.13.2 boundary suite proves the just-retired line rejected (identity + unsupported classification + 1.14-only members absent; v12 boundary suite and v12 helper retired with @@ -42,7 +42,7 @@ Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下, ## CP4 — Alignment (Slice 6) -- [ ] Four owner groups red/green recorded. +- [x] Four owner groups red/green recorded (constant-follow noted where applicable; production untouched). ## CP5 — Delivery diff --git a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md index ad61a465..174f7e82 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md @@ -163,3 +163,64 @@ deferred; the registry + its test were updated together in the integrator pass. - `pnpm --filter @openspecui/server run typecheck` (10 tsconfig lanes) → 0 errors. - Note for future rounds: multi-package `pnpm --filter A --filter B exec tsc` produces spurious TS6059 rootDir errors in this repo; per-package invocations are authoritative. + +## Batch B implementation record (2026-10-02) + +### Slice 4 — Status warnings Web direct plane (CP3) + +Production: `packages/web/src/routes/change-view.tsx` (statusRegion mounts the notice; +`hasDirectStatus` includes warnings presence). New owners: +`components/status-warnings-notice.tsx` (collapsed summary row `⚠ N status warning(s) — reported by +openspec status`, `role="status"`, amber advisory tone, in-place verbatim expansion, zero synthesis when +absent, never gates actions) + component test + route-level test in the dedicated +`routes/change-view-status-warnings.test.tsx` (change-view.test.tsx untouched by this slice to avoid +parallel-write overlap). Parallel implementation reusing the v13 apply-progress summary-row pattern was +chosen over extracting a shared component (the v13 notice is a compound surface; extraction would +re-verify its 12 assertions for a thin disclosure shell). Red: pre-render route test failed with the +status region absent. Green: 3 files / 17 tests + web typecheck. + +### Slice 5 — Pin guards + v14 fixture matrix (CP3) + +Pin guards rotated with a real red (`upstream-contract-regression` expected the v1.13.2 commit while the +submodule sat at `94ca9c1e`): `scripts/prepare-openspec-reference.mjs`, +`upstream-contract-regression.test.ts`, `packages/web/scripts/w2-project-binding-playwright.ts` (pin + +`--version` 1.13.2→1.14.0). Alias: `openspec-cli-114: npm:@fission-ai/openspec@1.14.0` added +(shim `--version` = 1.14.0 verified; lockfile +23 lines exactly); **`openspec-cli-112` retained** for its +two historical consumers. Matrix: new `__tests__/official-cli-v14-fixtures.ts` helper; 11 suites migrated +with git mv (apply-readiness keeps the 1132 tracking regressions and adds sourcePath/line assertions; +default-store adds the store edit-roots both-branches scenarios; show suite adds spec +requirement/scenario names; workflow mixed-tasks strict equality updated to real 1.14 output); +2 new suites (version-report envelope without network probing; change-list-inventory with +`--archived`/`--all`/default tri-state); boundary suite `official-cli-v13-boundary-fixtures.test.ts` +proves the just-retired 1.13.2: identity, unsupported classification, and 1.14-only members absent +(never asserting v13-era members absent). Retired: v13/v12 helpers + v12 boundary suite with rg orphan +scan clean. Mutation red recorded (helper bins pointed at 113 → version provenance assertion failed → +restored, zero residue). Two unplanned but evidenced fixes: `opsx-kernel-schemas-root.fixtures.test.ts` +migrated off the deleted v13 helper, and Batch A's `workflow.test.ts`/`opsx-types.test.ts` typed-fact +annotations corrected to optional members so the workflow-contract typecheck lane compiles (both files +were byte-identical to HEAD, proving pre-existing lane debt outside base `tsc --noEmit` scope). +Green: 15 files / 52 tests (heavy files also run split); core typecheck incl. all lanes. + +Real 1.14-vs-1.13 behavior deltas recorded in the slice report (apply source locations; show names; +status verbatim warning incl. the full known-keys list; store edit-roots two branches with exact +constraint strings — declaring-project `[implementationRoot, storeRoot]`, no-declaring `[storeRoot]` + +ask-user; list archived member; version envelope with pnpm install location). + +### Slice 6 — Cross-package alignment (CP4) + +Four owner groups, red-first where a real literal rotation happened, "constant-follow" recorded where +shared constants already carry the window: web compat copy (cli-health-gate suite 9-failed red → 87 +passed across 4 files), server evidence (change-diff 6-failed red → 9/9; findings-router 4-failed → 4/4; +router admitted fixture → 107/107; cold-start integration pin+version → 1/1), web evidence (19-failed +red across three files → 49/49), CLI/scripts (worktree-instance-manager 22/22 constant-follow; +setup-example consumer verified via static mirror equality). `tool-subscription-router.test.ts` needed +zero changes (Batch A integrator pass had rotated it). 12 files, +94/−82, production untouched; +"OpenSpec 1.13/1.13.2" prose kept only as upstream provenance in feature-origin notes. + +### Batch B integrator verification (2026-10-02) + +Cross-slice check after three parallel agents shared one worktree: core 18 files / 91 tests green +(matrix + boundary + regression + schemas-root + Batch A suites), web 6 files / 65 tests green — +including the cross-slice fixpoint: Slice 4's reported `change-view.test.tsx` 24/25 failure (admitted +fixture 1.13.0 rejected by the rotated window) is closed by Slice 6's literal rotation, now 25/25 — +server 4 files green, core+web typecheck clean, no orphan references, lockfile scoped to the new alias. diff --git a/packages/core/package.json b/packages/core/package.json index a87b213f..cc269367 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -256,6 +256,7 @@ "openspec-cli-111": "npm:@fission-ai/openspec@1.11.0", "openspec-cli-112": "npm:@fission-ai/openspec@1.12.0", "openspec-cli-113": "npm:@fission-ai/openspec@1.13.2", + "openspec-cli-114": "npm:@fission-ai/openspec@1.14.0", "tsdown": "^0.16.6", "typescript": "^5.7.2", "vitest": "^4.1.0" diff --git a/packages/core/src/__tests__/official-cli-v13-fixtures.ts b/packages/core/src/__tests__/official-cli-v13-fixtures.ts deleted file mode 100644 index 14028880..00000000 --- a/packages/core/src/__tests__/official-cli-v13-fixtures.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): - * 1. Run the pinned OpenSpec 1.13.2 npm executable for the v13 fixture matrix (the retired - * 1.12.0 line stays with the v12 helper for boundary negatives). - * 2. Share one isolated fixture environment and hidden-console runner across fixture files. - * 3. Keep fixture identity explicit so a passing matrix always names its executable line. - * 4. Assert the 1.13 JSON stream discipline. - * - * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" - * Original request (2026-09-17): rotate the pinned positive executable from 1.13.0 to the released - * 1.13.1 patch inside the unchanged v13 window (in-window patch rotation; the 1.13.0 executable - * retires from the matrix, keeping one pinned positive per series). - * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — in-window patch rotation 1.13.1 -> 1.13.2. - */ -import { execFile } from 'node:child_process' -import { mkdtemp, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join, resolve } from 'node:path' -import { expect } from 'vitest' - -/** OpenSpec CLI line admitted by the OpenSpecUI 13 compatibility law (single-series window). */ -export const PINNED_OPENSPEC_V13_VERSIONS = ['1.13.2'] as const - -export type PinnedOpenspecV13Version = (typeof PINNED_OPENSPEC_V13_VERSIONS)[number] - -const PINNED_V13_BINS = { - '1.13.2': resolve(import.meta.dirname, '../../node_modules/openspec-cli-113/bin/openspec.js'), -} satisfies Record - -export interface CliRunResult { - exitCode: number - stdout: string - stderr: string -} - -/** - * Isolated machine fixture environment: no shared HOME, config, data, state, or cache. - * - * Both XDG_CONFIG_HOME and XDG_DATA_HOME must stay inside the fixture root: config owns - * machine-global profile selection and data owns the Store registry/user schemas, so a - * leaked machine profile would change 1.13 Agent artifact counts (the isolated default - * core profile generates 6 skills + 6 commands; wider custom profiles generate more). - */ -export function pinnedFixtureEnv(root: string): NodeJS.ProcessEnv { - return { - ...process.env, - HOME: join(root, 'home'), - XDG_CONFIG_HOME: join(root, 'config'), - XDG_DATA_HOME: join(root, 'data'), - XDG_STATE_HOME: join(root, 'state'), - XDG_CACHE_HOME: join(root, 'cache'), - OPEN_SPEC_INTERACTIVE: '0', - OPENSPEC_TELEMETRY: '0', - NO_COLOR: '1', - } -} - -/** Execute one pinned OpenSpec executable with hidden console windows. */ -export function runPinnedOpenspec( - version: PinnedOpenspecV13Version, - args: readonly string[], - cwd: string, - env: NodeJS.ProcessEnv -): Promise { - return new Promise((complete) => { - execFile( - process.execPath, - [PINNED_V13_BINS[version], ...args], - { cwd, env, maxBuffer: 4 * 1024 * 1024, timeout: 30_000, windowsHide: true }, - (error, stdout, stderr) => { - complete({ - exitCode: typeof error?.code === 'number' ? error.code : error ? 1 : 0, - stdout, - stderr, - }) - } - ) - }) -} - -/** - * Assert the executable line identity before any contract assertion uses it. - * - * This is the provenance guard: a bins-map entry accidentally pointing at another alias - * (for example openspec-cli-112) prints a different `--version` string and fails here, - * so a passing matrix always proves it ran the 1.13.2 line. - */ -export async function expectPinnedVersion( - version: PinnedOpenspecV13Version, - cwd: string, - env: NodeJS.ProcessEnv -): Promise { - const result = await runPinnedOpenspec(version, ['--version'], cwd, env) - expect(result.exitCode, result.stdout + '\n' + result.stderr).toBe(0) - expect(result.stdout.trim()).toBe(version) -} - -/** - * Assert the 1.13 JSON stream discipline on one `--json` run. - * - * The 1.13 line keeps the discipline proven for admitted lines since the eager-JSON fast - * path: a JSON invocation must leave stdout as exactly one complete JSON document and - * stderr free of telemetry/tip noise. This remains fixture evidence for the eager-JSON - * fast path, which must keep refusing early exit while stderr carries real diagnostics; - * it does not license ignoring stderr generally. - */ -export function expectPinnedJsonDiscipline(result: CliRunResult): void { - // A JSON run prints exactly one document on stdout; `schemas --json` success is an - // array while most commands print an object, so either top-level shape is valid. - let parsed: unknown - try { - parsed = JSON.parse(result.stdout) - } catch (error) { - expect.fail( - `stdout is not one complete JSON document: ${result.stdout.slice(0, 200)}\n${result.stderr}`, - String(error) - ) - } - expect( - (typeof parsed === 'object' && parsed !== null) || Array.isArray(parsed), - result.stdout.slice(0, 200) - ).toBe(true) - expect(result.stderr, 'JSON runs must not emit telemetry or completion-tip noise').not.toMatch( - /telemetry|completions|tip/i - ) -} - -/** Fixture root that removes itself in afterEach when handed back. */ -export async function createPinnedFixtureRoot(label: string): Promise { - return mkdtemp(join(tmpdir(), `openspecui-${label}-`)) -} - -/** Remove one fixture root created by {@link createPinnedFixtureRoot}. */ -export async function removePinnedFixtureRoot(root: string | null): Promise { - if (!root) return - await rm(root, { recursive: true, force: true }) -} - -/** Parse CLI JSON output through one schema without asserting the exit code. */ -export function parsePinnedJson(result: CliRunResult, parse: (payload: unknown) => T): T { - return parse(JSON.parse(result.stdout)) -} - -/** Parse successful CLI JSON output through one schema, asserting exit code 0. */ -export function parsePinnedSuccessJson(result: CliRunResult, parse: (payload: unknown) => T): T { - expect(result.exitCode, result.stdout + '\n' + result.stderr).toBe(0) - return parse(JSON.parse(result.stdout)) -} diff --git a/packages/core/src/__tests__/official-cli-v12-fixtures.ts b/packages/core/src/__tests__/official-cli-v14-fixtures.ts similarity index 75% rename from packages/core/src/__tests__/official-cli-v12-fixtures.ts rename to packages/core/src/__tests__/official-cli-v14-fixtures.ts index 9988633c..6037e218 100644 --- a/packages/core/src/__tests__/official-cli-v12-fixtures.ts +++ b/packages/core/src/__tests__/official-cli-v14-fixtures.ts @@ -1,12 +1,12 @@ /** - * Orthogonal intents (created 2026-09-03 Asia/Shanghai): - * 1. Run the pinned OpenSpec 1.12.0 npm executable for the v12 fixture matrix (1.11.0 - * boundary negatives stay with the v11 helper). + * Orthogonal intents (created 2026-10-02 Asia/Shanghai): + * 1. Run the pinned OpenSpec 1.14.0 npm executable for the v14 fixture matrix (the retired + * 1.13.2 line stays available through the openspec-cli-113 alias for boundary negatives). * 2. Share one isolated fixture environment and hidden-console runner across fixture files. * 3. Keep fixture identity explicit so a passing matrix always names its executable line. - * 4. Assert the 1.12 JSON stream discipline. + * 4. Assert the 1.14 JSON stream discipline. * - * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { execFile } from 'node:child_process' import { mkdtemp, rm } from 'node:fs/promises' @@ -14,14 +14,14 @@ import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { expect } from 'vitest' -/** OpenSpec CLI line admitted by the OpenSpecUI 12 compatibility law (single-series window). */ -export const PINNED_OPENSPEC_V12_VERSIONS = ['1.12.0'] as const +/** OpenSpec CLI line admitted by the OpenSpecUI 14 compatibility law (single-series window). */ +export const PINNED_OPENSPEC_V14_VERSIONS = ['1.14.0'] as const -export type PinnedOpenspecV12Version = (typeof PINNED_OPENSPEC_V12_VERSIONS)[number] +export type PinnedOpenspecV14Version = (typeof PINNED_OPENSPEC_V14_VERSIONS)[number] -const PINNED_V12_BINS = { - '1.12.0': resolve(import.meta.dirname, '../../node_modules/openspec-cli-112/bin/openspec.js'), -} satisfies Record +const PINNED_V14_BINS = { + '1.14.0': resolve(import.meta.dirname, '../../node_modules/openspec-cli-114/bin/openspec.js'), +} satisfies Record export interface CliRunResult { exitCode: number @@ -34,7 +34,7 @@ export interface CliRunResult { * * Both XDG_CONFIG_HOME and XDG_DATA_HOME must stay inside the fixture root: config owns * machine-global profile selection and data owns the Store registry/user schemas, so a - * leaked machine profile would change 1.12 Agent artifact counts (the isolated default + * leaked machine profile would change 1.14 Agent artifact counts (the isolated default * core profile generates 6 skills + 6 commands; wider custom profiles generate more). */ export function pinnedFixtureEnv(root: string): NodeJS.ProcessEnv { @@ -53,7 +53,7 @@ export function pinnedFixtureEnv(root: string): NodeJS.ProcessEnv { /** Execute one pinned OpenSpec executable with hidden console windows. */ export function runPinnedOpenspec( - version: PinnedOpenspecV12Version, + version: PinnedOpenspecV14Version, args: readonly string[], cwd: string, env: NodeJS.ProcessEnv @@ -61,7 +61,7 @@ export function runPinnedOpenspec( return new Promise((complete) => { execFile( process.execPath, - [PINNED_V12_BINS[version], ...args], + [PINNED_V14_BINS[version], ...args], { cwd, env, maxBuffer: 4 * 1024 * 1024, timeout: 30_000, windowsHide: true }, (error, stdout, stderr) => { complete({ @@ -78,11 +78,11 @@ export function runPinnedOpenspec( * Assert the executable line identity before any contract assertion uses it. * * This is the provenance guard: a bins-map entry accidentally pointing at another alias - * (for example openspec-cli-111) prints a different `--version` string and fails here, - * so a passing matrix always proves it ran the 1.12.0 line. + * (for example openspec-cli-113) prints a different `--version` string and fails here, + * so a passing matrix always proves it ran the 1.14.0 line. */ export async function expectPinnedVersion( - version: PinnedOpenspecV12Version, + version: PinnedOpenspecV14Version, cwd: string, env: NodeJS.ProcessEnv ): Promise { @@ -92,14 +92,13 @@ export async function expectPinnedVersion( } /** - * Assert the 1.12 JSON stream discipline on one `--json` run. + * Assert the 1.14 JSON stream discipline on one `--json` run. * - * The 1.12 line keeps the discipline proven for admitted lines since the eager-JSON fast + * The 1.14 line keeps the discipline proven for admitted lines since the eager-JSON fast * path: a JSON invocation must leave stdout as exactly one complete JSON document and * stderr free of telemetry/tip noise. This remains fixture evidence for the eager-JSON * fast path, which must keep refusing early exit while stderr carries real diagnostics; - * it does not license ignoring stderr generally. Human (non-JSON) output legitimately - * gained per-issue lines in 1.12; that text surface is out of scope for this assertion. + * it does not license ignoring stderr generally. */ export function expectPinnedJsonDiscipline(result: CliRunResult): void { // A JSON run prints exactly one document on stdout; `schemas --json` success is an diff --git a/packages/core/src/agent-command-content.test.ts b/packages/core/src/agent-command-content.test.ts index 7a847d25..43b93189 100644 --- a/packages/core/src/agent-command-content.test.ts +++ b/packages/core/src/agent-command-content.test.ts @@ -2,7 +2,7 @@ * Orthogonal intents (updated 2026-09-03 Asia/Shanghai): * 1. Prove the pinned OpenSpec CLI yields exact Agent command contents through the runtime generator bridge * (the pinned generator fixtures here are HISTORICAL/equivalence evidence — they run retired executable - * lines like openspec-cli-112/111 deliberately; positive v13 acceptance lives in official-cli-v13-*). + * lines like openspec-cli-112/111 deliberately; positive v14 acceptance lives in official-cli-v14-*). * 2. Prove non-importable runners fail closed instead of fabricating current command evidence. * 3. Prove a CLI line without one adapter keeps every unrelated adapter's evidence * (retired 1.11 lacks codeassistant; historical 1.8 lacks Command Code). diff --git a/packages/core/src/cli-contracts/workflow.test.ts b/packages/core/src/cli-contracts/workflow.test.ts index 5dd6b5b2..8dfb6574 100644 --- a/packages/core/src/cli-contracts/workflow.test.ts +++ b/packages/core/src/cli-contracts/workflow.test.ts @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Lock OpenSpec 1.8/1.9 Status artifact status, dependency arrays, and planning completion. * 2. Lock Apply and Archive operation-instruction payloads at the CLI contract boundary. * 3. Lock the `schemas --json` success/failure sum type and archived Validate report decoding. @@ -46,7 +46,10 @@ * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — change-list nested/warnings projection (update-openspec-cli-1131 Slice 2). * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — generic diagnostics decode-regression evidence (update-openspec-cli-1131 Slice 4). * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — Apply tracking-evidence decode contract (update-openspec-cli-1132 Slice 2). - * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — typed contracts Slice 2 (target-openspec-cli-114-line). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — typed contracts Slice 2 (target-openspec-cli-114-line); + * Slice 5 fixture-matrix lane hygiene: the typed-fact guard annotation becomes optional + * members (matching the optional schema members) and the version-command execute mock + * declares its argv parameter so the workflow-contract typecheck lane compiles. */ import { beforeEach, describe, expect, it, vi, type Mock, type MockInstance } from 'vitest' import { CliExecutor, type CliResult } from '../cli-executor.js' @@ -1432,8 +1435,8 @@ describe('OpenSpec 1.14 apply task source locations CLI contract', () => { expect(parsed.tasks).toHaveLength(2) // Typed-fact guard: source locations exist on the decoded task type, not merely - // as passthrough residue. - const first: { sourcePath: string | undefined; line: number | undefined } = parsed.tasks[0]! + // as passthrough residue (optional members — older payloads may lack them). + const first: { sourcePath?: string | undefined; line?: number | undefined } = parsed.tasks[0]! expect(first.sourcePath).toBe( '/private/tmp/os114-slice2/openspec/changes/no-specs-but-tasks/tasks.md' ) @@ -1692,7 +1695,7 @@ describe('OpenSpec 1.14 version CLI contract', () => { }) it('builds version argv without --check by default and with --check only when explicit', async () => { - const execute = vi.fn(async () => ({ + const execute = vi.fn(async (_args: string[]) => ({ success: true, stdout: JSON.stringify(executedVersion114), stderr: '', diff --git a/packages/core/src/official-cli-v12-boundary-fixtures.test.ts b/packages/core/src/official-cli-v12-boundary-fixtures.test.ts deleted file mode 100644 index b6a615c1..00000000 --- a/packages/core/src/official-cli-v12-boundary-fixtures.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): - * 1. Prove the executable identity of both pinned fixture lines: the v13 helper's - * 1.13.2 bin and the retained v12 helper's 1.12.0 boundary bin. - * 2. Record the apply-readiness projection boundary: the retained 1.12.0 executable - * answers `instructions apply --json` without the 1.13 `missingPrerequisites` - * build-order closure and without the ready-state `warnings` advisory, so the - * v13 projection contract cannot silently ride a below-admitted line. - * 3. Keep boundary negatives executable so the v13 single-series window cannot - * silently re-admit a line that lacks the apply-readiness evidence fields. - * - * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" - * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — in-window patch rotation moved the v13 - * boundary executable to 1.13.1 (version-identity arguments follow the pinned constant). - * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — in-window patch rotation moved the v13 - * boundary executable to 1.13.2 (version-identity arguments follow the pinned constant). - * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" - */ -import { mkdir, writeFile } from 'node:fs/promises' -import { join } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' -import { - expectPinnedVersion as expectPinnedV12Version, - pinnedFixtureEnv as pinnedV12FixtureEnv, - runPinnedOpenspec as runPinnedV12Openspec, -} from './__tests__/official-cli-v12-fixtures.js' -import { - createPinnedFixtureRoot, - parsePinnedSuccessJson as parsePinnedV13SuccessJson, - pinnedFixtureEnv as pinnedV13FixtureEnv, - removePinnedFixtureRoot, - runPinnedOpenspec as runPinnedV13Openspec, -} from './__tests__/official-cli-v13-fixtures.js' -import { CliApplyInstructionsSuccessSchema } from './cli-contracts/workflow.js' - -describe('pinned OpenSpec 1.13 boundary fixtures', () => { - let fixtureRoot: string | null = null - - afterEach(async () => { - await removePinnedFixtureRoot(fixtureRoot) - fixtureRoot = null - }) - - it('prints the exact line identity for both pinned executables', async () => { - fixtureRoot = await createPinnedFixtureRoot('cli-113-boundary-identity') - const project = join(fixtureRoot, 'project') - const v13Env = pinnedV13FixtureEnv(fixtureRoot) - const v12Env = pinnedV12FixtureEnv(fixtureRoot) - await mkdir(project, { recursive: true }) - - // The provenance guard: each bins-map entry must print its own exact version. - const v13 = await runPinnedV13Openspec('1.13.2', ['--version'], project, v13Env) - expect(v13.exitCode, v13.stdout + '\n' + v13.stderr).toBe(0) - expect(v13.stdout.trim()).toBe('1.13.2') - - await expectPinnedV12Version('1.12.0', project, v12Env) - }, 60_000) - - it('keeps the apply-readiness projection fields a 1.13-only capability on the pinned 1.12.0 executable', async () => { - fixtureRoot = await createPinnedFixtureRoot('cli-1120-apply-readiness-boundary') - const project = join(fixtureRoot, 'project') - const v13Env = pinnedV13FixtureEnv(fixtureRoot) - const v12Env = pinnedV12FixtureEnv(fixtureRoot) - await mkdir(project, { recursive: true }) - - // One fixture repo feeds both executables: the identical proposal+tasks change - // without delta specs is the 1.13 ready-with-warning scenario. - for (const env of [v13Env, v12Env]) { - const initialized = await runPinnedV13Openspec( - '1.13.2', - ['init', project, '--tools=none'], - project, - env - ) - expect(initialized.exitCode, initialized.stdout + '\n' + initialized.stderr).toBe(0) - } - const created = await runPinnedV13Openspec( - '1.13.2', - ['new', 'change', 'no-specs-but-tasks'], - project, - v13Env - ) - expect(created.exitCode, created.stdout + '\n' + created.stderr).toBe(0) - const changeDir = join(project, 'openspec', 'changes', 'no-specs-but-tasks') - await writeFile( - join(changeDir, 'proposal.md'), - '# Proposal\n\nExercise the ready-state no-delta-specs advisory.\n' - ) - await writeFile( - join(changeDir, 'tasks.md'), - ['# Tasks', '', '- [x] Finish the analysis', '- [ ] Implement it', ''].join('\n') - ) - - // The admitted 1.13.2 line projects the readiness evidence fields. - const v13Result = await runPinnedV13Openspec( - '1.13.2', - ['instructions', 'apply', '--change', 'no-specs-but-tasks', '--json'], - project, - v13Env - ) - const v13Apply = parsePinnedV13SuccessJson(v13Result, (payload) => - CliApplyInstructionsSuccessSchema.parse(payload) - ) - expect(v13Apply.state).toBe('ready') - expect(v13Apply.missingPrerequisites).toEqual(['specs', 'design']) - expect(v13Apply.warnings?.[0]).toContain('no delta specs') - - // The retained 1.12.0 executable answers the same command over the same change - // with the same verdict, but its document carries neither 1.13 evidence field: - // below-admitted lines must stay incapable of the v13 projection contract. - const v12Result = await runPinnedV12Openspec( - '1.12.0', - ['instructions', 'apply', '--change', 'no-specs-but-tasks', '--json'], - project, - v12Env - ) - const v12Apply = parsePinnedV13SuccessJson(v12Result, (payload) => - CliApplyInstructionsSuccessSchema.parse(payload) - ) - expect(v12Apply.state).toBe('ready') - expect(v12Apply.progress).toEqual(v13Apply.progress) - expect(v12Apply.missingPrerequisites).toBeUndefined() - expect(v12Apply.warnings).toBeUndefined() - }, 60_000) -}) diff --git a/packages/core/src/official-cli-v13-boundary-fixtures.test.ts b/packages/core/src/official-cli-v13-boundary-fixtures.test.ts new file mode 100644 index 00000000..b47451dd --- /dev/null +++ b/packages/core/src/official-cli-v13-boundary-fixtures.test.ts @@ -0,0 +1,226 @@ +/** + * Orthogonal intents (created 2026-10-02 Asia/Shanghai): + * 1. Prove the executable identity of the retired boundary line: the retained + * `openspec-cli-113` npm alias (1.13.2, the line the v14 gate revoked) and the + * admitted v14 helper's 1.14.0 bin. + * 2. Prove the production v14 gate classifies 1.13.2 as `unsupported` (the v13 window + * is below-admitted), so the single-series window cannot silently re-admit it. + * 3. Record the 1.14-only protocol members as absent on the 1.13.2 executable over + * the same fixture repo the admitted line reads: apply tasks carry no + * `sourcePath`/`line`, `list --archived` is rejected, show-Spec requirements and + * scenarios carry no `name`, and status emits no top-level `warnings`. + * 4. Never assert v13-era members (for example `taskTrackingConfigured`) absent — + * those are 1.13.2's own facts; the boundary proves only what 1.14 added. + * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + */ +import { execFile } from 'node:child_process' +import { mkdir, writeFile } from 'node:fs/promises' +import { join, resolve } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + createPinnedFixtureRoot, + expectPinnedVersion as expectPinnedV14Version, + parsePinnedSuccessJson as parsePinnedV14SuccessJson, + pinnedFixtureEnv, + removePinnedFixtureRoot, + runPinnedOpenspec as runPinnedV14Openspec, +} from './__tests__/official-cli-v14-fixtures.js' +import { classifyOpenSpecCliVersion } from './openspec-compat.js' +import { + CliApplyInstructionsSuccessSchema, + CliShowSpecDocumentSchema, + CliWorkflowStatusSuccessSchema, +} from './cli-contracts/workflow.js' + +/** The retired below-admitted boundary executable kept through the openspec-cli-113 alias. */ +const RETIRED_V13_BIN = resolve( + import.meta.dirname, + '../node_modules/openspec-cli-113/bin/openspec.js' +) + +interface BoundaryRunResult { + exitCode: number + stdout: string + stderr: string +} + +function runRetiredV13Openspec( + args: readonly string[], + cwd: string, + env: NodeJS.ProcessEnv +): Promise { + return new Promise((complete) => { + execFile( + process.execPath, + [RETIRED_V13_BIN, ...args], + { cwd, env, maxBuffer: 4 * 1024 * 1024, timeout: 30_000, windowsHide: true }, + (error, stdout, stderr) => { + complete({ + exitCode: typeof error?.code === 'number' ? error.code : error ? 1 : 0, + stdout, + stderr, + }) + } + ) + }) +} + +describe('pinned OpenSpec 1.13 boundary fixtures', () => { + let fixtureRoot: string | null = null + + afterEach(async () => { + await removePinnedFixtureRoot(fixtureRoot) + fixtureRoot = null + }) + + it('prints the exact line identity for both pinned executables', async () => { + fixtureRoot = await createPinnedFixtureRoot('cli-113-boundary-identity') + const project = join(fixtureRoot, 'project') + const env = pinnedFixtureEnv(fixtureRoot) + await mkdir(project, { recursive: true }) + + // The provenance guard for the retired line: the alias bin must print its own + // exact 1.13.2 identity, never the admitted 1.14.0 string. + const retired = await runRetiredV13Openspec(['--version'], project, env) + expect(retired.exitCode, retired.stdout + '\n' + retired.stderr).toBe(0) + expect(retired.stdout.trim()).toBe('1.13.2') + + await expectPinnedV14Version('1.14.0', project, env) + }, 60_000) + + it('classifies the retired 1.13.2 line as unsupported under the v14 gate', () => { + const compatibility = classifyOpenSpecCliVersion('1.13.2') + expect(compatibility.status).toBe('unsupported') + expect(compatibility.supported).toBe(false) + expect(compatibility.recommended).toBe(false) + }) + + it('keeps the 1.14-only members absent on the retired 1.13.2 executable', async () => { + fixtureRoot = await createPinnedFixtureRoot('cli-1132-boundary-members') + const project = join(fixtureRoot, 'project') + const env = pinnedFixtureEnv(fixtureRoot) + await mkdir(project, { recursive: true }) + + // One fixture repo feeds both executables; the admitted line initializes it. + const initialized = await runPinnedV14Openspec( + '1.14.0', + ['init', project, '--tools=none'], + project, + env + ) + expect(initialized.exitCode, initialized.stdout + '\n' + initialized.stderr).toBe(0) + + // A ready change with two checkbox lines: 1.14 attaches the LocatedTask + // members, the retired line must answer without them. + const created = await runPinnedV14Openspec( + '1.14.0', + ['new', 'change', 'located-tasks'], + project, + env + ) + expect(created.exitCode, created.stdout + created.stderr).toBe(0) + const changeDir = join(project, 'openspec', 'changes', 'located-tasks') + await writeFile(join(changeDir, 'proposal.md'), '# Proposal\n\nBoundary member matrix.\n') + await writeFile( + join(changeDir, 'tasks.md'), + ['# Tasks', '', '- [x] Finish the analysis', '- [ ] Implement it', ''].join('\n') + ) + // An unrecognized .openspec.yaml key: 1.14 turns it into a top-level warning, + // the retired line ignores it silently. + await writeFile(join(changeDir, '.openspec.yaml'), 'schema: spec-driven\nskip_design: true\n') + + // A main spec: 1.14 shows requirement/scenario names, the retired line does not. + await mkdir(join(project, 'openspec', 'specs', 'billing'), { recursive: true }) + await writeFile( + join(project, 'openspec', 'specs', 'billing', 'spec.md'), + [ + '## Purpose', + 'Billing rules.', + '', + '## Requirements', + '', + '### Requirement: Billing rules', + 'The system SHALL apply billing rules.', + '', + '#### Scenario: Existing behavior', + '- **WHEN** billing runs', + '- **THEN** billing succeeds', + '', + ].join('\n') + ) + + // Apply: same verdict and progress, but no task carries sourcePath or line. + const applyRetired = await runRetiredV13Openspec( + ['instructions', 'apply', '--change', 'located-tasks', '--json'], + project, + env + ) + expect(applyRetired.exitCode, applyRetired.stdout + '\n' + applyRetired.stderr).toBe(0) + const apply = parsePinnedV14SuccessJson(applyRetired, (payload) => + CliApplyInstructionsSuccessSchema.parse(payload) + ) + expect(apply.state).toBe('ready') + expect(apply.progress).toEqual({ total: 2, complete: 1, remaining: 1 }) + expect(apply.tasks.map((task) => task.description)).toEqual([ + 'Finish the analysis', + 'Implement it', + ]) + for (const task of apply.tasks) { + expect('sourcePath' in task, JSON.stringify(task)).toBe(false) + expect('line' in task, JSON.stringify(task)).toBe(false) + expect(task.sourcePath).toBeUndefined() + expect(task.line).toBeUndefined() + } + + // Status: the unrecognized key produces no top-level warnings member. + const statusRetired = await runRetiredV13Openspec( + ['status', '--change', 'located-tasks', '--json'], + project, + env + ) + expect(statusRetired.exitCode, statusRetired.stdout + '\n' + statusRetired.stderr).toBe(0) + const statusPayload = JSON.parse(statusRetired.stdout) + expect('warnings' in statusPayload).toBe(false) + const status = parsePinnedV14SuccessJson(statusRetired, (payload) => + CliWorkflowStatusSuccessSchema.parse(payload) + ) + expect(status.warnings).toBeUndefined() + expect(status.changeName).toBe('located-tasks') + + // Show spec: requirement and scenario bodies survive without the name members. + const showRetired = await runRetiredV13Openspec( + ['show', 'billing', '--type', 'spec', '--json'], + project, + env + ) + expect(showRetired.exitCode, showRetired.stdout + showRetired.stderr).toBe(0) + const shown = parsePinnedV14SuccessJson(showRetired, (payload) => + CliShowSpecDocumentSchema.parse(payload) + ) + expect(shown).toMatchObject({ + id: 'billing', + requirementCount: 1, + requirements: [ + { + text: 'The system SHALL apply billing rules.', + scenarios: [{ rawText: '- **WHEN** billing runs\n- **THEN** billing succeeds' }], + }, + ], + }) + for (const requirement of shown.requirements) { + expect('name' in requirement, JSON.stringify(requirement)).toBe(false) + for (const scenario of requirement.scenarios) { + expect('name' in scenario, JSON.stringify(scenario)).toBe(false) + } + } + + // List: the archive inventory flags are not merely absent — the whole option + // is unknown to the retired line and the invocation fails without JSON output. + const listRetired = await runRetiredV13Openspec(['list', '--archived', '--json'], project, env) + expect(listRetired.exitCode).toBe(1) + expect(listRetired.stderr).toContain("unknown option '--archived'") + // No JSON document shares the failed invocation's stdout. + expect(listRetired.stdout.trim()).toBe('') + }, 60_000) +}) diff --git a/packages/core/src/official-cli-v13-default-store-fixtures.test.ts b/packages/core/src/official-cli-v13-default-store-fixtures.test.ts deleted file mode 100644 index d9e1bb1c..00000000 --- a/packages/core/src/official-cli-v13-default-store-fixtures.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Orthogonal intents (created 2026-09-12 Asia/Shanghai): - * 1. Execute the pinned OpenSpec 1.13.1 CLI against machine `defaultStore` root - * selection. - * 2. Prove effective, absent, and stale fallback outcomes preserve upstream provenance - * and fixes on the v13 single-series window. - * 3. Carry over the admitted-line defaultStore contract proven for 1.10-1.12; hide - * fixture subprocess console windows (`windowsHide`) for uniform hidden-console - * execution on Windows. - * - * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" - * Original request (2026-08-14): "在Windows平台上,执行命令总是会弹出cmd窗口,这个可否统一隐藏,你先调查一下原因" - * Original request (2026-08-01): adapt OpenSpec 1.7 machine `defaultStore` without fabricating effective Root truth. - * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11" - */ -import { mkdir, realpath, writeFile } from 'node:fs/promises' -import { join } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' -import { - PINNED_OPENSPEC_V13_VERSIONS, - createPinnedFixtureRoot, - expectPinnedJsonDiscipline, - parsePinnedJson, - pinnedFixtureEnv, - removePinnedFixtureRoot, - runPinnedOpenspec, -} from './__tests__/official-cli-v13-fixtures.js' -import { CliDiagnosticFailureSchema } from './cli-contracts/common.js' -import { CliContextSchema, CliDoctorSchema } from './cli-contracts/store.js' - -describe('pinned OpenSpec 1.13 defaultStore fixtures', () => { - let fixtureRoot: string | null = null - - afterEach(async () => { - await removePinnedFixtureRoot(fixtureRoot) - fixtureRoot = null - }) - - for (const version of PINNED_OPENSPEC_V13_VERSIONS) { - it(`reports global_default only when the configured Store is selected effectively on OpenSpec ${version}`, async () => { - fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-default-store`) - const storeRoot = join(fixtureRoot, 'team-context') - const scratch = join(fixtureRoot, 'scratch') - const env = pinnedFixtureEnv(fixtureRoot) - await mkdir(join(storeRoot, 'openspec', 'specs'), { recursive: true }) - await mkdir(join(storeRoot, 'openspec', 'changes', 'archive'), { recursive: true }) - await mkdir(scratch, { recursive: true }) - await writeFile(join(storeRoot, 'openspec', 'config.yaml'), 'schema: spec-driven\n') - const physicalStoreRoot = await realpath(storeRoot) - - const registered = await runPinnedOpenspec( - version, - ['store', 'register', storeRoot, '--id', 'team-context', '--yes', '--json'], - scratch, - env - ) - expect(registered.exitCode, registered.stdout + registered.stderr).toBe(0) - const configured = await runPinnedOpenspec( - version, - ['config', 'set', 'defaultStore', 'team-context', '--string'], - scratch, - env - ) - expect(configured.exitCode, configured.stdout + configured.stderr).toBe(0) - - const doctorResult = await runPinnedOpenspec(version, ['doctor', '--json'], scratch, env) - expect(doctorResult.exitCode, doctorResult.stdout + doctorResult.stderr).toBe(0) - expectPinnedJsonDiscipline(doctorResult) - expect( - parsePinnedJson(doctorResult, (payload) => CliDoctorSchema.parse(payload)).root - ).toMatchObject({ - path: physicalStoreRoot, - source: 'global_default', - store_id: 'team-context', - }) - - const contextResult = await runPinnedOpenspec(version, ['context', '--json'], scratch, env) - expect(contextResult.exitCode, contextResult.stdout + contextResult.stderr).toBe(0) - expect( - parsePinnedJson(contextResult, (payload) => CliContextSchema.parse(payload)).root - ).toMatchObject({ - path: physicalStoreRoot, - source: 'global_default', - store_id: 'team-context', - }) - }, 60_000) - - it(`keeps absent and stale fallback failures distinct on OpenSpec ${version}`, async () => { - fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-default-store`) - const scratch = join(fixtureRoot, 'scratch') - const env = pinnedFixtureEnv(fixtureRoot) - await mkdir(scratch, { recursive: true }) - - const absent = await runPinnedOpenspec(version, ['doctor', '--json'], scratch, env) - expect(absent.exitCode).toBe(1) - expect( - parsePinnedJson(absent, (payload) => CliDiagnosticFailureSchema.parse(payload)).status[0] - ?.code - ).toBe('no_openspec_root') - - const configured = await runPinnedOpenspec( - version, - ['config', 'set', 'defaultStore', 'ghost-plans', '--string'], - scratch, - env - ) - expect(configured.exitCode, configured.stdout + configured.stderr).toBe(0) - const stale = await runPinnedOpenspec(version, ['doctor', '--json'], scratch, env) - expect(stale.exitCode).toBe(1) - expect( - parsePinnedJson(stale, (payload) => CliDiagnosticFailureSchema.parse(payload)).status[0] - ).toMatchObject({ - code: 'no_registered_stores', - message: expect.stringContaining("Global defaultStore 'ghost-plans'"), - fix: expect.stringContaining('openspec config unset defaultStore'), - }) - }, 60_000) - } -}) diff --git a/packages/core/src/official-cli-v13-agent-delivery-fixtures.test.ts b/packages/core/src/official-cli-v14-agent-delivery-fixtures.test.ts similarity index 89% rename from packages/core/src/official-cli-v13-agent-delivery-fixtures.test.ts rename to packages/core/src/official-cli-v14-agent-delivery-fixtures.test.ts index bc1d002f..fb1998a6 100644 --- a/packages/core/src/official-cli-v13-agent-delivery-fixtures.test.ts +++ b/packages/core/src/official-cli-v14-agent-delivery-fixtures.test.ts @@ -1,6 +1,6 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): - * 1. Execute the pinned OpenSpec 1.13 SourceCraft Code Assistant delivery contract + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Execute the pinned OpenSpec 1.14 SourceCraft Code Assistant delivery contract * against an isolated machine environment: 6 skills + 6 commands under the default * core profile with the physical `.codeassistant` layout. * 2. Prove init anchors empty directories with `.gitkeep`, restores missing anchors on @@ -10,17 +10,15 @@ * its content (upstream `ensureDirectoryAnchor` writes with the `wx` flag and * tolerates EEXIST; this suite asserts that observable behavior only). * 4. Prove the shared IDE restart hint wording: qoder prints it, codeassistant does not. - * 5. (2026-09-26, update-openspec-cli-1132 Slice 4) Prove the 1.13.2 Kilo Code command - * path rotation on the executed executable: commands deliver as plain markdown - * (no frontmatter) under `.kilo/command/opsx-.md` while skills stay in - * `.kilocode/skills/`, the restart hint prints, and no legacy `.kilocode/workflows/` - * directory is created, per `references/openspec-1.13.2-report.md` P2. Legacy - * two-generation cleanup stays projection evidence (`agent-delivery-registry` / - * `tool-init-state` tests); this harness exercises fresh init only. + * 5. Prove the Kilo Code command path on the executed executable: commands deliver as + * plain markdown (no frontmatter) under `.kilo/command/opsx-.md` while skills + * stay in `.kilocode/skills/`, the restart hint prints, and no legacy + * `.kilocode/workflows/` directory is created. Legacy two-generation cleanup stays + * projection evidence (`agent-delivery-registry` / `tool-init-state` tests); this + * harness exercises fresh init only. The 1.14 registry grows to 50 tools without + * changing these per-tool physical layouts. * - * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" - * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — in-window patch rotation 1.13.1 -> 1.13.2, - * Kilo command-path executable matrix (update-openspec-cli-1132 Slice 4). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { access, lstat, mkdir, readdir, readFile, rm, symlink, writeFile } from 'node:fs/promises' import { join } from 'node:path' @@ -28,11 +26,11 @@ import { afterEach, describe, expect, it } from 'vitest' import { createPinnedFixtureRoot, expectPinnedVersion, - PINNED_OPENSPEC_V13_VERSIONS, + PINNED_OPENSPEC_V14_VERSIONS, pinnedFixtureEnv, removePinnedFixtureRoot, runPinnedOpenspec, -} from './__tests__/official-cli-v13-fixtures.js' +} from './__tests__/official-cli-v14-fixtures.js' /** The default core profile selects exactly these six workflows. */ const CORE_PROFILE_SKILLS = [ @@ -62,7 +60,7 @@ async function pathExists(path: string): Promise { } } -describe('pinned OpenSpec 1.13 Agent delivery fixtures', () => { +describe('pinned OpenSpec 1.14 Agent delivery fixtures', () => { let fixtureRoot: string | null = null afterEach(async () => { @@ -70,7 +68,7 @@ describe('pinned OpenSpec 1.13 Agent delivery fixtures', () => { fixtureRoot = null }) - for (const version of PINNED_OPENSPEC_V13_VERSIONS) { + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { it(`delivers six skills and six commands to .codeassistant under the default core profile on OpenSpec ${version}`, async () => { fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-codeassistant`) const project = join(fixtureRoot, 'project') diff --git a/packages/core/src/official-cli-v13-apply-readiness-fixtures.test.ts b/packages/core/src/official-cli-v14-apply-readiness-fixtures.test.ts similarity index 88% rename from packages/core/src/official-cli-v13-apply-readiness-fixtures.test.ts rename to packages/core/src/official-cli-v14-apply-readiness-fixtures.test.ts index f9bf6114..9be9e9f0 100644 --- a/packages/core/src/official-cli-v13-apply-readiness-fixtures.test.ts +++ b/packages/core/src/official-cli-v14-apply-readiness-fixtures.test.ts @@ -1,6 +1,6 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): - * 1. Execute the pinned OpenSpec 1.13 Apply readiness contract (`instructions apply + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Execute the pinned OpenSpec 1.14 Apply readiness contract (`instructions apply * --json`) against a real fixture repo: the blocked-state build-order closure and * the ready-state no-delta-specs advisory. * 2. Prove `missingPrerequisites` is additive readiness evidence, never a new gate: @@ -10,32 +10,35 @@ * 3. Prove the ready-state `warnings` advisory names the objective downstream fact * (`openspec validate` fails on the change) plus both remedies (write the delta * specs / declare `skip_specs: true`), matching the Verified CLI observations in - * `references/openspec-1.13.1-report.md`. + * `references/openspec-1.14.0-report.md`. * 4. Prove upstream conditional spreading: `warnings` is absent (not `[]`) while * blocked, and `missingArtifacts` is absent while ready. - * 5. (2026-09-26, update-openspec-cli-1132 Slice 4) Prove the 1.13.2 tracking-evidence - * members on the executed executable: every standard-schema payload emits - * `taskTrackingConfigured: true` with `unavailableTrackingFiles` absent. - * 6. (2026-09-26) Prove glob-tracked aggregation through a project-local custom schema + * 5. Prove the 1.13.2 tracking-evidence members carried forward on the 1.14 executable: + * every standard-schema payload emits `taskTrackingConfigured: true` with + * `unavailableTrackingFiles` absent. + * 6. Prove glob-tracked aggregation through a project-local custom schema * (`*`-class `apply.tracks` over two files): `tasks`/`progress` aggregate across every * matched file; zero matches keep `taskTrackingConfigured: true` with empty `tasks` * (configured-but-unmatched is not no-tracking) and the tracking-file-missing branch. - * 7. (2026-09-26) Prove matched-but-unreadable tracking files (POSIX chmod 000, non-root - * only) surface as `unavailableTrackingFiles` absolute-path + reason evidence, the + * 7. Prove matched-but-unreadable tracking files (POSIX chmod 000, non-root only) + * surface as `unavailableTrackingFiles` absolute-path + reason evidence, the * readable file still contributes tasks, `state` stays `ready` while readable tasks * remain (with the not-verified instruction suffix) and blocks with * "No readable task descriptions are available." when none remain — evidence only, * never a synthesized gate, per `references/openspec-1.13.2-report.md` P1. + * 8. (2026-10-02, target-openspec-cli-114-line Slice 5) Prove the 1.14 LocatedTask + * members on the executed executable: every task carries an absolute `sourcePath` + * plus its 1-based `line`, including per-file locations across a glob-tracked + * aggregate — verbatim CLI evidence OpenSpecUI never re-derives, per + * `references/openspec-1.14.0-report.md` section 2. * - * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" - * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — in-window patch rotation 1.13.1 -> 1.13.2, - * tracking-evidence executable matrix (update-openspec-cli-1132 Slice 4). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { chmod, mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { - PINNED_OPENSPEC_V13_VERSIONS, + PINNED_OPENSPEC_V14_VERSIONS, createPinnedFixtureRoot, expectPinnedJsonDiscipline, expectPinnedVersion, @@ -43,12 +46,12 @@ import { pinnedFixtureEnv, removePinnedFixtureRoot, runPinnedOpenspec, - type PinnedOpenspecV13Version, -} from './__tests__/official-cli-v13-fixtures.js' + type PinnedOpenspecV14Version, +} from './__tests__/official-cli-v14-fixtures.js' import { CliApplyInstructionsSuccessSchema } from './cli-contracts/workflow.js' async function initProject( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv ): Promise { @@ -62,7 +65,7 @@ async function initProject( } async function newChange( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv, changeId: string @@ -140,7 +143,7 @@ const RUNS_UNREADABLE_TRACKING_FIXTURE = typeof process.getuid === 'function' && process.getuid() !== 0 -describe('pinned OpenSpec 1.13 apply readiness fixtures', () => { +describe('pinned OpenSpec 1.14 apply readiness fixtures', () => { let fixtureRoot: string | null = null afterEach(async () => { @@ -148,7 +151,7 @@ describe('pinned OpenSpec 1.13 apply readiness fixtures', () => { fixtureRoot = null }) - for (const version of PINNED_OPENSPEC_V13_VERSIONS) { + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { it(`names the whole prerequisite chain on the blocked state and stays warning-free on OpenSpec ${version}`, async () => { fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-apply-blocked`) const project = join(fixtureRoot, 'project') @@ -235,6 +238,15 @@ describe('pinned OpenSpec 1.13 apply readiness fixtures', () => { // unavailable member stays absent (never `[]`). expect(apply.taskTrackingConfigured).toBe(true) expect(apply.unavailableTrackingFiles).toBeUndefined() + + // 1.14 LocatedTask evidence: every task names the absolute file its checkbox + // lives in plus the 1-based line (tasks.md lines 3 and 4). The payload's + // changeDir is the canonical absolute base the CLI itself resolved. + const tasksPath = join(apply.changeDir, 'tasks.md') + expect(apply.tasks.map((task) => [task.sourcePath, task.line])).toEqual([ + [tasksPath, 3], + [tasksPath, 4], + ]) }, 60_000) it(`aggregates tasks across every file matched by a wildcard apply.tracks on OpenSpec ${version}`, async () => { @@ -286,6 +298,16 @@ describe('pinned OpenSpec 1.13 apply readiness fixtures', () => { 'beta open two', 'beta open three', ]) + // 1.14 LocatedTask evidence across the aggregate: each task carries the + // absolute path of its own matched file plus its 1-based line inside it + // (alpha lines 3-4, then beta lines 3-5). + expect(apply.tasks.map((task) => [task.sourcePath, task.line])).toEqual([ + [join(apply.changeDir, 'docs', 'tasks-alpha.md'), 3], + [join(apply.changeDir, 'docs', 'tasks-alpha.md'), 4], + [join(apply.changeDir, 'docs', 'tasks-beta.md'), 3], + [join(apply.changeDir, 'docs', 'tasks-beta.md'), 4], + [join(apply.changeDir, 'docs', 'tasks-beta.md'), 5], + ]) expect(apply.progress).toEqual({ total: 5, complete: 2, remaining: 3 }) expect(apply.state).toBe('ready') // Every matched file was readable: absent, never an empty array. diff --git a/packages/core/src/official-cli-v13-batch-status-fixtures.test.ts b/packages/core/src/official-cli-v14-batch-status-fixtures.test.ts similarity index 90% rename from packages/core/src/official-cli-v13-batch-status-fixtures.test.ts rename to packages/core/src/official-cli-v14-batch-status-fixtures.test.ts index 02adba82..48790846 100644 --- a/packages/core/src/official-cli-v13-batch-status-fixtures.test.ts +++ b/packages/core/src/official-cli-v14-batch-status-fixtures.test.ts @@ -1,13 +1,14 @@ /** - * Orthogonal intents (created 2026-09-12 Asia/Shanghai): - * 1. Execute the pinned OpenSpec 1.13.1 `status --all --json` batch envelope against a + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Execute the pinned OpenSpec 1.14.0 `status --all --json` batch envelope against a * real fixture project: healthy-entry field parity, in-place failure entries, and * the empty-set message shape. * 2. Prove partial failure keeps stdout one complete valid JSON document while the * process exits 1 (decoding must never consult the exit code). - * 3. Carry over the admitted-line batch status contract proven for 1.11-1.12 onto the - * v13 single-series window; the retired 1.12.0 executable stays in the boundary suite. + * 3. Carry over the admitted-line batch status contract proven for 1.11-1.13 onto the + * v14 single-series window; the retired 1.13.2 executable stays in the boundary suite. * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11" */ @@ -15,7 +16,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { - PINNED_OPENSPEC_V13_VERSIONS, + PINNED_OPENSPEC_V14_VERSIONS, createPinnedFixtureRoot, expectPinnedJsonDiscipline, expectPinnedVersion, @@ -24,8 +25,8 @@ import { pinnedFixtureEnv, removePinnedFixtureRoot, runPinnedOpenspec, - type PinnedOpenspecV13Version, -} from './__tests__/official-cli-v13-fixtures.js' + type PinnedOpenspecV14Version, +} from './__tests__/official-cli-v14-fixtures.js' import { CliBatchStatusSchema, isCliBatchStatusEntryFailure, @@ -34,7 +35,7 @@ import { import { CliWorkflowStatusSuccessSchema } from './cli-contracts/workflow.js' async function initProject( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv ): Promise { @@ -47,7 +48,7 @@ async function initProject( expect(initialized.exitCode, initialized.stdout + '\n' + initialized.stderr).toBe(0) } -describe('pinned OpenSpec 1.13 batch status fixtures', () => { +describe('pinned OpenSpec 1.14 batch status fixtures', () => { let fixtureRoot: string | null = null afterEach(async () => { @@ -55,7 +56,7 @@ describe('pinned OpenSpec 1.13 batch status fixtures', () => { fixtureRoot = null }) - for (const version of PINNED_OPENSPEC_V13_VERSIONS) { + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { it(`returns healthy entries with single-change Status fields and one envelope root on OpenSpec ${version}`, async () => { fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-batch-healthy`) const project = join(fixtureRoot, 'project') diff --git a/packages/core/src/official-cli-v14-change-list-inventory-fixtures.test.ts b/packages/core/src/official-cli-v14-change-list-inventory-fixtures.test.ts new file mode 100644 index 00000000..6ab353ce --- /dev/null +++ b/packages/core/src/official-cli-v14-change-list-inventory-fixtures.test.ts @@ -0,0 +1,129 @@ +/** + * Orthogonal intents (created 2026-10-02 Asia/Shanghai): + * 1. Execute the pinned OpenSpec 1.14.0 change-list archive inventory (`list --archived` + * and `list --all`, new in 1.14) against a real fixture project holding one active + * and one archived change. + * 2. Prove the per-entry `archived` flag discipline: `--archived` returns only archive + * inventory with `archived: true`; `--all` mixes both with explicit `false` beside + * the active entries; the default `list --json` never emits the member, so absence + * carries no information and is never synthesized. + * 3. Prove the archived entry keeps the ordinary per-entry shape (task counts, status) + * over the date-prefixed archive name, per `references/openspec-1.14.0-report.md` + * section 3 — CLI evidence only; the Archive list projection owner stays the + * reactive-filesystem adapter this line. + * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + */ +import { mkdir, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + PINNED_OPENSPEC_V14_VERSIONS, + createPinnedFixtureRoot, + expectPinnedJsonDiscipline, + expectPinnedVersion, + parsePinnedSuccessJson, + pinnedFixtureEnv, + removePinnedFixtureRoot, + runPinnedOpenspec, +} from './__tests__/official-cli-v14-fixtures.js' +import { CliChangeListSchema } from './cli-contracts/workflow.js' + +describe('pinned OpenSpec 1.14 change list inventory fixtures', () => { + let fixtureRoot: string | null = null + + afterEach(async () => { + await removePinnedFixtureRoot(fixtureRoot) + fixtureRoot = null + }) + + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { + it(`marks archived inventory and keeps the default list flag-free on OpenSpec ${version}`, async () => { + fixtureRoot = await createPinnedFixtureRoot( + `cli-${version.replace(/\./g, '')}-list-inventory` + ) + const project = join(fixtureRoot, 'project') + const env = pinnedFixtureEnv(fixtureRoot) + await mkdir(project, { recursive: true }) + + await expectPinnedVersion(version, project, env) + const initialized = await runPinnedOpenspec( + version, + ['init', project, '--tools=none'], + project, + env + ) + expect(initialized.exitCode, initialized.stdout + '\n' + initialized.stderr).toBe(0) + + // One change with one done and one open task so the archived row keeps a + // meaningful in-progress status; archive without validate so the open task + // does not block the move. + const created = await runPinnedOpenspec( + version, + ['new', 'change', 'shipped-work'], + project, + env + ) + expect(created.exitCode, created.stdout + created.stderr).toBe(0) + const changeDir = join(project, 'openspec', 'changes', 'shipped-work') + await writeFile(join(changeDir, 'proposal.md'), '# Proposal\n\nShipped work.\n') + await writeFile( + join(changeDir, 'tasks.md'), + ['# Tasks', '', '- [x] Finish the work', '- [ ] Follow-up later', ''].join('\n') + ) + const archived = await runPinnedOpenspec( + version, + ['archive', 'shipped-work', '--json', '--yes', '--no-validate'], + project, + env + ) + expect(archived.exitCode, archived.stdout + archived.stderr).toBe(0) + + // A second change stays active beside the archive. + const activeDir = join(project, 'openspec', 'changes', 'active-work') + await mkdir(activeDir, { recursive: true }) + await writeFile(join(activeDir, '.openspec.yaml'), 'schema: spec-driven\n') + await writeFile(join(activeDir, 'proposal.md'), '# Proposal\n\nActive work.\n') + + // The archive inventory names the archived entry only, flag true, with the + // date-prefixed archive name and the ordinary task/status shape intact. + const archivedList = parsePinnedSuccessJson( + await runPinnedOpenspec(version, ['list', '--archived', '--json'], project, env), + (payload) => CliChangeListSchema.parse(payload) + ) + expect(archivedList.changes).toHaveLength(1) + const archivedEntry = archivedList.changes[0] + expect(archivedEntry?.archived).toBe(true) + expect(archivedEntry?.name.endsWith('shipped-work')).toBe(true) + expect(archivedEntry?.completedTasks).toBe(1) + expect(archivedEntry?.totalTasks).toBe(2) + expect(archivedEntry?.status).toBe('in-progress') + + // --all mixes both sides of the inventory with an explicit flag per entry. + const allList = parsePinnedSuccessJson( + await runPinnedOpenspec(version, ['list', '--all', '--json'], project, env), + (payload) => CliChangeListSchema.parse(payload) + ) + expect(allList.changes).toHaveLength(2) + const activeInAll = allList.changes.find((entry) => entry.name === 'active-work') + expect(activeInAll?.archived).toBe(false) + const archivedInAll = allList.changes.find((entry) => entry.archived === true) + expect(archivedInAll?.name).toBe(archivedEntry?.name) + expect(archivedInAll?.completedTasks).toBe(1) + expect(archivedInAll?.totalTasks).toBe(2) + + // The default list keeps its pre-1.14 shape: active entries only and the + // archived member absent (never null, never false). + const defaultListResult = await runPinnedOpenspec(version, ['list', '--json'], project, env) + expectPinnedJsonDiscipline(defaultListResult) + const defaultList = parsePinnedSuccessJson(defaultListResult, (payload) => + CliChangeListSchema.parse(payload) + ) + expect(defaultList.changes.map((entry) => entry.name)).toEqual(['active-work']) + for (const entry of defaultList.changes) { + expect('archived' in entry).toBe(false) + expect(entry.archived).toBeUndefined() + } + }, 60_000) + } +}) diff --git a/packages/core/src/official-cli-v14-default-store-fixtures.test.ts b/packages/core/src/official-cli-v14-default-store-fixtures.test.ts new file mode 100644 index 00000000..ed21ab5a --- /dev/null +++ b/packages/core/src/official-cli-v14-default-store-fixtures.test.ts @@ -0,0 +1,206 @@ +/** + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Execute the pinned OpenSpec 1.14.0 CLI against machine `defaultStore` root + * selection. + * 2. Prove effective, absent, and stale fallback outcomes preserve upstream provenance + * and fixes on the v14 single-series window. + * 3. Carry over the admitted-line defaultStore contract proven for 1.10-1.13; hide + * fixture subprocess console windows (`windowsHide`) for uniform hidden-console + * execution on Windows. + * 4. (2026-10-02, target-openspec-cli-114-line Slice 5) Prove the 1.14 store-backed + * `status` edit-roots dual branch (upstream #2013 `findDeclaringProjectRoot`): with + * the declaring project on the current path, `allowedEditRoots` is + * `[implementationRoot, projectRoot]` with the declaring-repo constraint; without + * one, `[projectRoot]` with the ask-user constraint; the non-store repo-local + * contrast keeps its single-root constraint. Same array shape, changed semantics — + * per `references/openspec-1.14.0-report.md` section 6. + * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-08-14): "在Windows平台上,执行命令总是会弹出cmd窗口,这个可否统一隐藏,你先调查一下原因" + * Original request (2026-08-01): adapt OpenSpec 1.7 machine `defaultStore` without fabricating effective Root truth. + * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11" + */ +import { mkdir, realpath, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + PINNED_OPENSPEC_V14_VERSIONS, + createPinnedFixtureRoot, + expectPinnedJsonDiscipline, + parsePinnedJson, + parsePinnedSuccessJson, + pinnedFixtureEnv, + removePinnedFixtureRoot, + runPinnedOpenspec, +} from './__tests__/official-cli-v14-fixtures.js' +import { CliDiagnosticFailureSchema } from './cli-contracts/common.js' +import { CliContextSchema, CliDoctorSchema } from './cli-contracts/store.js' +import { CliWorkflowStatusSuccessSchema } from './cli-contracts/workflow.js' + +describe('pinned OpenSpec 1.14 defaultStore fixtures', () => { + let fixtureRoot: string | null = null + + afterEach(async () => { + await removePinnedFixtureRoot(fixtureRoot) + fixtureRoot = null + }) + + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { + it(`reports global_default only when the configured Store is selected effectively on OpenSpec ${version}`, async () => { + fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-default-store`) + const storeRoot = join(fixtureRoot, 'team-context') + const scratch = join(fixtureRoot, 'scratch') + const env = pinnedFixtureEnv(fixtureRoot) + await mkdir(join(storeRoot, 'openspec', 'specs'), { recursive: true }) + await mkdir(join(storeRoot, 'openspec', 'changes', 'archive'), { recursive: true }) + await mkdir(scratch, { recursive: true }) + await writeFile(join(storeRoot, 'openspec', 'config.yaml'), 'schema: spec-driven\n') + const physicalStoreRoot = await realpath(storeRoot) + + const registered = await runPinnedOpenspec( + version, + ['store', 'register', storeRoot, '--id', 'team-context', '--yes', '--json'], + scratch, + env + ) + expect(registered.exitCode, registered.stdout + registered.stderr).toBe(0) + const configured = await runPinnedOpenspec( + version, + ['config', 'set', 'defaultStore', 'team-context', '--string'], + scratch, + env + ) + expect(configured.exitCode, configured.stdout + configured.stderr).toBe(0) + + const doctorResult = await runPinnedOpenspec(version, ['doctor', '--json'], scratch, env) + expect(doctorResult.exitCode, doctorResult.stdout + doctorResult.stderr).toBe(0) + expectPinnedJsonDiscipline(doctorResult) + expect( + parsePinnedJson(doctorResult, (payload) => CliDoctorSchema.parse(payload)).root + ).toMatchObject({ + path: physicalStoreRoot, + source: 'global_default', + store_id: 'team-context', + }) + + const contextResult = await runPinnedOpenspec(version, ['context', '--json'], scratch, env) + expect(contextResult.exitCode, contextResult.stdout + contextResult.stderr).toBe(0) + expect( + parsePinnedJson(contextResult, (payload) => CliContextSchema.parse(payload)).root + ).toMatchObject({ + path: physicalStoreRoot, + source: 'global_default', + store_id: 'team-context', + }) + }, 60_000) + + it(`keeps absent and stale fallback failures distinct on OpenSpec ${version}`, async () => { + fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-default-store`) + const scratch = join(fixtureRoot, 'scratch') + const env = pinnedFixtureEnv(fixtureRoot) + await mkdir(scratch, { recursive: true }) + + const absent = await runPinnedOpenspec(version, ['doctor', '--json'], scratch, env) + expect(absent.exitCode).toBe(1) + expect( + parsePinnedJson(absent, (payload) => CliDiagnosticFailureSchema.parse(payload)).status[0] + ?.code + ).toBe('no_openspec_root') + + const configured = await runPinnedOpenspec( + version, + ['config', 'set', 'defaultStore', 'ghost-plans', '--string'], + scratch, + env + ) + expect(configured.exitCode, configured.stdout + configured.stderr).toBe(0) + const stale = await runPinnedOpenspec(version, ['doctor', '--json'], scratch, env) + expect(stale.exitCode).toBe(1) + expect( + parsePinnedJson(stale, (payload) => CliDiagnosticFailureSchema.parse(payload)).status[0] + ).toMatchObject({ + code: 'no_registered_stores', + message: expect.stringContaining("Global defaultStore 'ghost-plans'"), + fix: expect.stringContaining('openspec config unset defaultStore'), + }) + }, 60_000) + + it(`names the declaring project in store-backed status edit roots on OpenSpec ${version}`, async () => { + fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-edit-roots`) + const storeRoot = join(fixtureRoot, 'team-store') + const declaringProject = join(fixtureRoot, 'declaring-project') + const scratch = join(fixtureRoot, 'scratch') + const env = pinnedFixtureEnv(fixtureRoot) + await mkdir(join(storeRoot, 'openspec', 'specs'), { recursive: true }) + await mkdir(join(storeRoot, 'openspec', 'changes', 'archive'), { recursive: true }) + await writeFile(join(storeRoot, 'openspec', 'config.yaml'), 'schema: spec-driven\n') + // A config-only pointer root (no planning shape) is the declaring project. + await mkdir(join(declaringProject, 'openspec'), { recursive: true }) + await writeFile(join(declaringProject, 'openspec', 'config.yaml'), 'store: team-context\n') + await mkdir(scratch, { recursive: true }) + const physicalStoreRoot = await realpath(storeRoot) + const physicalDeclaringRoot = await realpath(declaringProject) + + const registered = await runPinnedOpenspec( + version, + ['store', 'register', storeRoot, '--id', 'team-context', '--yes', '--json'], + scratch, + env + ) + expect(registered.exitCode, registered.stdout + registered.stderr).toBe(0) + + // The change lives in the store itself, so only the current-path declaring + // pointer can distinguish the two branches. + const changeDir = join(storeRoot, 'openspec', 'changes', 'store-change') + await mkdir(changeDir, { recursive: true }) + await writeFile(join(changeDir, '.openspec.yaml'), 'schema: spec-driven\n') + await writeFile(join(changeDir, 'proposal.md'), '# Proposal\n\nStore-backed change.\n') + + // Branch 1 — declaring project on the current path: implementation edits are + // routed to the declaring repo, which joins the store in allowedEditRoots + // (implementation first, then the planning store). + const fromDeclaring = await runPinnedOpenspec( + version, + ['status', '--change', 'store-change', '--store', 'team-context', '--json'], + declaringProject, + env + ) + expectPinnedJsonDiscipline(fromDeclaring) + const declaring = parsePinnedSuccessJson(fromDeclaring, (payload) => + CliWorkflowStatusSuccessSchema.parse(payload) + ) + expect(declaring.root.store_id).toBe('team-context') + expect(declaring.actionContext.allowedEditRoots).toEqual([ + physicalDeclaringRoot, + physicalStoreRoot, + ]) + expect(declaring.actionContext.constraints).toEqual([ + `Change artifacts live in store 'team-context' (${physicalStoreRoot}). ` + + `Implementation edits go in ${physicalDeclaringRoot}, the project on the current path that declares this store; ` + + 'ask the user before editing any other repository.', + ]) + + // Branch 2 — no declaring project anywhere above the scratch cwd: the store + // stays the only edit root and the guidance tells the agent to ask which + // repository implements the change. + const fromScratch = await runPinnedOpenspec( + version, + ['status', '--change', 'store-change', '--store', 'team-context', '--json'], + scratch, + env + ) + expectPinnedJsonDiscipline(fromScratch) + const storeOnly = parsePinnedSuccessJson(fromScratch, (payload) => + CliWorkflowStatusSuccessSchema.parse(payload) + ) + expect(storeOnly.root.store_id).toBe('team-context') + expect(storeOnly.actionContext.allowedEditRoots).toEqual([physicalStoreRoot]) + expect(storeOnly.actionContext.constraints).toEqual([ + `Change artifacts live in store 'team-context' (${physicalStoreRoot}). ` + + 'OpenSpec could not determine which repository implements this change; ' + + 'ask the user which repository to edit, and make implementation edits there.', + ]) + }, 60_000) + } +}) diff --git a/packages/core/src/official-cli-v13-nested-change-fixtures.test.ts b/packages/core/src/official-cli-v14-nested-change-fixtures.test.ts similarity index 90% rename from packages/core/src/official-cli-v13-nested-change-fixtures.test.ts rename to packages/core/src/official-cli-v14-nested-change-fixtures.test.ts index 67a9a872..99aff86b 100644 --- a/packages/core/src/official-cli-v13-nested-change-fixtures.test.ts +++ b/packages/core/src/official-cli-v14-nested-change-fixtures.test.ts @@ -1,6 +1,6 @@ /** - * Orthogonal intents (created 2026-09-17 Asia/Shanghai): - * 1. Execute the pinned OpenSpec 1.13.1 change-list nested-directory contract on a real + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Execute the pinned OpenSpec 1.14.0 change-list nested-directory contract on a real * repository: a namespace-folder entry carries `nested` with 0/0 task fields, a * coexisting flat change carries none, and the top-level `warnings` array reports * code `nested_change_directory` with the upstream message preserved verbatim. @@ -8,7 +8,11 @@ * dedicated ERROR finding (single item and bulk `--all`) instead of the generic * delta-authoring error, while the flat change keeps its ordinary error and the * buried change stays invisible to bulk enumeration. + * 3. Carry over the 1.13.1 nested/warnings members as v14 regression fixtures onto the + * v14 single-series window (the one new 1.14 list member, `archived`, is owned by + * the change-list inventory suite). * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — executable nested/warnings change-list and validate-finding evidence (update-openspec-cli-1131 Slice 4). */ @@ -16,7 +20,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { - PINNED_OPENSPEC_V13_VERSIONS, + PINNED_OPENSPEC_V14_VERSIONS, createPinnedFixtureRoot, expectPinnedJsonDiscipline, expectPinnedVersion, @@ -25,12 +29,12 @@ import { pinnedFixtureEnv, removePinnedFixtureRoot, runPinnedOpenspec, - type PinnedOpenspecV13Version, -} from './__tests__/official-cli-v13-fixtures.js' + type PinnedOpenspecV14Version, +} from './__tests__/official-cli-v14-fixtures.js' import { CliChangeListSchema, CliValidateReportSchema } from './cli-contracts/workflow.js' async function initProject( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv ): Promise { @@ -44,7 +48,8 @@ async function initProject( } /** - * Fixture topology (verified against the v1.13.1 pin, 634c557): + * Fixture topology (verified against the v1.14.0 pin, 94ca9c1e; constraint first + * verified at v1.13.1, 634c557): * * - `changes/area/` is a namespace folder: its root must hold ONLY directories. A root * marker (`.openspec.yaml`, `proposal.md`, `tasks.md`, `design.md`) or any own @@ -74,7 +79,7 @@ async function writeNestedFixtureProject(project: string): Promise { ) } -describe('pinned OpenSpec 1.13 nested change directory fixtures', () => { +describe('pinned OpenSpec 1.14 nested change directory fixtures', () => { let fixtureRoot: string | null = null afterEach(async () => { @@ -82,7 +87,7 @@ describe('pinned OpenSpec 1.13 nested change directory fixtures', () => { fixtureRoot = null }) - for (const version of PINNED_OPENSPEC_V13_VERSIONS) { + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { it(`projects namespace entries and hygiene warnings from list --json on OpenSpec ${version}`, async () => { fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-nested-list`) const project = join(fixtureRoot, 'project') diff --git a/packages/core/src/official-cli-v13-nested-spec-fixtures.test.ts b/packages/core/src/official-cli-v14-nested-spec-fixtures.test.ts similarity index 75% rename from packages/core/src/official-cli-v13-nested-spec-fixtures.test.ts rename to packages/core/src/official-cli-v14-nested-spec-fixtures.test.ts index 922428ce..49e91cd6 100644 --- a/packages/core/src/official-cli-v13-nested-spec-fixtures.test.ts +++ b/packages/core/src/official-cli-v14-nested-spec-fixtures.test.ts @@ -1,10 +1,12 @@ /** - * Orthogonal intents (created 2026-09-12 Asia/Shanghai): - * 1. Execute the pinned OpenSpec 1.13.1 CLI against a recursive owned Spec identity. + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Execute the pinned OpenSpec 1.14.0 CLI against a recursive owned Spec identity. * 2. Prove list and show preserve every identity segment and requirement content on - * the v13 single-series window. - * 3. Carry over the admitted-line nested Spec contract proven for 1.10-1.12. + * the v14 single-series window, now including the 1.14 requirement/scenario name + * members on the nested show payload. + * 3. Carry over the admitted-line nested Spec contract proven for 1.10-1.13. * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-08-01): adapt OpenSpec 1.7 nested Spec ids such as `platform/auth`. * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11" @@ -13,7 +15,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { - PINNED_OPENSPEC_V13_VERSIONS, + PINNED_OPENSPEC_V14_VERSIONS, createPinnedFixtureRoot, expectPinnedJsonDiscipline, expectPinnedVersion, @@ -21,10 +23,10 @@ import { pinnedFixtureEnv, removePinnedFixtureRoot, runPinnedOpenspec, -} from './__tests__/official-cli-v13-fixtures.js' +} from './__tests__/official-cli-v14-fixtures.js' import { CliShowSpecSchema, CliSpecListSchema } from './cli-contracts/workflow.js' -describe('pinned OpenSpec 1.13 nested Spec fixtures', () => { +describe('pinned OpenSpec 1.14 nested Spec fixtures', () => { let fixtureRoot: string | null = null afterEach(async () => { @@ -32,7 +34,7 @@ describe('pinned OpenSpec 1.13 nested Spec fixtures', () => { fixtureRoot = null }) - for (const version of PINNED_OPENSPEC_V13_VERSIONS) { + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { it(`lists and shows platform/auth as one complete Spec identity on OpenSpec ${version}`, async () => { fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-nested-spec`) const project = join(fixtureRoot, 'project') @@ -95,7 +97,17 @@ describe('pinned OpenSpec 1.13 nested Spec fixtures', () => { requirementCount: 1, requirements: [ { + // 1.14 name members: the nested identity carries them like any other + // Spec, requirement and scenario alike. + name: 'Nested authentication', text: 'The platform SHALL preserve recursive Spec identity.', + scenarios: [ + { + name: 'Show nested Spec', + rawText: + '- **WHEN** the nested Spec is listed and shown\n- **THEN** every identity segment is preserved', + }, + ], }, ], }) diff --git a/packages/core/src/official-cli-v13-show-diff-fixtures.test.ts b/packages/core/src/official-cli-v14-show-fixtures.test.ts similarity index 64% rename from packages/core/src/official-cli-v13-show-diff-fixtures.test.ts rename to packages/core/src/official-cli-v14-show-fixtures.test.ts index 1f6f8608..087d250a 100644 --- a/packages/core/src/official-cli-v13-show-diff-fixtures.test.ts +++ b/packages/core/src/official-cli-v14-show-fixtures.test.ts @@ -1,13 +1,20 @@ /** - * Orthogonal intents (created 2026-09-12 Asia/Shanghai): - * 1. Execute the pinned OpenSpec 1.13.1 `show --json --diff` contract against - * a real fixture project: MODIFIED deltas carry the unified diff body. + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Execute the pinned OpenSpec 1.14.0 `show` family against a real fixture project: + * `show --json --diff` (MODIFIED deltas carry the unified diff body) and + * `show --type spec --json` (1.14 requirement/scenario name members). * 2. Prove the diff stays CLI-owned evidence (`@@` hunks, `-`/`+` lines, no warning on * a clean modification; the exact upstream near-miss header warning when names * differ in case). - * 3. Carry over the admitted-line requirement-diff contract proven for 1.11-1.12 onto - * the v13 single-series window. + * 3. Carry over the admitted-line requirement-diff contract proven for 1.11-1.13 onto + * the v14 single-series window. + * 4. (2026-10-02, target-openspec-cli-114-line Slice 5) Prove the 1.14 show-Spec + * `name` members on the executed executable: every requirement and scenario carries + * its name (upstream normalizeRequirementName/scenarioNameFromHeaderText) beside + * the retained text/rawText bodies — the identity archive matches on, per + * `references/openspec-1.14.0-report.md` Protocol delta. * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11" */ @@ -15,7 +22,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { - PINNED_OPENSPEC_V13_VERSIONS, + PINNED_OPENSPEC_V14_VERSIONS, createPinnedFixtureRoot, expectPinnedJsonDiscipline, expectPinnedVersion, @@ -23,9 +30,10 @@ import { pinnedFixtureEnv, removePinnedFixtureRoot, runPinnedOpenspec, - type PinnedOpenspecV13Version, -} from './__tests__/official-cli-v13-fixtures.js' + type PinnedOpenspecV14Version, +} from './__tests__/official-cli-v14-fixtures.js' import { CliShowChangeDiffSuccessSchema } from './cli-contracts/show-diff.js' +import { CliShowSpecDocumentSchema } from './cli-contracts/workflow.js' const MAIN_SPEC = [ '## Purpose', @@ -43,7 +51,7 @@ const MAIN_SPEC = [ ].join('\n') async function initProject( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv ): Promise { @@ -57,7 +65,7 @@ async function initProject( } async function createChangeWithDelta( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv, changeId: string, @@ -81,7 +89,7 @@ async function createChangeWithDelta( await writeFile(join(changeDir, 'specs', 'billing', 'spec.md'), delta) } -describe('pinned OpenSpec 1.13 show --diff fixtures', () => { +describe('pinned OpenSpec 1.14 show fixtures', () => { let fixtureRoot: string | null = null afterEach(async () => { @@ -89,7 +97,7 @@ describe('pinned OpenSpec 1.13 show --diff fixtures', () => { fixtureRoot = null }) - for (const version of PINNED_OPENSPEC_V13_VERSIONS) { + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { it(`attaches a unified diff with hunks to a clean MODIFIED delta on OpenSpec ${version}`, async () => { fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-show-diff`) const project = join(fixtureRoot, 'project') @@ -196,5 +204,68 @@ describe('pinned OpenSpec 1.13 show --diff fixtures', () => { expect(delta.diff).toBeDefined() expect(delta.diff).toMatch(/^@@ /) }, 60_000) + + it(`carries requirement and scenario names on show --type spec on OpenSpec ${version}`, async () => { + fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-show-spec`) + const project = join(fixtureRoot, 'project') + const env = pinnedFixtureEnv(fixtureRoot) + await mkdir(project, { recursive: true }) + await mkdir(join(project, 'openspec', 'specs', 'billing'), { recursive: true }) + await writeFile( + join(project, 'openspec', 'specs', 'billing', 'spec.md'), + [ + '## Purpose', + 'Billing rules.', + '', + '## Requirements', + '', + '### Requirement: Billing rules', + 'The system SHALL apply billing rules.', + '', + '#### Scenario: Existing behavior', + '- **WHEN** billing runs', + '- **THEN** billing succeeds', + '', + ].join('\n') + ) + + await expectPinnedVersion(version, project, env) + const initialized = await runPinnedOpenspec( + version, + ['init', project, '--tools=none'], + project, + env + ) + expect(initialized.exitCode, initialized.stdout + '\n' + initialized.stderr).toBe(0) + + const result = await runPinnedOpenspec( + version, + ['show', 'billing', '--type', 'spec', '--json'], + project, + env + ) + expectPinnedJsonDiscipline(result) + const shown = parsePinnedSuccessJson(result, (payload) => + CliShowSpecDocumentSchema.parse(payload) + ) + + expect(shown.id).toBe('billing') + expect(shown.requirementCount).toBe(1) + // 1.14 name members: the requirement and each scenario carry their header + // name beside the retained text/rawText bodies. The requirement name is + // the identity archive matches on — verbatim CLI value, never re-derived. + expect(shown.requirements).toEqual([ + { + name: 'Billing rules', + text: 'The system SHALL apply billing rules.', + scenarios: [ + { + name: 'Existing behavior', + rawText: '- **WHEN** billing runs\n- **THEN** billing succeeds', + }, + ], + }, + ]) + }, 60_000) } }) diff --git a/packages/core/src/official-cli-v13-task-reading-fixtures.test.ts b/packages/core/src/official-cli-v14-task-reading-fixtures.test.ts similarity index 84% rename from packages/core/src/official-cli-v13-task-reading-fixtures.test.ts rename to packages/core/src/official-cli-v14-task-reading-fixtures.test.ts index f18551cc..5e827e31 100644 --- a/packages/core/src/official-cli-v13-task-reading-fixtures.test.ts +++ b/packages/core/src/official-cli-v14-task-reading-fixtures.test.ts @@ -1,12 +1,14 @@ /** - * Orthogonal intents (created 2026-09-17 Asia/Shanghai): - * 1. Execute the pinned OpenSpec 1.13.1 widened task-line reading through `list --json` + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Execute the pinned OpenSpec 1.14.0 widened task-line reading through `list --json` * on a real repository: plus/ordered markers, an indented `~` wave, and a padded * `[ x]` box all count, multi-token `[WIP]` labels and link bullets never count, and * only an `x`/`X` marker reads as done. * 2. Prove the shared local parser (`parseMarkdownTasks`) counts the exact same file - * identically — executable parity evidence for the Slice 3 task-line reading law. + * identically — executable parity evidence for the task-line reading law, carried + * onto the v14 single-series window (1.14 keeps `{x}`-style markers counted). * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — task-count parity fixture evidence (update-openspec-cli-1131 Slice 4). */ @@ -14,7 +16,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { - PINNED_OPENSPEC_V13_VERSIONS, + PINNED_OPENSPEC_V14_VERSIONS, createPinnedFixtureRoot, expectPinnedJsonDiscipline, expectPinnedVersion, @@ -22,14 +24,15 @@ import { pinnedFixtureEnv, removePinnedFixtureRoot, runPinnedOpenspec, - type PinnedOpenspecV13Version, -} from './__tests__/official-cli-v13-fixtures.js' + type PinnedOpenspecV14Version, +} from './__tests__/official-cli-v14-fixtures.js' import { CliChangeListSchema } from './cli-contracts/workflow.js' import { parseMarkdownTasks } from './task-progress.js' /** * Widened-syntax tasks fixture. Counting derivation against the pinned upstream pattern - * (references/openspec/src/utils/task-progress.ts, v1.13.1 634c557): + * (references/openspec/src/utils/task-progress.ts, v1.14.0 94ca9c1e; widened pattern + * first shipped in v1.13.1, 634c557): * * `+ [ ] plus marker` -> task, NOT done (whitespace-only box under a `+` marker) * `1. [ ] ordered marker` -> task, NOT done (ordered `1.` marker) @@ -56,7 +59,7 @@ const WIDENED_TASKS_MARKDOWN = [ ].join('\n') async function initProject( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv ): Promise { @@ -69,7 +72,7 @@ async function initProject( expect(initialized.exitCode, initialized.stdout + '\n' + initialized.stderr).toBe(0) } -describe('pinned OpenSpec 1.13 task-line reading fixtures', () => { +describe('pinned OpenSpec 1.14 task-line reading fixtures', () => { let fixtureRoot: string | null = null afterEach(async () => { @@ -77,7 +80,7 @@ describe('pinned OpenSpec 1.13 task-line reading fixtures', () => { fixtureRoot = null }) - for (const version of PINNED_OPENSPEC_V13_VERSIONS) { + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { it(`counts widened task lines exactly like the local parser on OpenSpec ${version}`, async () => { fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-task-reading`) const project = join(fixtureRoot, 'project') diff --git a/packages/core/src/official-cli-v13-validation-findings-fixtures.test.ts b/packages/core/src/official-cli-v14-validation-findings-fixtures.test.ts similarity index 93% rename from packages/core/src/official-cli-v13-validation-findings-fixtures.test.ts rename to packages/core/src/official-cli-v14-validation-findings-fixtures.test.ts index 566f24c8..cc6848e8 100644 --- a/packages/core/src/official-cli-v13-validation-findings-fixtures.test.ts +++ b/packages/core/src/official-cli-v14-validation-findings-fixtures.test.ts @@ -1,6 +1,6 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): - * 1. Execute the pinned OpenSpec 1.13 `validate --report findings --json` contract + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Execute the pinned OpenSpec 1.14 `validate --report findings --json` contract * against real fixture projects: the populated findings document, empty-scope * documents, and the typed request-error envelope. * 2. Prove the merge-conflict INFO class stays verdict-neutral (`valid: true`, exit 0) @@ -8,22 +8,21 @@ * 3. Prove the findings transport preserves the full-run exit rule: a failing fixture * exits 1 while stdout stays one complete JSON document and `summary` keeps the * full-run totals. - * 4. (2026-09-26, update-openspec-cli-1132 Slice 4) Prove the 1.13.2 purpose-marker - * case rules on the executed executable: a shouted `TODO` opening reports whatever - * follows, while a lowercase prose opener (`Todo el …`) stays quiet. - * 5. (2026-09-26) Prove the 1.13.2 MODIFIED scenario balance suffix: the drop error - * also describes what the block adds (`It adds 1 scenario not in the current - * spec: …`), per `references/openspec-1.13.2-report.md` P4. + * 4. Prove the purpose-marker case rules on the executed executable: a shouted `TODO` + * opening reports whatever follows, while a lowercase prose opener (`Todo el …`) + * stays quiet. + * 5. Prove the MODIFIED scenario balance suffix: the drop error also describes what + * the block adds (`It adds 1 scenario not in the current spec: …`). The 1.14 line + * ships ~20 validate/parser fixes without changing these message surfaces; this + * suite carries them onto the v14 window as regression fixtures. * - * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" - * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — in-window patch rotation 1.13.1 -> 1.13.2, - * validation message-content executable matrix (update-openspec-cli-1132 Slice 4). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { - PINNED_OPENSPEC_V13_VERSIONS, + PINNED_OPENSPEC_V14_VERSIONS, createPinnedFixtureRoot, expectPinnedJsonDiscipline, expectPinnedVersion, @@ -32,8 +31,8 @@ import { pinnedFixtureEnv, removePinnedFixtureRoot, runPinnedOpenspec, - type PinnedOpenspecV13Version, -} from './__tests__/official-cli-v13-fixtures.js' + type PinnedOpenspecV14Version, +} from './__tests__/official-cli-v14-fixtures.js' import { CliDiagnosticFailureSchema } from './cli-contracts/common.js' import { CliValidateFindingsResultSchema, @@ -48,7 +47,7 @@ const REPORT_REQUEST_FIX = 'without an item name. Do not combine archived and active scopes.' async function initProject( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv ): Promise { @@ -63,7 +62,7 @@ async function initProject( /** A change whose delta MODIFIES requirements of a spec that does not exist. */ async function createModifiedAgainstMissingSpecChange( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv, changeId: string @@ -101,7 +100,7 @@ async function createModifiedAgainstMissingSpecChange( /** A loadable change that fails validation through an ERROR issue. */ async function createFailingChange( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv, changeId: string @@ -167,7 +166,7 @@ async function writeMainSpec( ) } -describe('pinned OpenSpec 1.13 validation findings fixtures', () => { +describe('pinned OpenSpec 1.14 validation findings fixtures', () => { let fixtureRoot: string | null = null afterEach(async () => { @@ -175,7 +174,7 @@ describe('pinned OpenSpec 1.13 validation findings fixtures', () => { fixtureRoot = null }) - for (const version of PINNED_OPENSPEC_V13_VERSIONS) { + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { it(`returns a populated findings document whose merge-conflict INFO stays verdict-neutral on OpenSpec ${version}`, async () => { fixtureRoot = await createPinnedFixtureRoot( `cli-${version.replace(/\./g, '')}-findings-populated` diff --git a/packages/core/src/official-cli-v13-validation-full-fixtures.test.ts b/packages/core/src/official-cli-v14-validation-full-fixtures.test.ts similarity index 94% rename from packages/core/src/official-cli-v13-validation-full-fixtures.test.ts rename to packages/core/src/official-cli-v14-validation-full-fixtures.test.ts index ffc6c569..1ff23e37 100644 --- a/packages/core/src/official-cli-v13-validation-full-fixtures.test.ts +++ b/packages/core/src/official-cli-v14-validation-full-fixtures.test.ts @@ -1,12 +1,13 @@ /** - * Orthogonal intents (created 2026-09-12 Asia/Shanghai): - * 1. Execute the pinned OpenSpec 1.13.1 full bulk `validate --json` report: schema - * compatibility with the 1.11/1.12 envelope plus the merge-conflict INFO issues. + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Execute the pinned OpenSpec 1.14.0 full bulk `validate --json` report: schema + * compatibility with the 1.11-1.13 envelope plus the merge-conflict INFO issues. * 2. Prove `--report full` stays the explicit default and strict escalation plus the * human next-steps footer behavior are unchanged. - * 3. Carry over the admitted-line contracts the v11/v12 validation matrix proved: + * 3. Carry over the admitted-line contracts the v11-v13 validation matrix proved: * schemas success/selected-Root failure envelopes and archived task validation. * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-08-15): "v9的适配需要同时适配 1.8和1.9。" * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11" @@ -15,7 +16,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { - PINNED_OPENSPEC_V13_VERSIONS, + PINNED_OPENSPEC_V14_VERSIONS, createPinnedFixtureRoot, expectPinnedJsonDiscipline, expectPinnedVersion, @@ -24,8 +25,8 @@ import { pinnedFixtureEnv, removePinnedFixtureRoot, runPinnedOpenspec, - type PinnedOpenspecV13Version, -} from './__tests__/official-cli-v13-fixtures.js' + type PinnedOpenspecV14Version, +} from './__tests__/official-cli-v14-fixtures.js' import { CliSchemasFailureSchema, CliSchemasSuccessSchema, @@ -38,7 +39,7 @@ import { } from './cli-contracts/workflow.js' async function initProject( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv ): Promise { @@ -53,7 +54,7 @@ async function initProject( /** A change whose delta MODIFIES requirements of a spec that does not exist. */ async function createModifiedAgainstMissingSpecChange( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv, changeId: string @@ -91,7 +92,7 @@ async function createModifiedAgainstMissingSpecChange( /** A loadable change that fails validation through an ERROR issue. */ async function createFailingChange( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv, changeId: string @@ -124,7 +125,7 @@ async function createFailingChange( } async function createChangeWithTasks( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv, changeId: string, @@ -158,7 +159,7 @@ async function writePlaceholderSpec(project: string): Promise { ) } -describe('pinned OpenSpec 1.13 full validation report fixtures', () => { +describe('pinned OpenSpec 1.14 full validation report fixtures', () => { let fixtureRoot: string | null = null afterEach(async () => { @@ -166,7 +167,7 @@ describe('pinned OpenSpec 1.13 full validation report fixtures', () => { fixtureRoot = null }) - for (const version of PINNED_OPENSPEC_V13_VERSIONS) { + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { it(`keeps the full bulk report schema-compatible and carries the merge-conflict INFO beside real errors on OpenSpec ${version}`, async () => { fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-validate-full`) const project = join(fixtureRoot, 'project') diff --git a/packages/core/src/official-cli-v14-version-report-fixtures.test.ts b/packages/core/src/official-cli-v14-version-report-fixtures.test.ts new file mode 100644 index 00000000..58efe5af --- /dev/null +++ b/packages/core/src/official-cli-v14-version-report-fixtures.test.ts @@ -0,0 +1,63 @@ +/** + * Orthogonal intents (created 2026-10-02 Asia/Shanghai): + * 1. Execute the pinned OpenSpec 1.14.0 `openspec version --json` command (new in + * 1.14) against the isolated fixture environment: the typed envelope decodes with + * `schemaVersion: 1`, the running `version`, and the resolved `install` facts. + * 2. Prove the no-probe default: without `--check` the `update` member stays absent + * (never null, never a synthesized offline status) — the registry probe is opt-in + * and this suite never performs network access, per + * `references/openspec-1.14.0-report.md` section 1. + * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + */ +import { mkdir } from 'node:fs/promises' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + PINNED_OPENSPEC_V14_VERSIONS, + createPinnedFixtureRoot, + expectPinnedJsonDiscipline, + expectPinnedVersion, + parsePinnedSuccessJson, + pinnedFixtureEnv, + removePinnedFixtureRoot, + runPinnedOpenspec, +} from './__tests__/official-cli-v14-fixtures.js' +import { CliVersionSchema } from './cli-contracts/workflow.js' + +describe('pinned OpenSpec 1.14 version report fixtures', () => { + let fixtureRoot: string | null = null + + afterEach(async () => { + await removePinnedFixtureRoot(fixtureRoot) + fixtureRoot = null + }) + + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { + it(`reports the install envelope without the check-gated update member on OpenSpec ${version}`, async () => { + fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-version-report`) + const project = join(fixtureRoot, 'project') + const env = pinnedFixtureEnv(fixtureRoot) + await mkdir(project, { recursive: true }) + + await expectPinnedVersion(version, project, env) + + const result = await runPinnedOpenspec(version, ['version', '--json'], project, env) + expectPinnedJsonDiscipline(result) + const report = parsePinnedSuccessJson(result, (payload) => CliVersionSchema.parse(payload)) + + // The envelope names its schema version and the running executable line. + expect(report.schemaVersion).toBe(1) + expect(report.version).toBe(version) + // The resolved install location is a real absolute path of the running + // executable's package; packageManager/scope stay nullable install facts + // (an unresolvable install reports all-null upstream, never a guess). + expect(report.install.location).toBeTruthy() + expect(report.install.location).toMatch(/@fission-ai[/\\]openspec/) + // The registry probe is opt-in: without `--check` the update member is + // absent (conditionally spread upstream), never null or synthesized. + expect(report.update).toBeUndefined() + expect('update' in report).toBe(false) + }, 60_000) + } +}) diff --git a/packages/core/src/official-cli-v13-workflow-fixtures.test.ts b/packages/core/src/official-cli-v14-workflow-fixtures.test.ts similarity index 90% rename from packages/core/src/official-cli-v13-workflow-fixtures.test.ts rename to packages/core/src/official-cli-v14-workflow-fixtures.test.ts index dea7f267..8f6fa848 100644 --- a/packages/core/src/official-cli-v13-workflow-fixtures.test.ts +++ b/packages/core/src/official-cli-v14-workflow-fixtures.test.ts @@ -1,15 +1,16 @@ /** - * Orthogonal intents (created 2026-09-12 Asia/Shanghai): - * 1. Execute the pinned OpenSpec 1.13.1 workflow contract end to end: skipped Status, + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Execute the pinned OpenSpec 1.14.0 workflow contract end to end: skipped Status, * tasks/Apply/Archive Instructions on one real skip-specs change. * 2. Prove explicit planning completion (`isPlanningComplete`) stays protocol truth while * `isComplete` remains retained alias evidence, and Apply `progress` stays authoritative * over the actionable task list (planning/task separation). * 3. Prove Apply and Archive operation guidance stays distinct from artifact rules. * 4. Prove `init --language` persists the fixed context block and never overwrites config. - * 5. Prove the 1.13 JSON stream discipline: one stdout document, stderr without + * 5. Prove the 1.14 JSON stream discipline: one stdout document, stderr without * telemetry or completion-tip noise. * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11" * Original request (2026-08-01): adapt the complete observable OpenSpec 1.7 workflow protocol. @@ -18,7 +19,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { - PINNED_OPENSPEC_V13_VERSIONS, + PINNED_OPENSPEC_V14_VERSIONS, createPinnedFixtureRoot, expectPinnedJsonDiscipline, expectPinnedVersion, @@ -26,8 +27,8 @@ import { pinnedFixtureEnv, removePinnedFixtureRoot, runPinnedOpenspec, - type PinnedOpenspecV13Version, -} from './__tests__/official-cli-v13-fixtures.js' + type PinnedOpenspecV14Version, +} from './__tests__/official-cli-v14-fixtures.js' import { CliApplyInstructionsSuccessSchema, CliArchiveInstructionsSuccessSchema, @@ -36,7 +37,7 @@ import { } from './cli-contracts/workflow.js' async function initProject( - version: PinnedOpenspecV13Version, + version: PinnedOpenspecV14Version, project: string, env: NodeJS.ProcessEnv ): Promise { @@ -49,7 +50,7 @@ async function initProject( expect(initialized.exitCode, initialized.stdout + '\n' + initialized.stderr).toBe(0) } -describe('pinned OpenSpec 1.13 workflow fixtures', () => { +describe('pinned OpenSpec 1.14 workflow fixtures', () => { let fixtureRoot: string | null = null afterEach(async () => { @@ -57,7 +58,7 @@ describe('pinned OpenSpec 1.13 workflow fixtures', () => { fixtureRoot = null }) - for (const version of PINNED_OPENSPEC_V13_VERSIONS) { + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { it(`executes skipped Status plus Apply and Archive Instructions on OpenSpec ${version}`, async () => { fixtureRoot = await createPinnedFixtureRoot(`cli-${version.replace(/\./g, '')}-workflow`) const project = join(fixtureRoot, 'project') @@ -212,11 +213,19 @@ describe('pinned OpenSpec 1.13 workflow fixtures', () => { // The upstream parser counts every checkbox line, including indented // sub-tasks and blank-description lines; the actionable tasks list hides // blank-description entries. progress is the denominator, tasks.length - // is only the actionable presentation. + // is only the actionable presentation. 1.14 LocatedTask members ride every + // entry: absolute sourcePath plus 1-based line (tasks.md lines 3 and 4). expect(apply.progress).toEqual({ total: 3, complete: 1, remaining: 2 }) + const tasksPath = join(apply.changeDir, 'tasks.md') expect(apply.tasks).toEqual([ - { id: '1', description: 'Plan the migration', done: true }, - { id: '2', description: 'Nested sub-task counted by progress', done: false }, + { id: '1', description: 'Plan the migration', done: true, sourcePath: tasksPath, line: 3 }, + { + id: '2', + description: 'Nested sub-task counted by progress', + done: false, + sourcePath: tasksPath, + line: 4, + }, ]) expect(apply.tasks.length).toBe(2) expect(apply.state).toBe('ready') diff --git a/packages/core/src/opsx-kernel-schemas-root.fixtures.test.ts b/packages/core/src/opsx-kernel-schemas-root.fixtures.test.ts index 6e30d6f5..16410b2c 100644 --- a/packages/core/src/opsx-kernel-schemas-root.fixtures.test.ts +++ b/packages/core/src/opsx-kernel-schemas-root.fixtures.test.ts @@ -1,7 +1,7 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Prove the Kernel forwards the selected Root's Store selector to schemas on the - * admitted OpenSpec line (the v13 single-series window: 1.13 resolves schemas + * admitted OpenSpec line (the v14 single-series window: 1.14 resolves schemas * through the selected Root). * 2. Drive the proof through the production OpsxKernel path with the pinned executable. * @@ -17,18 +17,21 @@ * it is now registered in tsconfig.workflow-contract-tests.json per the typed-test-evidence law). * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — in-window patch rotation moved the * local PINNED_BINS key to 1.13.2 together with the v13 helper's pinned constant. + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + * — window rotation moves the admitted-line proof to the 1.14.0 executable together + * with the v14 helper (target-openspec-cli-114-line Slice 5). */ import { mkdir } from 'node:fs/promises' import { join, resolve } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { - PINNED_OPENSPEC_V13_VERSIONS, + PINNED_OPENSPEC_V14_VERSIONS, createPinnedFixtureRoot, pinnedFixtureEnv, removePinnedFixtureRoot, runPinnedOpenspec, - type PinnedOpenspecV13Version, -} from './__tests__/official-cli-v13-fixtures.js' + type PinnedOpenspecV14Version, +} from './__tests__/official-cli-v14-fixtures.js' import { cleanupTempDir } from './__tests__/test-utils.js' import { CliExecutor } from './cli-executor.js' import { CliProjectionCommandError } from './cli-projection.js' @@ -37,8 +40,8 @@ import { OpsxKernel } from './opsx-kernel.js' import { RuntimeInvalidationIndex } from './runtime-invalidation.js' const PINNED_BINS = { - '1.13.2': resolve(import.meta.dirname, '../node_modules/openspec-cli-113/bin/openspec.js'), -} satisfies Record + '1.14.0': resolve(import.meta.dirname, '../node_modules/openspec-cli-114/bin/openspec.js'), +} satisfies Record const tempDirs: string[] = [] @@ -54,7 +57,7 @@ describe('OpsxKernel schemas selected-Root forwarding', () => { fixtureRoot = null }) - for (const version of PINNED_OPENSPEC_V13_VERSIONS) { + for (const version of PINNED_OPENSPEC_V14_VERSIONS) { it(`forwards the Store selector to schemas on OpenSpec ${version}`, async () => { fixtureRoot = await createPinnedFixtureRoot( `kernel-schemas-root-${version.replace(/\./g, '')}` diff --git a/packages/core/src/opsx-types.test.ts b/packages/core/src/opsx-types.test.ts index 35ffd299..edc9706d 100644 --- a/packages/core/src/opsx-types.test.ts +++ b/packages/core/src/opsx-types.test.ts @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Verify Apply instruction context-file normalization. * 2. Require command-specific CLI evidence on demand-driven instruction leaves. * 3. Preserve typed OpenSpec 1.6 Reference indexes on both instruction surfaces. @@ -26,7 +26,9 @@ * Original request (2026-07-23): "OPSX Status 不应等待完整 Kernel warmup,且必须保留 CLI evidence。" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — tracking-evidence projection + glob-recognition parity (update-openspec-cli-1132 Slice 2). - * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — typed contracts Slice 2 (target-openspec-cli-114-line). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — typed contracts Slice 2 (target-openspec-cli-114-line); + * Slice 5 fixture-matrix lane hygiene: the source-location guard annotation becomes + * optional members (matching the optional schema members) so the typecheck lane compiles. */ import { describe, expect, it } from 'vitest' import { @@ -311,7 +313,7 @@ describe('ApplyInstructionsSchema', () => { ], }) - const first: { sourcePath: string | undefined; line: number | undefined } = parsed.tasks[0]! + const first: { sourcePath?: string | undefined; line?: number | undefined } = parsed.tasks[0]! expect(first.sourcePath).toBe('/repo/openspec/changes/add-example/tasks.md') expect(first.line).toBe(1) expect(parsed.tasks[1]?.line).toBe(3) diff --git a/packages/core/src/tool-init-state.test.ts b/packages/core/src/tool-init-state.test.ts index 7b40b5b4..3bceface 100644 --- a/packages/core/src/tool-init-state.test.ts +++ b/packages/core/src/tool-init-state.test.ts @@ -27,7 +27,7 @@ * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" * — Slice 3 rotates generated-by currency to the admitted 1.14.x line. * Note: openspec-cli-112 generator fixtures below are historical/boundary evidence for staleness and - * legacy-compat classification; positive v13 acceptance lives in the official-cli-v13 suite. + * legacy-compat classification; positive v14 acceptance lives in the official-cli-v14 suite. */ import { mkdir, writeFile } from 'fs/promises' import { dirname, join, resolve, sep } from 'path' diff --git a/packages/core/src/upstream-contract-regression.test.ts b/packages/core/src/upstream-contract-regression.test.ts index 9a4adde2..66e392c8 100644 --- a/packages/core/src/upstream-contract-regression.test.ts +++ b/packages/core/src/upstream-contract-regression.test.ts @@ -1,6 +1,6 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): - * 1. Pin the official OpenSpec v1.4 through v1.13 source contracts used by OpenSpecUI. + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Pin the official OpenSpec v1.4 through v1.14 source contracts used by OpenSpecUI. * 2. Prevent a version-gate-only adaptation from masking missing workflow or root behavior. * 3. Keep validation, archive, task, batch status, requirement-diff, and init-language * fixtures traceable to first-party source. @@ -18,6 +18,9 @@ * released v1.13.1 patch tag (634c557) inside the unchanged v13 window. * Original request (2026-09-26): rotate the reference pin assertion from v1.13.1 (634c557) to the * released v1.13.2 patch tag (db230978) inside the unchanged v13 window. + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + * — rotate the reference pin assertion from v1.13.2 (db230978) to the released v1.14.0 tag + * (94ca9c1e) for the OpenSpecUI 14 adaptation line. */ import { execFileSync } from 'node:child_process' import { readFileSync } from 'node:fs' @@ -91,13 +94,13 @@ describe('first-party OpenSpec 1.4-1.7 contracts', () => { expect(rootInspection).toContain('inspectOptionalPlanningDirectory') }) - it('pins the reference checkout to the official v1.13.2 commit', () => { + it('pins the reference checkout to the official v1.14.0 commit', () => { const commit = execFileSync('git', ['-C', upstreamRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8', windowsHide: true, }).trim() - expect(commit).toBe('db2309783547a14e150dbcbfc19120e4028446c3') + expect(commit).toBe('94ca9c1eb15d1b49c06c988419b75c3d95f8b2b5') }) it('locks the retained planning-completion, schemas sum type, and archived validation sources', () => { diff --git a/packages/core/tsconfig.agent-delivery-tests.json b/packages/core/tsconfig.agent-delivery-tests.json index 7d51e8b2..63dbef8e 100644 --- a/packages/core/tsconfig.agent-delivery-tests.json +++ b/packages/core/tsconfig.agent-delivery-tests.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.json", "include": [ "src/agent-command-content.test.ts", - "src/official-cli-v13-agent-delivery-fixtures.test.ts", + "src/official-cli-v14-agent-delivery-fixtures.test.ts", "src/agent-delivery-registry.test.ts", "src/agent-delivery-policy.test.ts", "src/tool-config.test.ts", diff --git a/packages/core/tsconfig.default-store-tests.json b/packages/core/tsconfig.default-store-tests.json index 902678cb..3a03ab04 100644 --- a/packages/core/tsconfig.default-store-tests.json +++ b/packages/core/tsconfig.default-store-tests.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.json", "include": [ "src/default-store-contracts.test.ts", - "src/official-cli-v13-default-store-fixtures.test.ts", + "src/official-cli-v14-default-store-fixtures.test.ts", "src/planning-config.ts", "src/root-context.ts" ], diff --git a/packages/core/tsconfig.nested-spec-tests.json b/packages/core/tsconfig.nested-spec-tests.json index 01f67355..692ff001 100644 --- a/packages/core/tsconfig.nested-spec-tests.json +++ b/packages/core/tsconfig.nested-spec-tests.json @@ -3,7 +3,7 @@ "include": [ "src/adapter.test.ts", "src/entity-id.test.ts", - "src/official-cli-v13-nested-spec-fixtures.test.ts", + "src/official-cli-v14-nested-spec-fixtures.test.ts", "src/spec-catalog.test.ts" ], "exclude": ["node_modules", "dist"] diff --git a/packages/core/tsconfig.workflow-contract-tests.json b/packages/core/tsconfig.workflow-contract-tests.json index d4d9f140..78eeeec4 100644 --- a/packages/core/tsconfig.workflow-contract-tests.json +++ b/packages/core/tsconfig.workflow-contract-tests.json @@ -2,15 +2,17 @@ "extends": "./tsconfig.json", "include": [ "src/cli-contracts/workflow.test.ts", - "src/official-cli-v13-validation-findings-fixtures.test.ts", - "src/official-cli-v13-validation-full-fixtures.test.ts", - "src/official-cli-v13-batch-status-fixtures.test.ts", - "src/official-cli-v13-show-diff-fixtures.test.ts", - "src/official-cli-v13-workflow-fixtures.test.ts", - "src/official-cli-v13-apply-readiness-fixtures.test.ts", - "src/official-cli-v13-nested-change-fixtures.test.ts", - "src/official-cli-v13-task-reading-fixtures.test.ts", - "src/official-cli-v12-boundary-fixtures.test.ts", + "src/official-cli-v14-validation-findings-fixtures.test.ts", + "src/official-cli-v14-validation-full-fixtures.test.ts", + "src/official-cli-v14-batch-status-fixtures.test.ts", + "src/official-cli-v14-show-fixtures.test.ts", + "src/official-cli-v14-workflow-fixtures.test.ts", + "src/official-cli-v14-apply-readiness-fixtures.test.ts", + "src/official-cli-v14-nested-change-fixtures.test.ts", + "src/official-cli-v14-task-reading-fixtures.test.ts", + "src/official-cli-v13-boundary-fixtures.test.ts", + "src/official-cli-v14-version-report-fixtures.test.ts", + "src/official-cli-v14-change-list-inventory-fixtures.test.ts", "src/opsx-kernel-schemas-root.fixtures.test.ts", "src/opsx-types.test.ts" ], diff --git a/packages/server/src/change-diff-evidence-service.test.ts b/packages/server/src/change-diff-evidence-service.test.ts index 76e1c439..cce3311d 100644 --- a/packages/server/src/change-diff-evidence-service.test.ts +++ b/packages/server/src/change-diff-evidence-service.test.ts @@ -1,13 +1,14 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Prove the capability-gated `show --diff` projection retains per-delta diff/warning and CLI provenance. - * 2. Prove retired (1.11/1.12), older, and unavailable-CLI sessions never construct the command (transport count 0). + * 2. Prove retired (1.12/1.13), older, and unavailable-CLI sessions never construct the command (transport count 0). * 3. Prove capability refusal, root loss, and command failure project as typed unavailability, not errors. * 4. Prove the root Store selector is preserved into the command invocation and display provenance. * * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import type { CliExecutor, RootContext } from '@openspecui/core' import { describe, expect, it, vi } from 'vitest' @@ -91,14 +92,14 @@ function depsFixture( } describe('readChangeDiffEvidence', () => { - it('projects MODIFIED delta diff and warning with CLI provenance for an admitted 1.13 session', async () => { + it('projects MODIFIED delta diff and warning with CLI provenance for an admitted 1.14 session', async () => { const deps = depsFixture({ kind: 'executed', result: commandResultFixture(successPayload()), }) const projection = await readChangeDiffEvidence( deps, - rootContextFixture({ version: '1.13.0' }), + rootContextFixture({ version: '1.14.0' }), 'add-search' ) @@ -135,7 +136,7 @@ describe('readChangeDiffEvidence', () => { const projection = await readChangeDiffEvidence( deps, rootContextFixture({ - version: '1.13.2', + version: '1.14.1', planningRoot: { path: '/store/repo', source: 'store' }, storeId: 'team-store', }), @@ -155,14 +156,14 @@ describe('readChangeDiffEvidence', () => { }) }) - it('never constructs the command on a retired 1.12 session and projects capability unavailability', async () => { + it('never constructs the command on a retired 1.13 session and projects capability unavailability', async () => { const deps = depsFixture({ kind: 'executed', result: commandResultFixture(successPayload()), }) const projection = await readChangeDiffEvidence( deps, - rootContextFixture({ version: '1.12.0' }), + rootContextFixture({ version: '1.13.0' }), 'add-search' ) @@ -170,7 +171,7 @@ describe('readChangeDiffEvidence', () => { expect(projection).toEqual({ kind: 'unavailable', reason: 'capability', - detectedVersion: '1.12.0', + detectedVersion: '1.13.0', }) }) @@ -206,7 +207,7 @@ describe('readChangeDiffEvidence', () => { }) const projection = await readChangeDiffEvidence( deps, - rootContextFixture({ version: '1.13.0', planningRoot: null }), + rootContextFixture({ version: '1.14.0', planningRoot: null }), 'add-search' ) @@ -218,14 +219,14 @@ describe('readChangeDiffEvidence', () => { const deps = depsFixture({ kind: 'unavailable', capability: 'requirementDiff' }) const projection = await readChangeDiffEvidence( deps, - rootContextFixture({ version: '1.13.0' }), + rootContextFixture({ version: '1.14.0' }), 'add-search' ) expect(projection).toEqual({ kind: 'unavailable', reason: 'capability', - detectedVersion: '1.13.0', + detectedVersion: '1.14.0', }) }) @@ -239,7 +240,7 @@ describe('readChangeDiffEvidence', () => { }) const projection = await readChangeDiffEvidence( deps, - rootContextFixture({ version: '1.13.0' }), + rootContextFixture({ version: '1.14.0' }), 'missing-change' ) @@ -264,7 +265,7 @@ describe('readChangeDiffEvidence', () => { }) const projection = await readChangeDiffEvidence( drift, - rootContextFixture({ version: '1.13.0' }), + rootContextFixture({ version: '1.14.0' }), 'add-search' ) @@ -286,7 +287,7 @@ describe('readChangeDiffEvidence', () => { }) const projection = await readChangeDiffEvidence( deps, - rootContextFixture({ version: '1.13.0' }), + rootContextFixture({ version: '1.14.0' }), 'add-search' ) diff --git a/packages/server/src/cli-validate-findings-router.test.ts b/packages/server/src/cli-validate-findings-router.test.ts index a13ac2a6..0283ceb8 100644 --- a/packages/server/src/cli-validate-findings-router.test.ts +++ b/packages/server/src/cli-validate-findings-router.test.ts @@ -1,7 +1,7 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Prove the public `cli.validate` findings member runs the OpenSpec 1.12+ - * `validate --report findings` transport (admitted 1.13 line) through the real Core + * `validate --report findings` transport (admitted 1.14 line) through the real Core * contract executor. * 2. Prove the typed findings document and the `invalid_validation_report_request` * status envelope both pass through the route boundary without rewriting. @@ -10,6 +10,7 @@ * * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { OpenSpecCliContractExecutor, type RootContext } from '@openspecui/core' import { isCliValidateFindings } from '@openspecui/core/openspec-compat' @@ -119,8 +120,8 @@ function createFindingsCallerFixture( } describe('cli.validate findings transport', () => { - it('runs the findings report through the real contract executor on an admitted 1.13 session', async () => { - const fixture = createFindingsCallerFixture('1.13.0', () => ({ + it('runs the findings report through the real contract executor on an admitted 1.14 session', async () => { + const fixture = createFindingsCallerFixture('1.14.0', () => ({ stdout: JSON.stringify(findingsPayload()), exitCode: 0, })) @@ -155,7 +156,7 @@ describe('cli.validate findings transport', () => { }) it('maps the archived findings scope onto the archived bulk flag', async () => { - const fixture = createFindingsCallerFixture('1.13.0', () => ({ + const fixture = createFindingsCallerFixture('1.14.0', () => ({ stdout: JSON.stringify(findingsPayload()), exitCode: 0, })) @@ -168,7 +169,7 @@ describe('cli.validate findings transport', () => { }) it('passes the typed request-error envelope through without rewriting it as a thrown error', async () => { - const fixture = createFindingsCallerFixture('1.13.0', () => ({ + const fixture = createFindingsCallerFixture('1.14.0', () => ({ stdout: JSON.stringify(requestErrorPayload()), exitCode: 1, })) @@ -189,7 +190,7 @@ describe('cli.validate findings transport', () => { }) it('refuses findings on a non-admitted CLI session before any argv is constructed', async () => { - const fixture = createFindingsCallerFixture('1.12.0', () => ({ + const fixture = createFindingsCallerFixture('1.13.0', () => ({ stdout: JSON.stringify(findingsPayload()), exitCode: 0, })) @@ -198,7 +199,7 @@ describe('cli.validate findings transport', () => { fixture.caller.cli.validate({ kind: 'findings', scope: 'changes' }) ).rejects.toMatchObject({ code: 'PRECONDITION_FAILED', - message: expect.stringContaining('>=1.13.0 <1.14.0'), + message: expect.stringContaining('>=1.14.0 <1.15.0'), }) // The findings report flag never reaches a spawned CLI process on a retired session. expect(fixture.executedArgv).toEqual([]) diff --git a/packages/server/src/root-context-cold-start.integration.test.ts b/packages/server/src/root-context-cold-start.integration.test.ts index 6e9a5f3c..91ec5236 100644 --- a/packages/server/src/root-context-cold-start.integration.test.ts +++ b/packages/server/src/root-context-cold-start.integration.test.ts @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Separate HTTP server readiness from pinned OpenSpec CLI root resolution. * 2. Exercise lifecycle-only WebSocket Push followed by typed HTTP Root Projection Pull. * 3. Preserve pinned CLI runner start/exit timing for timeout classification. @@ -17,6 +17,7 @@ * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" — the pin moved to v1.13.0. * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — in-window patch rotation moved the pin to v1.13.1. * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — in-window patch rotation moved the pin to v1.13.2. + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — the pin moved to v1.14.0. */ import { ConfigManager, @@ -40,7 +41,7 @@ import type { AppRouter, RunningServer } from './server.js' import { startServer } from './server.js' const execFileAsync = promisify(execFile) -const PINNED_OPENSPEC_COMMIT = 'db2309783547a14e150dbcbfc19120e4028446c3' +const PINNED_OPENSPEC_COMMIT = '94ca9c1eb15d1b49c06c988419b75c3d95f8b2b5' const PINNED_OPENSPEC_ROOT = resolve(import.meta.dirname, '../../../references/openspec') const CLI_BIN = resolve(PINNED_OPENSPEC_ROOT, 'bin/openspec.js') @@ -169,7 +170,7 @@ function rootStateReady(state: RootContextResolvedState): RootContextResolvedSta return state } -describe('pinned OpenSpec 1.13 Root Context cold start', () => { +describe('pinned OpenSpec 1.14 Root Context cold start', () => { afterEach(async () => { for (const client of wsClients.splice(0)) client.close() await Promise.all(runningServers.splice(0).map((server) => server.close())) @@ -265,12 +266,12 @@ describe('pinned OpenSpec 1.13 Root Context cold start', () => { expect(httpState.data).toMatchObject({ planningRoot: { path: planningRootPath, source: 'declared', store_id: 'plan-a' }, storeId: 'plan-a', - cli: { available: true, version: '1.13.2' }, + cli: { available: true, version: '1.14.0' }, }) expect(wsState.data).toMatchObject({ planningRoot: { path: planningRootPath, source: 'declared', store_id: 'plan-a' }, storeId: 'plan-a', - cli: { available: true, version: '1.13.2' }, + cli: { available: true, version: '1.14.0' }, }) expect(wsStates).toContain('ready') expect(wsNotices.every((notice) => !Object.hasOwn(Object(notice), 'data'))).toBe(true) diff --git a/packages/server/src/router.test.ts b/packages/server/src/router.test.ts index f2e457e2..3583b724 100644 --- a/packages/server/src/router.test.ts +++ b/packages/server/src/router.test.ts @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Prove public Router owner boundaries and dedicated Planning-root stream settlement. * 2. Prove strict Archive identity, generation, validation, diagnostics, and Store selection publicly. * 3. Prove reactive configuration, Dashboard Summary v2, Git, notification, and runtime procedures retain scoped behavior. @@ -27,6 +27,7 @@ * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — change-list nested/warnings projection (update-openspec-cli-1131 Slice 2). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { acquireWatcherRoot, @@ -3211,7 +3212,7 @@ apply: status: [], }, storeId: 'shared', - cli: { available: true, version: '1.13.0' }, + cli: { available: true, version: '1.14.0' }, } const caller = appRouter.createCaller(context) diff --git a/packages/web/scripts/w2-project-binding-playwright.ts b/packages/web/scripts/w2-project-binding-playwright.ts index f1578f4a..eb964118 100644 --- a/packages/web/scripts/w2-project-binding-playwright.ts +++ b/packages/web/scripts/w2-project-binding-playwright.ts @@ -1,7 +1,7 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Execute one deterministic same-origin Project Binding A-to-B acceptance flow. - * 2. Pin the OpenSpec 1.13 executable, Store registry scope, and disposable roots. + * 2. Pin the OpenSpec 1.14 executable, Store registry scope, and disposable roots. * 3. Assert desktop/mobile layout and browser error hygiene with bounded process-tree cleanup. * 4. Resolve repository, temporary, and isolated home paths through native Windows APIs. * 5. Hide fixture subprocess console windows (`windowsHide`) for uniform hidden-console execution on Windows. @@ -21,6 +21,8 @@ * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — pin + version assertion rotate to v1.13.2 (update-openspec-cli-1132). * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — in-window patch rotation moved the pin to v1.13.1. * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — in-window patch rotation moved the pin to v1.13.2. + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + * — pin + version assertion rotate to v1.14.0 (94ca9c1e) for the OpenSpecUI 14 adaptation line. */ import { execFile, spawn, type ChildProcess } from 'node:child_process' import { access, mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises' @@ -38,7 +40,7 @@ const TSX_CLI = join(REPO_ROOT, 'packages/web/node_modules/tsx/dist/cli.mjs') const VITE_CLI = join(REPO_ROOT, 'packages/web/node_modules/vite/bin/vite.js') const PINNED_OPENSPEC_ROOT = join(REPO_ROOT, 'references/openspec') const PINNED_OPENSPEC_BIN = join(PINNED_OPENSPEC_ROOT, 'bin/openspec.js') -const PINNED_OPENSPEC_COMMIT = 'db2309783547a14e150dbcbfc19120e4028446c3' +const PINNED_OPENSPEC_COMMIT = '94ca9c1eb15d1b49c06c988419b75c3d95f8b2b5' const BACKEND_PORT = 14_236 const WEB_PORT = 14_237 const STARTUP_TIMEOUT_MS = 30_000 @@ -130,7 +132,7 @@ async function assertPinnedCli(env: NodeJS.ProcessEnv): Promise { throw new Error(`Pinned OpenSpec SHA mismatch: ${stdout.trim()}`) } const version = await runPinnedCli(['--version'], REPO_ROOT, env) - if (version.exitCode !== 0 || version.stdout.trim() !== '1.13.2') { + if (version.exitCode !== 0 || version.stdout.trim() !== '1.14.0') { throw new Error(`Pinned OpenSpec version check failed: ${version.stdout}\n${version.stderr}`) } } diff --git a/packages/web/src/components/archived-validation-evidence.test.tsx b/packages/web/src/components/archived-validation-evidence.test.tsx index 85f9269c..ab34ea7a 100644 --- a/packages/web/src/components/archived-validation-evidence.test.tsx +++ b/packages/web/src/components/archived-validation-evidence.test.tsx @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Present the typed admitted-line archived-validation report with items, issues, totals, and root. * 2. Preserve CLI failure evidence without repair or automatic archive actions. * 3. Identify the evidence as unavailable in static snapshots instead of fabricating it. @@ -13,6 +13,7 @@ * Original request (2026-08-28): "使用移动端的 list-detail 思维……分成两栏,左侧 list,右侧详情。这种结构替代手风琴会更好" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { isStaticMode } from '@/lib/static-mode' import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' @@ -43,7 +44,7 @@ const rootActionStateMock = vi.hoisted(() => ({ message: '', evidence: [], context: { - cli: { available: true, version: '1.13.0' }, + cli: { available: true, version: '1.14.0' }, }, observedAt: 1, }, @@ -107,7 +108,7 @@ describe('ArchivedValidationEvidence', () => { message: '', evidence: [], context: { - cli: { available: true, version: '1.13.0' }, + cli: { available: true, version: '1.14.0' }, }, observedAt: 1, } diff --git a/packages/web/src/components/cli-health-gate.test.tsx b/packages/web/src/components/cli-health-gate.test.tsx index a745ea66..a583de8d 100644 --- a/packages/web/src/components/cli-health-gate.test.tsx +++ b/packages/web/src/components/cli-health-gate.test.tsx @@ -1,7 +1,7 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): - * 1. Lock the Web compatibility gate to the OpenSpecUI 13 / CLI 1.13 single-series line. - * 2. Prove incompatible-version blocking (retired v12 window, prereleases, next series) and + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Lock the Web compatibility gate to the OpenSpecUI 14 / CLI 1.14 single-series line. + * 2. Prove incompatible-version blocking (retired v13 window, prereleases, next series) and * the current-page-runtime escape hatch. * 3. Prove shared Root Context is the gate's only CLI availability truth and refresh is readonly. * 4. Prove the install hint names the admitted CLI series, never an unversioned @latest. @@ -17,6 +17,7 @@ * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import type { RootContext, RootContextState } from '@openspecui/core' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' @@ -133,7 +134,7 @@ function renderGate() { describe('CliHealthGate', () => { beforeEach(() => { - setAvailability({ available: true, version: '1.13.0' }) + setAvailability({ available: true, version: '1.14.0' }) config = undefined rootRefreshCalls = 0 }) @@ -142,8 +143,8 @@ describe('CliHealthGate', () => { cleanup() }) - it('does not render for the current recommended OpenSpec CLI 1.13.x line', async () => { - setAvailability({ available: true, version: '1.13.1' }) + it('does not render for the current recommended OpenSpec CLI 1.14.x line', async () => { + setAvailability({ available: true, version: '1.14.1' }) renderGate() await waitFor(() => { @@ -152,75 +153,75 @@ describe('CliHealthGate', () => { }) }) - it('blocks the retired OpenSpec CLI 1.11.x/1.12.x v12 window in OpenSpecUI 13', async () => { - setAvailability({ available: true, version: '1.12.0' }) + it('blocks the retired OpenSpec CLI 1.12.x/1.13.x v13 window in OpenSpecUI 14', async () => { + setAvailability({ available: true, version: '1.13.0' }) const retiredRuntime = renderGate() expect( - await screen.findByText(/OpenSpec CLI >=1\.13\.0 <1\.14\.0 Required/) + await screen.findByText(/OpenSpec CLI >=1\.14\.0 <1\.15\.0 Required/) ).toBeInTheDocument() - expect(screen.getByText(/Detected OpenSpec CLI 1\.12\.0/)).toBeInTheDocument() + expect(screen.getByText(/Detected OpenSpec CLI 1\.13\.0/)).toBeInTheDocument() retiredRuntime.unmount() cleanup() - setAvailability({ available: true, version: '1.11.0' }) + setAvailability({ available: true, version: '1.12.0' }) renderGate() expect( - await screen.findByText(/OpenSpec CLI >=1\.13\.0 <1\.14\.0 Required/) + await screen.findByText(/OpenSpec CLI >=1\.14\.0 <1\.15\.0 Required/) ).toBeInTheDocument() - expect(screen.getByText(/Detected OpenSpec CLI 1\.11\.0/)).toBeInTheDocument() + expect(screen.getByText(/Detected OpenSpec CLI 1\.12\.0/)).toBeInTheDocument() }) - it('blocks the retired OpenSpec CLI 1.9.x line in OpenSpecUI 13', async () => { + it('blocks the retired OpenSpec CLI 1.9.x line in OpenSpecUI 14', async () => { setAvailability({ available: true, version: '1.9.0' }) renderGate() expect( - await screen.findByText(/OpenSpec CLI >=1\.13\.0 <1\.14\.0 Required/) + await screen.findByText(/OpenSpec CLI >=1\.14\.0 <1\.15\.0 Required/) ).toBeInTheDocument() expect(screen.getByText(/Detected OpenSpec CLI 1\.9\.0/)).toBeInTheDocument() - expect(screen.getByText(/recommends >=1\.13\.0 <1\.14\.0/)).toBeInTheDocument() + expect(screen.getByText(/recommends >=1\.14\.0 <1\.15\.0/)).toBeInTheDocument() }) it('blocks prerelease versions inside the accepted range', async () => { - setAvailability({ available: true, version: '1.13.0-rc.1' }) + setAvailability({ available: true, version: '1.14.0-rc.1' }) renderGate() expect( - await screen.findByText(/OpenSpec CLI >=1\.13\.0 <1\.14\.0 Required/) + await screen.findByText(/OpenSpec CLI >=1\.14\.0 <1\.15\.0 Required/) ).toBeInTheDocument() - expect(screen.getByText(/Detected OpenSpec CLI 1\.13\.0-rc\.1/)).toBeInTheDocument() + expect(screen.getByText(/Detected OpenSpec CLI 1\.14\.0-rc\.1/)).toBeInTheDocument() }) - it('blocks OpenSpec CLI 1.14.0 and newer', async () => { - setAvailability({ available: true, version: '1.14.0' }) + it('blocks OpenSpec CLI 1.15.0 and newer', async () => { + setAvailability({ available: true, version: '1.15.0' }) renderGate() expect( - await screen.findByText(/OpenSpec CLI >=1\.13\.0 <1\.14\.0 Required/) + await screen.findByText(/OpenSpec CLI >=1\.14\.0 <1\.15\.0 Required/) ).toBeInTheDocument() - expect(screen.getByText(/Detected OpenSpec CLI 1\.14\.0/)).toBeInTheDocument() + expect(screen.getByText(/Detected OpenSpec CLI 1\.15\.0/)).toBeInTheDocument() }) it('recommends installing the admitted CLI series instead of an unversioned @latest', async () => { - setAvailability({ available: true, version: '1.12.0' }) + setAvailability({ available: true, version: '1.13.0' }) renderGate() expect( - await screen.findByText(/OpenSpec CLI >=1\.13\.0 <1\.14\.0 Required/) + await screen.findByText(/OpenSpec CLI >=1\.14\.0 <1\.15\.0 Required/) ).toBeInTheDocument() - expect(screen.getByText(/npm install -g @fission-ai\/openspec@1\.13/)).toBeInTheDocument() + expect(screen.getByText(/npm install -g @fission-ai\/openspec@1\.14/)).toBeInTheDocument() expect(screen.queryByText(/^npm install -g @fission-ai\/openspec$/)).not.toBeInTheDocument() }) it('offers a skip-version-check escape hatch when the CLI is available', async () => { - setAvailability({ available: true, version: '1.14.0' }) + setAvailability({ available: true, version: '1.15.0' }) renderGate() @@ -237,7 +238,7 @@ describe('CliHealthGate', () => { await waitFor(() => { expect( - screen.queryByText(/OpenSpec CLI >=1\.13\.0 <1\.14\.0 Required/) + screen.queryByText(/OpenSpec CLI >=1\.14\.0 <1\.15\.0 Required/) ).not.toBeInTheDocument() }) @@ -245,7 +246,7 @@ describe('CliHealthGate', () => { renderGate() expect( - await screen.findByText(/OpenSpec CLI >=1\.13\.0 <1\.14\.0 Required/) + await screen.findByText(/OpenSpec CLI >=1\.14\.0 <1\.15\.0 Required/) ).toBeInTheDocument() expect(screen.getByText(/Detected OpenSpec CLI 1\.9\.0/)).toBeInTheDocument() }) @@ -256,7 +257,7 @@ describe('CliHealthGate', () => { renderGate() expect( - await screen.findByText(/OpenSpec CLI >=1\.13\.0 <1\.14\.0 Required/) + await screen.findByText(/OpenSpec CLI >=1\.14\.0 <1\.15\.0 Required/) ).toBeInTheDocument() expect(screen.queryByText(/Skip version check/)).not.toBeInTheDocument() }) diff --git a/packages/web/src/components/cli-health-gate.tsx b/packages/web/src/components/cli-health-gate.tsx index f3a138e7..d7dedad2 100644 --- a/packages/web/src/components/cli-health-gate.tsx +++ b/packages/web/src/components/cli-health-gate.tsx @@ -1,11 +1,11 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Gate Web compatibility from the shared Root Context CLI evidence only. * 2. Preserve session-only bypass and execute-path repair controls. * 3. Revalidate the shared Root Context Work through readonly refresh after explicit repair. * 4. Recommend installing the admitted CLI series, never an out-of-range `@latest` (issue #258). - * 5. Derive all mismatch dialog copy from the shared compatibility constants so the v13 - * admitted single-series 1.13 window never forks from packages/core. + * 5. Derive all mismatch dialog copy from the shared compatibility constants so the v14 + * admitted single-series 1.14 window never forks from packages/core. * * Original request (2026-07-15): "CLI 1.6 compatibility gate." * Original request (2026-07-26): "最终计算结果本质是来自于 OpenSpec CLI 所提供的内容。" @@ -20,6 +20,7 @@ * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { isStaticMode } from '@/lib/static-mode' import { queryClient, trpc, trpcClient } from '@/lib/trpc' diff --git a/packages/web/src/components/settings/openspec-settings-diagnostics.tsx b/packages/web/src/components/settings/openspec-settings-diagnostics.tsx index a667396d..8da75778 100644 --- a/packages/web/src/components/settings/openspec-settings-diagnostics.tsx +++ b/packages/web/src/components/settings/openspec-settings-diagnostics.tsx @@ -1,9 +1,9 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Summarize read-only Root compatibility, selection, and failed-attempt state with links to Context. * 2. Summarize Environment Global profile, delivery, drift, and data scope with a link to Config. * 3. Preserve settled diagnostic facts during revalidation and use stable skeleton geometry on admission. - * 4. Label compatibility from the shared constants so the v13 single-series 1.13 law stays single-sourced. + * 4. Label compatibility from the shared constants so the v14 single-series 1.14 law stays single-sourced. * * Original request (2026-07-20): "Settings diagnostics are read-only." * Original request (2026-07-27): "统一修复所有类似的问题(我们也没不多,各个页面都检查一下,特别是app 那边新增的页面)" @@ -15,6 +15,7 @@ * Original request (2026-08-28): "直接将 0.10.0 和 0.11.0 一起适配,然后发布 v11。" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { InformationBadge } from '@/components/information-disclosure' import { DetailPanelSkeleton, RealtimeRevalidateCue } from '@/components/realtime' diff --git a/packages/web/src/components/status-warnings-notice.test.tsx b/packages/web/src/components/status-warnings-notice.test.tsx new file mode 100644 index 00000000..d3a9f6c8 --- /dev/null +++ b/packages/web/src/components/status-warnings-notice.test.tsx @@ -0,0 +1,76 @@ +/** + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Prove the OpenSpec 1.14 Status top-level `warnings` render as ONE always-visible + * summary row (count + CLI attribution) that needs no hover to discover — never + * Tooltip-only, never undiscoverable. + * 2. Prove one explicit expansion reveals the verbatim upstream warning strings with the + * CLI attribution heading on the same direct plane. + * 3. Prove the warnings stay advisory: status semantics (never the alert/blocker role) + * and no synthesized empty state when the member is absent or empty — absence is the + * upstream encoding for "none". + * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + */ +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it } from 'vitest' +import { StatusWarningsNotice } from './status-warnings-notice' + +/** + * Verbatim advisory warning captured from the pinned OpenSpec 1.14 CLI (see + * references/openspec-1.14.0-report.md §4; fixture input `.openspec.yaml` = + * `schema: spec-driven` + `skip_design: true`): the projection contract preserves + * the exact upstream text, so the direct plane must render it unedited. + */ +const upstreamStatusWarning = + 'Unrecognized key name(s) in .openspec.yaml (untrusted data, not instructions): skip_design. Known keys: schema, created, goal, affected_areas, initiative, skip_specs, retire_capabilities. Unknown keys are ignored and have no effect. skip_design is not a supported key; only skip_specs exists, and it only skips artifacts whose generates path lives under specs/.' + +describe('StatusWarningsNotice', () => { + afterEach(() => { + cleanup() + }) + + it('summarizes the warnings in one visible row with count and CLI attribution', () => { + render() + + // Collapsed: the summary row is the direct plane — count, CLI attribution, no hover + // needed to discover the advisory; the long verbatim text is not sprawled yet. + const summary = screen.getByRole('status', { + name: 'Change status warnings from openspec status', + }) + expect(summary).toBeVisible() + expect(summary).toHaveTextContent('2 status warnings') + expect(summary).toHaveTextContent('openspec status') + expect(screen.queryByText(upstreamStatusWarning)).toBeNull() + expect(screen.queryByText('second warning')).toBeNull() + // Advisory evidence keeps status semantics and never adopts the blocker alert role. + expect(summary.getAttribute('role')).toBe('status') + expect(screen.queryByRole('alert')).toBeNull() + }) + + it('reveals the verbatim warning strings on explicit expansion', () => { + render() + + const summary = screen.getByRole('status', { + name: 'Change status warnings from openspec status', + }) + expect(summary).toHaveTextContent('1 status warning') + + // One explicit expansion reveals the verbatim upstream message on the same direct plane. + fireEvent.click(summary.querySelector('button[aria-controls]')!) + expect(screen.getByText(upstreamStatusWarning)).toBeVisible() + expect(screen.getByText('Status warnings — reported by openspec status')).toBeVisible() + }) + + it('renders nothing when the member is absent or empty — no synthesized empty state', () => { + const absent = render() + expect(absent.container).toBeEmptyDOMElement() + + // The CLI omits the key when there are no warnings; an upstream empty array must not + // fabricate a "0 warnings" surface either. + const empty = render() + expect(empty.container).toBeEmptyDOMElement() + expect( + screen.queryByRole('status', { name: 'Change status warnings from openspec status' }) + ).toBeNull() + }) +}) diff --git a/packages/web/src/components/status-warnings-notice.tsx b/packages/web/src/components/status-warnings-notice.tsx new file mode 100644 index 00000000..2f3a7f49 --- /dev/null +++ b/packages/web/src/components/status-warnings-notice.tsx @@ -0,0 +1,93 @@ +/** + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Render the OpenSpec 1.14 Status top-level `warnings` advisory array (unrecognized + * `.openspec.yaml` keys) as ONE always-visible summary row: count plus CLI attribution, + * discoverable without hover, so advisory evidence never hides behind a Tooltip. + * 2. Keep the verbatim upstream warning strings one explicit expansion away on the same + * direct plane, under a heading that names the owning CLI command. + * 3. Keep the warnings advisory: amber advisory tone and `role="status"` semantics, never + * the destructive blocker presentation, and never a gate on any action. + * 4. Degrade to nothing when the member is absent or empty — absence is the upstream + * encoding for "none", so no empty state is ever synthesized. + * 5. Mirror the v13 Apply guidance summary-row pattern (apply-progress-notice.tsx) as a + * parallel component: same DOM shape, roles, tone classes, and aria disclosure + * interaction, without refactoring the v13 notice's composite summary/detail surface. + * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + */ +import { AlertTriangle, ChevronDown, ChevronRight } from 'lucide-react' +import { useState } from 'react' + +/** Upstream command that owns every fact rendered by this notice. */ +const STATUS_COMMAND_ATTRIBUTION = 'openspec status' + +const STATUS_WARNINGS_DETAIL_ID = 'change-status-warnings-detail' + +/** + * Render the OpenSpec 1.14 Status `warnings` as one collapsible summary row on the Change + * Detail direct status plane: the row itself names the warning count and the owning CLI + * command (discovery needs no hover), and expanding reveals the verbatim upstream strings + * on the same plane. Returns null when the member is absent or empty — the upstream + * encoding for "none" — so no empty state is ever synthesized. The warnings are advisory + * evidence only and never gate any action. + */ +export function StatusWarningsNotice({ + warnings, +}: { + /** OpenSpec 1.14 additive member: verbatim upstream warning strings, absent when none. */ + warnings?: readonly string[] +}) { + const count = warnings?.length ?? 0 + const [expanded, setExpanded] = useState(false) + if (count === 0) return null + + return ( + <> +
+
+ {expanded ? ( +
+
+ Status warnings — reported by {STATUS_COMMAND_ATTRIBUTION} +
+
    + {warnings?.map((warning) => ( +
  • + {warning} +
  • + ))} +
+
+ ) : null} + + ) +} diff --git a/packages/web/src/components/validation-findings-evidence.test.tsx b/packages/web/src/components/validation-findings-evidence.test.tsx index 33ae8502..70996865 100644 --- a/packages/web/src/components/validation-findings-evidence.test.tsx +++ b/packages/web/src/components/validation-findings-evidence.test.tsx @@ -1,6 +1,6 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): - * 1. Present the typed 1.12+ findings document (admitted 1.13 line): INFO as a distinct + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Present the typed 1.12+ findings document (admitted 1.14 line): INFO as a distinct * informational class, `returnedItems` beside preserved full-run totals, CLI provenance, * filtered labeling. * 2. Attribute findings to their owning change: the current change's entries are the @@ -17,6 +17,7 @@ * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Owner walkthrough correction (2026-09-04): findings must attribute their owning change; the Evidence detail panel styling follows the vision review. * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { isStaticMode } from '@/lib/static-mode' import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' @@ -47,7 +48,7 @@ const rootActionStateMock = vi.hoisted(() => ({ message: '', evidence: [], context: { - cli: { available: true, version: '1.13.0' }, + cli: { available: true, version: '1.14.0' }, }, observedAt: 1, }, @@ -123,7 +124,7 @@ describe('ValidationFindingsEvidence', () => { message: '', evidence: [], context: { - cli: { available: true, version: '1.13.0' }, + cli: { available: true, version: '1.14.0' }, }, observedAt: 1, } @@ -405,7 +406,7 @@ describe('ValidationFindingsEvidence', () => { it('offers no findings action on a retired CLI session', () => { rootActionStateMock.state = { ...rootActionStateMock.state, - context: { cli: { available: true, version: '1.12.0' } }, + context: { cli: { available: true, version: '1.13.0' } }, } render() @@ -413,7 +414,7 @@ describe('ValidationFindingsEvidence', () => { expect( screen.getByText(/Validation findings require the admitted OpenSpec CLI line/) ).toBeVisible() - expect(screen.getByText(/detected 1\.12\.0/)).toBeVisible() + expect(screen.getByText(/detected 1\.13\.0/)).toBeVisible() expect( screen.queryByRole('button', { name: 'Load validation findings' }) ).not.toBeInTheDocument() @@ -472,7 +473,7 @@ describe('ValidationFindingsEvidence', () => { const retiredChip = vi.fn() rootActionStateMock.state = { ...rootActionStateMock.state, - context: { cli: { available: true, version: '1.12.0' } }, + context: { cli: { available: true, version: '1.13.0' } }, } render() expect(retiredChip).toHaveBeenLastCalledWith({ label: 'unavailable', tone: 'unavailable' }) diff --git a/packages/web/src/lib/use-cli-runner.tsx b/packages/web/src/lib/use-cli-runner.tsx index c2c15d61..9f900602 100644 --- a/packages/web/src/lib/use-cli-runner.tsx +++ b/packages/web/src/lib/use-cli-runner.tsx @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-03 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Run ordered CLI streams with stable process/loading state across dedicated Root and Agent transports. * 2. Preserve stdout, stderr, exit, cancellation, and multiline diagnostics verbatim. * 3. Route root-dependent operations through dedicated Server-owned transports. @@ -16,6 +16,7 @@ * Server-owned stream, which installs the admitted series instead of an out-of-range @latest. * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import '@/styles/terminal-effects.css' import type { CliStreamEvent } from '@openspecui/core' diff --git a/packages/web/src/routes/change-view-status-warnings.test.tsx b/packages/web/src/routes/change-view-status-warnings.test.tsx new file mode 100644 index 00000000..6fe33200 --- /dev/null +++ b/packages/web/src/routes/change-view-status-warnings.test.tsx @@ -0,0 +1,297 @@ +/** + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Verify the OpenSpec 1.14 Status top-level `warnings` reach the Change Detail direct + * status plane: the advisory summary row mounts the status region on its own — without + * any other direct-status condition — carrying the CLI count and attribution. + * 2. Verify status warnings never gate actions: with current Root and Status authority the + * command bar stays enabled while the advisory is visible. + * 3. Verify the route-level expansion reveals the verbatim upstream warning strings with + * the CLI attribution heading on the same direct plane. + * + * This file is deliberately separate from change-view.test.tsx: the status-warnings route + * case owns its own fixture projection while the shared file rotates admitted CLI fixture + * literals in parallel work. + * + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + */ +import type { RootActionState } from '@/lib/use-root-action-state' +import type { ChangeStatus } from '@openspecui/core' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import type { ComponentProps, ReactNode } from 'react' +import { createContext } from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { ChangeView } from './change-view' + +const statusMock = vi.hoisted(() => vi.fn()) +const applyInstructionsMock = vi.hoisted(() => vi.fn()) +const changeFilesMock = vi.hoisted(() => vi.fn()) +const changesSubscriptionMock = vi.hoisted(() => + vi.fn(() => ({ data: undefined, isLoading: false, error: null })) +) +const rootActionMock = vi.hoisted(() => vi.fn()) +const openArchiveModalMock = vi.hoisted(() => vi.fn()) + +const diffEvidenceQueryMock = vi.hoisted(() => + vi.fn((..._args: unknown[]): Promise => Promise.reject(new Error('not under test'))) +) + +vi.mock('@/lib/trpc', () => ({ + trpcClient: { + change: { + diffEvidence: { + query: (...args: unknown[]) => diffEvidenceQueryMock(...args), + }, + }, + }, +})) + +vi.mock('@/lib/use-opsx', () => ({ + useOpsxApplyInstructionsSubscription: applyInstructionsMock, + useOpsxStatusSubscription: (...args: unknown[]) => { + const state = statusMock(...args) + return { + authority: state.error ? { state: 'failed', error: state.error } : { state: 'current' }, + refresh: vi.fn(), + refreshPending: false, + ...state, + } + }, +})) + +vi.mock('@/lib/use-subscription', () => ({ + useChangeFilesSubscription: changeFilesMock, + useChangesSubscription: changesSubscriptionMock, +})) + +vi.mock('@/lib/use-root-action-state', () => ({ + useRootActionState: () => rootActionMock(), +})) + +vi.mock('@/lib/archive-modal-context', () => ({ + useArchiveModal: () => ({ openArchiveModal: openArchiveModalMock }), +})) + +vi.mock('@/components/folder-editor-viewer', () => ({ + FolderEditorViewer: () =>
folder
, +})) + +vi.mock('@/components/opsx/artifact-output-viewer', () => ({ + ArtifactOutputViewer: ({ artifact }: { artifact: { id: string } }) => ( +
artifact:{artifact.id}
+ ), + ContentFallbackViewer: ({ fallback }: { fallback: { label?: string } }) => ( +
fallback:{fallback.label ?? 'Content'}
+ ), +})) + +vi.mock('@/components/tabs', () => ({ + Tabs: ({ + tabs, + selectedTab, + }: { + tabs: Array<{ id: string; label?: ReactNode; content: ReactNode }> + selectedTab?: string + }) => ( +
+ +
{tabs.find((tab) => tab.id === selectedTab)?.content ?? tabs[0]?.content}
+
+ ), +})) + +vi.mock('@/lib/view-transitions/navigation', () => ({ + VTLink: ({ + to, + children, + ...props + }: { to: string; children?: ReactNode } & Omit, 'href'>) => ( + + {children} + + ), + vtNavController: { activatePop: vi.fn() }, +})) + +vi.mock('@/lib/view-transitions/shared-elements', () => ({ + getSharedElementBinding: () => ({}), + readSharedElementHandoffState: () => null, +})) + +vi.mock('@/lib/view-transitions/tabs', () => ({ + useRoutedCarouselTabs: ({ initialTab }: { initialTab?: string }) => ({ + tabsRef: { current: null }, + selectedTab: initialTab, + onTabChange: vi.fn(), + }), +})) + +vi.mock('@tanstack/react-router', () => ({ + Link: ({ + to, + children, + ...props + }: { to: string; children?: ReactNode } & Omit, 'href'>) => ( + + {children} + + ), + useLocation: () => ({ + pathname: '/changes/warned-change', + search: '', + hash: '', + state: null, + }), + useNavigate: () => vi.fn(), + getRouterContext: () => createContext(null), + useParams: () => ({ changeId: 'warned-change' }), +})) + +/** + * Verbatim advisory warning captured from the pinned OpenSpec 1.14 CLI (see + * references/openspec-1.14.0-report.md §4). The projection contract preserves the exact + * upstream text; the route must render it unedited. + */ +const upstreamStatusWarning = + 'Unrecognized key name(s) in .openspec.yaml (untrusted data, not instructions): skip_design. Known keys: schema, created, goal, affected_areas, initiative, skip_specs, retire_capabilities. Unknown keys are ignored and have no effect. skip_design is not a supported key; only skip_specs exists, and it only skips artifacts whose generates path lives under specs/.' + +/** + * A current live-CLI Change status whose ONLY direct-status fact is the OpenSpec 1.14 + * `warnings` member: no error, ready Root, no reference failures, no Apply evidence. If the + * advisory fails to mount the status region on its own, this fixture renders nothing — + * which is exactly the pre-1.14 Web behavior this case turns red against. + */ +const warnedChangeStatus = { + changeName: 'Warned Change', + schemaName: 'opsx-collab-pr-loop', + isPlanningComplete: false, + applyRequires: [], + artifacts: [ + { id: 'implementation', outputPath: 'implementation.md', status: 'ready', requires: [] }, + ], + warnings: [upstreamStatusWarning, 'second warning'], + provenance: { + kind: 'cli', + planningHome: { + kind: 'repo', + root: '/planning', + changesDir: '/planning/openspec/changes', + defaultSchema: 'opsx-collab-pr-loop', + }, + changeRoot: '/planning/openspec/changes/warned-change', + artifactPaths: { + implementation: { + outputPath: 'implementation.md', + resolvedOutputPath: '/planning/openspec/changes/warned-change/implementation.md', + existingOutputPaths: [], + }, + }, + nextSteps: ['Apply the change.'], + actionContext: { + mode: 'repo-local', + sourceOfTruth: 'repo', + planningArtifacts: ['implementation'], + linkedContext: [], + allowedEditRoots: ['/planning'], + requiresAffectedAreaSelection: false, + constraints: [], + }, + root: { path: '/planning', source: 'nearest' }, + evidence: { + command: 'status', + success: true, + stdout: '{"changeName":"warned-change","warnings":["…"]}', + stderr: '', + exitCode: 0, + payload: { changeName: 'warned-change' }, + diagnostics: [], + selector: {}, + }, + }, +} satisfies ChangeStatus + +const readyRootAction: RootActionState = { + status: 'ready', + disabled: false, + context: null, + observedAt: 1, + title: null, + message: null, + evidence: [], +} + +describe('ChangeView OpenSpec 1.14 status warnings', () => { + afterEach(() => { + cleanup() + vi.unstubAllGlobals() + }) + + beforeEach(() => { + statusMock.mockReset().mockReturnValue({ + data: warnedChangeStatus, + isLoading: false, + error: null, + }) + applyInstructionsMock.mockReset().mockReturnValue({ data: undefined }) + changeFilesMock.mockReset().mockReturnValue({ + data: [{ path: 'notes/decision.md', type: 'file', content: '# Decision' }], + isLoading: false, + error: null, + }) + changesSubscriptionMock.mockReset().mockReturnValue({ + data: undefined, + isLoading: false, + error: null, + }) + rootActionMock.mockReset().mockReturnValue(readyRootAction) + openArchiveModalMock.mockReset() + }) + + it('mounts the status region from warnings alone, ungated, with verbatim expansion', () => { + render() + + // Direct reach: the advisory summary row mounts the status region by itself — count + // and CLI attribution visible with no hover — and never adopts the blocker alert role. + const summary = screen.getByRole('status', { + name: 'Change status warnings from openspec status', + }) + expect(summary).toBeVisible() + expect(summary).toHaveTextContent('2 status warnings') + expect(summary).toHaveTextContent('openspec status') + expect(screen.queryByRole('alert')).toBeNull() + + // Advisory evidence never gates actions: current Root + current Status keep the + // command bar enabled while the warnings are visible. + expect(screen.getByRole('button', { name: 'Update' })).toBeEnabled() + expect(screen.getByRole('button', { name: 'Verify' })).toBeEnabled() + + // One explicit expansion reveals the verbatim upstream strings on the same plane. + fireEvent.click(summary.querySelector('button[aria-controls]')!) + expect(screen.getByText(upstreamStatusWarning)).toBeVisible() + expect(screen.getByText('second warning')).toBeVisible() + expect(screen.getByText('Status warnings — reported by openspec status')).toBeVisible() + }) + + it('keeps the pre-1.14 presentation when the status carries no warnings member', () => { + const { warnings: _warnings, ...cleanChangeStatus } = warnedChangeStatus + statusMock.mockReturnValue({ + data: cleanChangeStatus, + isLoading: false, + error: null, + }) + + render() + + // Absent member = the upstream encoding for "none": no synthesized empty state and no + // status region mounted from warnings alone (this payload has no other direct fact). + expect( + screen.queryByRole('status', { name: 'Change status warnings from openspec status' }) + ).toBeNull() + expect(screen.getByText('Warned Change')).toBeTruthy() + }) +}) diff --git a/packages/web/src/routes/change-view.test.tsx b/packages/web/src/routes/change-view.test.tsx index c262c5b3..57f655bf 100644 --- a/packages/web/src/routes/change-view.test.tsx +++ b/packages/web/src/routes/change-view.test.tsx @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Verify change detail fallbacks and schema-driven artifact rendering. * 2. Verify retained errors and non-current authority lock actions without entering the Header. * 3. Verify Apply inputs remain separate and open from a Header Action Dialog. @@ -22,6 +22,7 @@ * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — tracking-evidence direct-plane mounting (update-openspec-cli-1132 Slice 2). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import type { RootActionState } from '@/lib/use-root-action-state' import type { ChangeStatus } from '@openspecui/core' @@ -1037,10 +1038,10 @@ describe('ChangeView', () => { launchProject: { path: '/tmp/project' }, planningRoot: { path: '/tmp/project', source: 'nearest', healthy: true, status: [] }, storeId: null, - // Admitted-line CLI for the v13 window (>=1.13.0 <1.14.0): the diff-evidence - // capability gate only fetches on the admitted series, so a retired 1.12 fixture + // Admitted-line CLI for the v14 window (>=1.14.0 <1.15.0): the diff-evidence + // capability gate only fetches on the admitted series, so a retired 1.13 fixture // would leave the transport untouched and time out this test. - cli: { available: true, version: '1.13.0' }, + cli: { available: true, version: '1.14.0' }, }, }) statusMock.mockReturnValue({ diff --git a/packages/web/src/routes/change-view.tsx b/packages/web/src/routes/change-view.tsx index 1cb9acfa..03498844 100644 --- a/packages/web/src/routes/change-view.tsx +++ b/packages/web/src/routes/change-view.tsx @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Render schema-aware change artifacts and source files while retaining terminal status errors. * 2. Dispatch change workflows through routed compose/verify surfaces and the shared Operator launcher. * 3. Lock every change workflow action behind current Root Context and Status projection authority. @@ -10,6 +10,10 @@ * 8. Mount OpenSpec 1.13.2 Apply tracking evidence on the same plane: unreadable tracking * files and no-tracking schemas (`taskTrackingConfigured === false`) mount the status * region and reach the notice, while absent members keep the pre-1.13.2 presentation. + * 9. Mount the OpenSpec 1.14 Status top-level `warnings` advisory (unrecognized + * `.openspec.yaml` keys) on the same plane: the advisory summary row mounts the status + * region on its own, stays amber advisory evidence (`role="status"`), never gates + * actions, and absent members keep the pre-1.14 presentation exactly. * * Original request (2026-07-15): "Root-dependent actions remain locked until root selection succeeds." * Review request (2026-07-23): "代码已经提交,开始review。如果有问题,那么可更新change。" @@ -29,6 +33,7 @@ * direct-plane evidence beside the existing divergence notice. * Original request (2026-09-26): "Openspec 1.13.2 释放了…" — tracking-evidence direct-plane mounting (update-openspec-cli-1132 Slice 2). * Original request (2026-09-27): Owner walkthrough P3 adjudication — badge receives taskTrackingConfigured. + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" */ import { ApplyProgressNotice } from '@/components/apply-progress-notice' import { @@ -41,6 +46,7 @@ import { ChangeCommandBar } from '@/components/opsx/change-command-bar' import { OperationInputsDialogAction } from '@/components/opsx/operation-inputs' import { OpsxEntityDetailView } from '@/components/opsx/opsx-entity-detail-view' import { RootActionNotice } from '@/components/root-action-notice' +import { StatusWarningsNotice } from '@/components/status-warnings-notice' import { changeDisplayTitle } from '@/lib/change-display-title' import { buildOpsxComposeHref, type OpsxComposeActionId } from '@/lib/opsx-compose' import { useChangeOperatorLauncher } from '@/lib/use-change-operator-launcher' @@ -167,7 +173,11 @@ export function ChangeView() { // tasks must reach this plane — otherwise the member-only-`false` payload mounts // nothing while presenting its empty task list. (applyInstructions?.unavailableTrackingFiles?.length ?? 0) > 0 || - applyInstructions?.taskTrackingConfigured === false + applyInstructions?.taskTrackingConfigured === false || + // OpenSpec 1.14 Status advisory: unrecognized `.openspec.yaml` keys are CLI-owned + // verbatim evidence that must stay scannable on the direct plane even when planning + // facts, references, and Apply state are all settled — it never gates actions. + (status?.warnings?.length ?? 0) > 0 return ( + {applyInstructions ? ( =20.19.0'} hasBin: true + '@fission-ai/openspec@1.14.0': + resolution: {integrity: sha512-V+zitRK918I6B3EIcYL3gVYoPoPgzknSxE1zL0zUQdbd4NsO5kbrxU5jVceuNNEznD94pbRPCvpCDIa6BeEaPA==} + engines: {node: '>=20.19.0'} + hasBin: true + '@fission-ai/openspec@1.4.0': resolution: {integrity: sha512-AjOoLVoBjBCyYZ1gUm5GzO5WAsjiF/V1pOSZuGzKVidSI0Xva2SkFYwtQJ4/Rp4rd7gTRhgDoh4Th3uScwflOg==} engines: {node: '>=20.19.0'} @@ -8938,6 +8946,21 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@fission-ai/openspec@1.14.0(@types/node@22.19.1)': + dependencies: + '@inquirer/core': 12.0.1(@types/node@22.19.1) + '@inquirer/prompts': 8.7.0(@types/node@22.19.1) + chalk: 5.6.2 + commander: 14.0.3 + cross-spawn: 7.0.6 + diff: 9.0.0 + fast-glob: 3.3.3 + ora: 9.4.1 + yaml: 2.9.0 + zod: 4.4.3 + transitivePeerDependencies: + - '@types/node' + '@fission-ai/openspec@1.4.0(@types/node@22.19.1)': dependencies: '@inquirer/core': 10.3.2(@types/node@22.19.1) diff --git a/scripts/prepare-openspec-reference.mjs b/scripts/prepare-openspec-reference.mjs index 1c8793c1..8b5e76ea 100644 --- a/scripts/prepare-openspec-reference.mjs +++ b/scripts/prepare-openspec-reference.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node /** - * Orthogonal intents (updated 2026-09-26 Asia/Shanghai): - * 1. Initialize the pinned OpenSpec 1.13 reference submodule for clean CI checkouts. + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Initialize the pinned OpenSpec 1.14 reference submodule for clean CI checkouts. * 2. Build the ignored CLI distribution consumed by pinned integration fixtures. * 3. Reject submodule drift before any fixture can execute a different upstream revision. * 4. Invoke pnpm through a Windows-safe executable or quoted command-shim boundary. @@ -21,6 +21,9 @@ * v1.13.1 patch tag (634c557) inside the unchanged v13 window (in-window patch rotation). * Original request (2026-09-26): rotate the reference pin from v1.13.1 (634c557) to the released * v1.13.2 patch tag (db230978) inside the unchanged v13 window (in-window patch rotation). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" + * — rotate the reference pin from v1.13.2 (db230978) to the released v1.14.0 tag (94ca9c1e) + * for the OpenSpecUI 14 adaptation line. */ import { execFileSync } from 'node:child_process' import { existsSync } from 'node:fs' @@ -30,7 +33,7 @@ import { resolvePnpmInvocation } from './lib/pnpm-invocation.mjs' const REPOSITORY_ROOT = process.cwd() const REFERENCE_PATH = resolve(REPOSITORY_ROOT, 'references/openspec') -const EXPECTED_COMMIT = 'db2309783547a14e150dbcbfc19120e4028446c3' +const EXPECTED_COMMIT = '94ca9c1eb15d1b49c06c988419b75c3d95f8b2b5' const CLI_DIST_PATH = resolve(REFERENCE_PATH, 'dist/cli/index.js') function run(command, args, options = {}) { From c6a0b783393caa0cb33e4fa6109e2b2482617793 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 2 Oct 2026 04:44:59 +0800 Subject: [PATCH 09/12] docs: publish the v14 line across READMEs, changeset, and governance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 7 of the target-openspec-cli-114-line change plus full-gate residue: - Changeset major x4 (core/server/web/openspecui) describing the 1.14 line. - Repository README (en/zh): @^14 row, 1.15 non-pre-claim, v14 feature prose, @1.14 upgrade command, v13 archived snapshots; CLI package README scoped to the v14 release line (release README law). - CLAUDE.md session pointer to openspec-1.14.0-report.md; AGENTS.md v14 architecture decision and submodule pin rotation. - Full-gate residue: two capability-gated suites with hand-written version fixtures were outside every slice list and still modeled the v13 window — opsx-kernel-cli-projection.test.ts (batch/findings transport fell back to serial spawns, 7 real failures) and web change-diff-evidence.test.tsx (admitted 1.13.0 fixture rejected, 7 failures). Both rotate to '1.14.0' admitted / '1.13.0' retired. Gates: format:check, lint:ci, typecheck, test:ci green after the rotation (first test:ci run also proved the pnpm -r recursion masks later packages after a core failure — full re-run recorded). --- .changeset/openspec-114-line-adaptation.md | 8 + AGENTS.md | 3 +- CLAUDE.md | 2 +- README-1.13.0.md | 223 ++++++++++++++++++ README-zh-1.13.0.md | 209 ++++++++++++++++ README-zh.md | 35 +-- README.md | 46 ++-- .../loop/checkpoints.md | 2 +- .../loop/implementation.md | 18 ++ packages/cli/README.md | 15 +- .../src/opsx-kernel-cli-projection.test.ts | 57 ++--- .../components/change-diff-evidence.test.tsx | 15 +- 12 files changed, 553 insertions(+), 80 deletions(-) create mode 100644 .changeset/openspec-114-line-adaptation.md create mode 100644 README-1.13.0.md create mode 100644 README-zh-1.13.0.md diff --git a/.changeset/openspec-114-line-adaptation.md b/.changeset/openspec-114-line-adaptation.md new file mode 100644 index 00000000..0f4b6e8b --- /dev/null +++ b/.changeset/openspec-114-line-adaptation.md @@ -0,0 +1,8 @@ +--- +'@openspecui/core': major +'@openspecui/server': major +'@openspecui/web': major +'openspecui': major +--- + +OpenSpecUI 14: adapt to OpenSpec CLI 1.14.x as a new single-series line (`>=1.14.0 <1.15.0`); the previous 1.13 v13 window, prereleases, `>=1.15.0`, and unparseable versions are blocked by default. Six 1.14 JSON surfaces are typed end-to-end: the `version` command envelope (no network probing), Apply task `sourcePath`/`line` source locations, per-entry `archived` from `list --archived`/`--all`, `status` advisory warnings (single and batch shapes) preserved verbatim through the kernel projection and rendered on the Change Detail direct plane as a collapsed summary row that never gates actions, `show --type spec --json` requirement/scenario names, and store-backed `allowedEditRoots` two-branch action context. The Agent delivery registry rotates to the 1.14 series with ten new tools — amp and gsd join the shared `.agents` skills root (five-writer arbitration; antigravity stays commands-only) alongside atomcode, codestudio, dsh, easycode, gigacode, grok, veai, warp, and IBM Bob — the pinned generator baseline moves to 1.14.0 with series-aware staleness, and the pinned executable fixture matrix rotates to `@fission-ai/openspec@1.14.0` with the retained 1.13.2 executable proving below-admitted boundary rejections and the 1.14-only members absent. diff --git a/AGENTS.md b/AGENTS.md index 6a027db2..4b67f3cc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,6 +117,7 @@ Architecture decision (2026-09-03): OpenSpecUI 12 supports OpenSpec CLI `>=1.12. Architecture decision (2026-09-12): OpenSpecUI 13 supports OpenSpec CLI `>=1.13.0 <1.14.0` as a single-series window — stable 1.13.x is current/recommended; `1.14` is not pre-claimed, and its admission is a separately verified decision (13.x widening or a new major). CLI `<1.13.0` (including the whole 1.12 v12 window), prereleases, and `>=1.14.0` are blocked by default. The new-major path (rather than a 12.x bridge) follows the one-line-per-series cadence and keeps the generator staleness baseline unconditional per line. `instructions apply --json` adds optional `missingPrerequisites` (build-order closure, present even when ready) and `warnings` (the no-delta-specs advisory predicting a validate failure) as typed contract facts projected end-to-end (CLI contract, Core input/projection schemas, Server transport, direct-plane Change Detail presentation with readable build-order evidence); both stay optional with keys absent upstream when empty — never null or synthesized empty arrays — and neither gates apply state or CLI progress authority. The Agent registry rotates its series to `'1.13'` with every 1.12 physical fact carried forward (no new tools upstream; SourceCraft's `minCliSeries: '1.12'` stays a provenance fact), and generator staleness rotates to the 1.13.0 baseline with the historical provenance union retained. Fixture roles rotate: pinned `openspec-cli-113` proves accepted contracts across the full positive matrix; retained `openspec-cli-112` proves below-admitted boundary rejections only. Evidence map: `references/openspec-1.13.0-report.md` (pin `v1.13.0`, `9d4e5974`). Architecture decision (2026-09-17): OpenSpec CLI 1.13.1 is an in-window patch rotation for the unchanged `>=1.13.0 <1.14.0` v13 window — no new command, flag, or capability moves the compat gate, Agent registry series, or series-aware staleness. `list --json` gains per-entry `nested?: string[]` and top-level `warnings` (absent-when-empty; `code` typed open-string): the kernel `opsx-change-list` projection keeps `entries` and compat `value` actionable-only, carries a required structurally-derived `namespaces` set (entry names bearing `nested`; warnings are display evidence and message text is never parsed), and every change-row builder subtracts that set at its own id source (Changes projection, Dashboard inputs, search documents, Store content decode, and the `change.listWithMeta`/`change.subscribe` transports); CLI-list loss degrades to today's local-row behavior and never gates row visibility. Local task-line reading and `toggleMarkdownTask` mirror the CLI 1.13.1 pattern verbatim (all CommonMark + ordered markers, indentation, single-token markers incl. padded `[ x]`=done, whitespace-only boxes, link-continuation guard, CRLF tolerance) while CLI progress authority stays untouched. Pinned fixture rotation: `openspec-cli-113` = 1.13.1 (positive line), `openspec-cli-112` = 1.12.0 (boundary line); the reference pin guard and every `PINNED_OPENSPEC_COMMIT` constant rotate together. Evidence map: `references/openspec-1.13.1-report.md` (pin `v1.13.1`, `634c557`). Architecture decision (2026-09-26): OpenSpec CLI 1.13.2 is an in-window patch rotation for the unchanged `>=1.13.0 <1.14.0` v13 window — no new command, flag, or capability moves the compat gate, Agent registry series, or series-aware staleness. `instructions apply --json` adds two optional tracking-evidence members with absent-when-empty honesty: `taskTrackingConfigured` is a boolean whenever emitted (true for any non-null `apply.tracks`, even a glob matching zero files; absence means a pre-1.13.2 CLI, never a synthesized `false`) and `unavailableTrackingFiles` carries `{path, reason}` for every matched-but-unreadable tracking file (never `[]`); both are non-gating evidence projected end-to-end onto the Change Detail status region, apply aggregation resolves `apply.tracks` with glob semantics across every matched file, and `all_done` additionally requires zero unavailable files. The Kilo Code registry entry rotates its command delivery to `.kilo/command/opsx-.md` with the former `.kilocode/workflows/` path as legacy evidence plus two-generation cleanup (`opsx-*.md` and the three `openspec-*.md` files). Artifact-glob recognition parity (brace/extglob) is watcher-granularity only (`isGlobPattern`/dependency watching); `opsxPathMatchesPattern` tracked-task matching stays wildcard-class as a documented boundary. Pinned fixture roles unchanged: `openspec-cli-113` = 1.13.2 (positive line), `openspec-cli-112` = 1.12.0 (boundary line). Evidence map: `references/openspec-1.13.2-report.md` (pin `v1.13.2`, `db23097`). +Architecture decision (2026-10-02): OpenSpecUI 14 supports OpenSpec CLI `>=1.14.0 <1.15.0` as a single-series window — stable 1.14.x is current/recommended; `1.15` is not pre-claimed, and its admission is a separately verified decision (14.x widening or a new major). CLI `<1.14.0` (including the whole 1.13 v13 window), prereleases, and `>=1.15.0` are blocked by default. Six 1.14 JSON surfaces become typed contract facts projected end-to-end: `openspec version [--check] [--json]` (schemaVersion-1 envelope, no network probing), `instructions apply` task `sourcePath`/`line` (present whenever tasks exist), `list --archived`/`--all` per-entry `archived` (the one new list member; `nested`/list `warnings` are 1.13.1 carry-forward facts whose fixtures migrate only), `status` top-level advisory `warnings` (single and batch entry shapes; batch never synthesizes a top-level array) preserved verbatim through the kernel `projectWorkflowStatus` explicit rebuild on both paths, `show --type spec --json` requirement/scenario `name`, and store-backed `allowedEditRoots` two-branch action context (declaring project present: implementation root then store root; absent: store root only with the ask-user constraint) — none of which gates action authority. The Agent registry rotates its series to `'1.14'` with every 1.13 physical fact carried forward plus ten new tools (amp and gsd join the shared `.agents` skills root, widening `SHARED_AGENTS_SKILLS_OWNER_CANDIDATES` to the five upstream writers — antigravity stays commands-only; atomcode, codestudio, dsh, easycode, gigacode, grok, veai, warp, IBM Bob), and generator staleness rotates to the 1.14.0 baseline with the historical provenance union retained. Fixture roles rotate: pinned `openspec-cli-114` (1.14.0) proves accepted contracts across the full positive matrix including the 1132 tracking regressions; `openspec-cli-113` (1.13.2) proves the just-retired line rejected with identity, unsupported classification, and 1.14-only members absent (never asserting v13-era members absent); the `openspec-cli-112` alias stays for its two historical consumers (tool-init-state, agent-command-content) while the v12 boundary suite and both v12/v13 helpers retire. Evidence map: `references/openspec-1.14.0-report.md` (pin `v1.14.0`, `94ca9c1e`). Architecture decision (2026-08-15): `isPlanningComplete` states only planning-artifact completion. Apply `progress` remains implementation task truth even when actionable task entries omit blank descriptions or parse indented checkbox lines. `schemas --json` is a success-array or selected-Root-failure sum type; failure is never an empty successful Config catalog. Architecture decision (2026-08-15): Agent delivery is a CLI-owned physical-artifact protocol. The v9 registry must project current/legacy project roots, user-global skill roots, command paths, migration/cleanup, and restart evidence; OpenSpecUI never hand-writes or deletes Agent artifacts. Archive retirement and `validate --archived` remain typed CLI evidence, never UI-side archive logic. Architecture decision (2026-08-15): A current-page version bypass is not an admitted CLI capability boundary. Unsupported, unknown, and prerelease CLI versions select neither version-specific command capability nor Agent registry; retained Agent physical state and direct Agent Init share the admitted version-selected registry, while literal `tools: 'all'` remains CLI-owned. @@ -484,7 +485,7 @@ proof exist. - 6.11 acceptance uses direct same-origin Project Web with the pinned CLI and isolated `XDG_DATA_HOME`; the experimental App iframe is excluded. -- `references/openspec` is the official upstream submodule; the live pin follows the newest line's evidence report (v13: `v1.13.2`, `db23097`). Historical pins: v1.13.1 (`634c557`), v1.13.0 (`9d4e5974`), v1.12.0 (`e062b957`), v1.11.0 (`a0ddb60d`), v1.9.0 (`2826b888`), v1.7.0 (`4e16790d`). +- `references/openspec` is the official upstream submodule; the live pin follows the newest line's evidence report (v14: `v1.14.0`, `94ca9c1e`). Historical pins: v1.13.2 (`db23097`), v1.13.1 (`634c557`), v1.13.0 (`9d4e5974`), v1.12.0 (`e062b957`), v1.11.0 (`a0ddb60d`), v1.9.0 (`2826b888`), v1.7.0 (`4e16790d`). - OpenSpecUI 7 targets only OpenSpec CLI 1.7.x. OpenSpec CLI 1.6.x and every other CLI line are unsupported and blocked by default; the page-runtime-only **Skip version check** escape hatch creates no compatibility promise. OpenSpecUI 6.1 remains the historical 1.6.x product line with its temporary 1.7 compatibility bridge. - OpenSpec CLI remains the workflow source of truth. Prefer CLI JSON and exit status over parallel parsers or inferred workflow state. - OpenSpec 1.6 Doctor References carry Store identity, root, and diagnostics, but no embedded Spec index. Build Referenced Spec Catalog entries by enumerating each direct Doctor-declared Reference through typed `list --specs --store --json`; never treat an absent `references[*].specs` field as an empty Store. diff --git a/CLAUDE.md b/CLAUDE.md index 51a105e6..7696b138 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -121,7 +121,7 @@ await ctx.cliExecutor.archiveStream(changeId, options, (event) => { ## Important Reference Files -**At the start of each session, read `references/openspec-1.13.2-report.md` for the current OpenSpec protocol and OpenSpecUI adaptation boundary. Use `references/openspec-1.13.1-report.md`, `references/openspec-1.13.0-report.md`, `references/openspec-1.12.0-report.md`, `references/openspec-1.9.0-report.md`, and `references/openspec-0.16.0-report.md` only as historical domain-model context.** +**At the start of each session, read `references/openspec-1.14.0-report.md` for the current OpenSpec protocol and OpenSpecUI adaptation boundary. Use `references/openspec-1.13.2-report.md`, `references/openspec-1.13.1-report.md`, `references/openspec-1.13.0-report.md`, `references/openspec-1.12.0-report.md`, `references/openspec-1.9.0-report.md`, and `references/openspec-0.16.0-report.md` only as historical domain-model context.** This file contains: diff --git a/README-1.13.0.md b/README-1.13.0.md new file mode 100644 index 00000000..077da29f --- /dev/null +++ b/README-1.13.0.md @@ -0,0 +1,223 @@ + + +# OpenSpec UI + +[English](./README.md) | [中文](./README-zh.md) + +OpenSpecUI is a web interface for OpenSpec workflows (live mode + static export). + +## Version Compatibility + +| OpenSpecUI | OpenSpec CLI line | +| ------------------ | ---------------------------------------------------------- | +| `@latest` / `@^13` | current + supported: `>=1.13.0 <1.14.0` | +| `@^12` (legacy) | current + supported: `>=1.12.0 <1.13.0` | +| `@^11` (legacy) | current: `>=1.11.0 <1.12.0`; supported: `>=1.10.0 <1.12.0` | +| `@^9` | current: `>=1.9.0 <1.10.0`; supported: `>=1.8.0 <1.10.0` | +| `@^7` | `>=1.7.0 <1.8.0` | +| `@^6` | current: `>=1.6.0 <1.7.0`; compatible: `>=1.7.0 <1.8.0` | +| `@^5` | current: `>=1.5.0 <1.6.0`; accepted: `>=1.4.0 <1.6.0` | +| `@^4` | current: `>=1.4.0 <1.5.0`; accepted: `>=1.3.0 <1.5.0` | +| `@^3` | `>=1.3.0 <1.4.0` | +| `@^2` | `>=1.2.0 <1.3.0` | +| `@^1` | `>=1.0.0 <1.2.0` | + +OpenSpecUI major versions ordinarily track OpenSpec CLI minor lines. OpenSpecUI 13 adapts OpenSpec +CLI 1.13.x as a single-series line: stable 1.13.x is current and recommended, and `1.14` is not +pre-claimed — when it ships, admission is a separately verified decision (a 13.x window widening or +a new major). OpenSpecUI 12 remains the historical 1.12.x product line, OpenSpecUI 11 the historical +1.10.x/1.11.x line. + +Legacy docs: + +- 1.12: [`README-1.12.0.md`](./README-1.12.0.md) +- 1.11: [`README-1.11.0.md`](./README-1.11.0.md) +- 1.9: [`README-1.9.0.md`](./README-1.9.0.md) +- 1.7: [`README-1.7.0.md`](./README-1.7.0.md) +- 1.6: [`README-1.6.0.md`](./README-1.6.0.md) +- 1.3: [`README-1.3.0.md`](./README-1.3.0.md) +- 1.2: [`README-1.2.0.md`](./README-1.2.0.md) +- 1.x UI / pre-1.2 CLI line: [`README-1.x.md`](./README-1.x.md) +- 0.16: [`README-0.16.0.md`](./README-0.16.0.md) + +## Quick Start + +```bash +# Recommended: run without global install +npx openspecui@latest +bunx openspecui@latest + +# Optional: install globally +npm install -g openspecui +openspecui +``` + +Direct Project Web defaults to `http://localhost:3100` when that presentation is selected. + +## OpenSpec CLI Compatibility + +- OpenSpecUI 13 accepts stable OpenSpec CLI `>=1.13.0 <1.14.0` and recommends the 1.13 line. +- Stable 1.13.x is identified as the current line; this is a single-series window. +- OpenSpec CLI 1.12.x (the OpenSpecUI 12 window), older lines, CLI `>=1.14.0`, and every + prerelease are unsupported by OpenSpecUI 13 and blocked by default. +- If an incompatible CLI executable is available, the mismatch Dialog offers **Skip version check**. + This bypass is held only by the current page runtime, clears on refresh/reopen, and does not + create a compatibility promise. + +OpenSpecUI 13 adds: CLI-owned Apply readiness guidance — `instructions apply` warnings (the +no-delta-specs advisory that predicts a `validate` failure) render on the Change Detail direct plane, +and the `missingPrerequisites` build-order chain renders as readable next-step evidence. The Agent +delivery registry rotates to the 1.13 series with every 1.12 physical fact carried forward, and the +pinned generator baseline moves to 1.13.0. Validation findings (`--report findings`), merge-conflict +informational findings, SourceCraft Code Assistant (`.codeassistant`), batch status (`status --all`), +requirement diff evidence (`show --diff`), `init --language`, Zed, and the Antigravity `.agent` → +`.agents` migration continue on the admitted 1.13 line. + +Upgrade CLI: + +```bash +npm install -g @fission-ai/openspec@1.13 +``` + +## Common Flows + +### Serve a project + +```bash +openspecui +openspecui ./my-project +openspecui serve ./my-project +openspecui --port 3200 +``` + +The bare command is an alias for `serve`. Each `serve` process owns its project Server. If an App +daemon is already running, the project is added to its **Workspaces**. Otherwise an interactive +terminal asks `Start OpenSpecUI App? [Y/n]`; non-interactive execution opens Direct Project Web. + +### App daemon and explicit presentation + +```bash +# Serve this project through the local App daemon (native OpenTray by default) +openspecui --app +openspecui serve --app + +# Open Direct Project Web; also attach to an already-running daemon +openspecui --web +openspecui serve --web +openspecui serve --no-open + +# Manage only the user-level App daemon, never project Servers +openspecui start +openspecui start --web +openspecui stop +openspecui restart +openspecui restart --web +``` + +Daemon host mode is fixed at startup: native uses a retained OpenTray window; `--web` uses the +Browser host. If a repeated `start` explicitly requests another mode, use the exact `restart` +command reported by the CLI. `serve --no-open` performs no prompt, daemon start, Workspace +registration, or Browser opening. URL-valued App mode and project-level shell-location settings are +no longer supported; the daemon serves the App shell bundled with the same CLI release. Every +Workspace tab can ask the daemon to open its current backend in the system browser without exposing +the backend URL as page-controlled input. + +### Static export + +```bash +openspecui export -o ./dist +openspecui export -o ./dist --base-path /docs --clean +``` + +### Nix + +```bash +nix run github:jixoai/openspecui -- --help +nix develop +``` + +## Project Hooks + +OpenSpecUI can load project-local hooks from `openspec/openspecui.hooks.ts`. +Hooks are intentionally kept outside `openspec/.openspecui.json` so executable project behavior +does not pollute persisted UI configuration. + +Install-time types are available from the CLI package: + +```ts +import type { OnReadDocumentHookV1, OnRunWorkflowHookV2 } from 'openspecui/hooks' +``` + +### `onReadDocument` + +Use `onReadDocument` when a project needs to project OpenSpec markdown differently for UI +consumers without rewriting source files. Typical use cases include resolving requirement IDs from +another file, translating markdown for readers, or adding derived context for search/export. + +```ts +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import type { OnReadDocumentHookV1 } from 'openspecui/hooks' + +export const onReadDocument: OnReadDocumentHookV1 = async (ctx, read) => { + const result = await read() + if (ctx.document.kind !== 'spec') return result + + const glossaryPath = join(ctx.projectDir, 'openspec', 'glossary.md') + const glossary = await readFile(glossaryPath, 'utf-8') + + return { + ...result, + markdown: `${result.markdown}\n\n---\n\n${glossary}`, + watchFiles: [glossaryPath], + } +} +``` + +`onReadDocument` runs server-side in OpenSpecUI V1. It applies to processed document reads for +live views, search, and static export. Source reads stay raw and audit-safe, so editing, +validation, and source inspection still use the original OpenSpec files. + +### `onRunWorkflow` + +Use `onRunWorkflow` to adjust the final OPSX invocation payload before OpenSpecUI hands it to an +agent or command runner. OpenSpec CLI remains the source of truth for workflow status, +instructions, schemas, validation, and archive behavior. + +```ts +import type { OnRunWorkflowHookV2 } from 'openspecui/hooks' + +export const onRunWorkflow: OnRunWorkflowHookV2 = async (ctx, run) => { + const result = await run() + if (result.kind !== 'agent-prompt') return result + + return { + ...result, + text: `${result.text}\n\nPlanning root: ${ctx.target.planningRoot.path}\nProject policy: include security impact in the final summary.`, + } +} +``` + +If a hook throws, OpenSpecUI falls back to the default result and attaches diagnostics instead of +blocking the UI. + +## Key Features + +- Dashboard for specs/changes/tasks status +- Config/Schema viewers and editors +- OPSX compose panel for change actions +- Multi-tab PTY terminal (xterm + ghostty-web) +- Search in live mode and static mode +- Static snapshot export for docs hosting +- Project-local hooks for document projection and OPSX invocation customization diff --git a/README-zh-1.13.0.md b/README-zh-1.13.0.md new file mode 100644 index 00000000..0b0050e7 --- /dev/null +++ b/README-zh-1.13.0.md @@ -0,0 +1,209 @@ + + +# OpenSpec UI + +[English](./README.md) | [中文](./README-zh.md) + +OpenSpecUI 是 OpenSpec 工作流的 Web 界面(动态模式 + 静态导出)。 + +## 版本兼容关系 + +| OpenSpecUI | OpenSpec CLI 线 | +| ------------------ | ---------------------------------------------------- | +| `@latest` / `@^13` | 当前 + 已支持:`>=1.13.0 <1.14.0` | +| `@^12`(历史) | 当前 + 已支持:`>=1.12.0 <1.13.0` | +| `@^11`(历史) | 当前:`>=1.11.0 <1.12.0`;已支持:`>=1.10.0 <1.12.0` | +| `@^9` | 当前:`>=1.9.0 <1.10.0`;已支持:`>=1.8.0 <1.10.0` | +| `@^7` | `>=1.7.0 <1.8.0` | +| `@^6` | 当前:`>=1.6.0 <1.7.0`;兼容:`>=1.7.0 <1.8.0` | +| `@^5` | 当前:`>=1.5.0 <1.6.0`;接受:`>=1.4.0 <1.6.0` | +| `@^4` | 当前:`>=1.4.0 <1.5.0`;接受:`>=1.3.0 <1.5.0` | +| `@^3` | `>=1.3.0 <1.4.0` | +| `@^2` | `>=1.2.0 <1.3.0` | +| `@^1` | `>=1.0.0 <1.2.0` | + +OpenSpecUI 的 major 版本通常跟随 OpenSpec CLI 的 minor 线。OpenSpecUI 13 以单系列线适配 OpenSpec +CLI 1.13.x:稳定版 1.13.x 是当前推荐线;`1.14` 不做预先承诺——发布后另行验证决定(13.x 扩窗或新 +major)。OpenSpecUI 12 保留为历史 1.12.x 产品线,OpenSpecUI 11 保留为历史 1.10.x/1.11.x 产品线。 + +历史文档: + +- 1.12:[`README-zh-1.12.0.md`](./README-zh-1.12.0.md) +- 1.11:[`README-zh-1.11.0.md`](./README-zh-1.11.0.md) +- 1.9:[`README-zh-1.9.0.md`](./README-zh-1.9.0.md) +- 1.7:[`README-zh-1.7.0.md`](./README-zh-1.7.0.md) +- 1.6:[`README-zh-1.6.0.md`](./README-zh-1.6.0.md) +- 1.3:[`README-zh-1.3.0.md`](./README-zh-1.3.0.md) +- 1.2:[`README-zh-1.2.0.md`](./README-zh-1.2.0.md) +- 1.x UI / 1.2 之前 CLI 线:[`README-zh-1.x.md`](./README-zh-1.x.md) +- 0.16:[`README-0.16.0.md`](./README-0.16.0.md) + +## 快速开始 + +```bash +# 推荐:不全局安装直接运行 +npx openspecui@latest +bunx openspecui@latest + +# 可选:全局安装 +npm install -g openspecui +openspecui +``` + +选择 Direct Project Web 时,默认地址为 `http://localhost:3100`。 + +## OpenSpec CLI 兼容性 + +- OpenSpecUI 13 接受稳定版 OpenSpec CLI `>=1.13.0 <1.14.0`,并推荐 1.13 线。 +- 稳定版 1.13.x 会被识别为当前线;这是单系列窗口。 +- OpenSpec CLI 1.12.x(OpenSpecUI 12 窗口)、更旧的 CLI 线、CLI `>=1.14.0` 以及所有预发布版均不受 OpenSpecUI 13 支持,并会被默认阻断。 +- 若不兼容的 CLI 可执行文件仍然存在,版本不匹配对话框会提供 **Skip version check**。该绕过只在当前页面运行期有效,刷新或重新打开后清除,也不构成兼容性承诺。 + +OpenSpecUI 13 新增:CLI 拥有的 Apply 就绪指引——`instructions apply` 的 warnings(无 delta specs 预警, +预示 `validate` 将失败)在 Change Detail 直接平面呈现,`missingPrerequisites` 构建序链作为可读的下一步 +证据呈现。Agent 交付注册表轮换到 1.13 系列(1.12 物理事实全部前向继承),pin 生成器基线移至 +1.13.0。校验 findings(`--report findings`)、合并冲突信息级 findings、SourceCraft Code Assistant +(`.codeassistant`)、批量状态(`status --all`)、requirement diff 证据(`show --diff`)、 +`init --language`、Zed 与 Antigravity `.agent` → `.agents` 迁移在已接受的 1.13 线上继续可用。 + +升级 CLI: + +```bash +npm install -g @fission-ai/openspec@1.13 +``` + +## 常见流程 + +### 启动项目服务 + +```bash +openspecui +openspecui ./my-project +openspecui serve ./my-project +openspecui --port 3200 +``` + +裸命令是 `serve` 的缩写,每个 `serve` 进程只负责自己的项目 Server。若 App daemon 已经 +运行,项目会被加入它的 **Workspaces**;否则交互式终端会询问 +`Start OpenSpecUI App? [Y/n]`,非交互环境则打开 Direct Project Web。 + +### App daemon 与显式呈现模式 + +```bash +# 通过本地 App daemon 呈现当前项目(默认使用原生 OpenTray) +openspecui --app +openspecui serve --app + +# 打开 Direct Project Web;若 daemon 已运行,同时附加 Workspace +openspecui --web +openspecui serve --web +openspecui serve --no-open + +# 只管理用户级 App daemon,不管理项目 Server +openspecui start +openspecui start --web +openspecui stop +openspecui restart +openspecui restart --web +``` + +daemon 的宿主模式在启动时固定:native 使用保留式 OpenTray 窗口,`--web` 使用 +普通 Browser Web 宿主。若重复 `start` 显式请求了另一种模式,请执行 CLI 提示的精确 +`restart` 命令。`serve --no-open` 不会询问、启动 daemon、注册 Workspace 或打开浏览器。 +带 URL 的 App 模式与项目级 App shell 地址设置已不再支持;daemon 只提供与当前 CLI +同版本打包的 App shell。每个 Workspace 标签都可以请求 daemon 在系统浏览器中打开当前 +backend,页面本身不能提交任意 URL。 + +### 静态导出 + +```bash +openspecui export -o ./dist +openspecui export -o ./dist --base-path /docs --clean +``` + +### Nix + +```bash +nix run github:jixoai/openspecui -- --help +nix develop +``` + +## 项目 Hooks + +OpenSpecUI 支持从 `openspec/openspecui.hooks.ts` 加载项目级 hooks。 +Hooks 刻意放在 `openspec/.openspecui.json` 之外,避免可执行的项目行为污染持久化的 UI 配置。 + +安装期类型可从 CLI 包导入: + +```ts +import type { OnReadDocumentHookV1, OnRunWorkflowHookV2 } from 'openspecui/hooks' +``` + +### `onReadDocument` + +当项目需要为 UI 消费者以不同方式呈现 OpenSpec markdown、又不想改写源文件时,使用 `onReadDocument`。 +典型场景包括从其它文件解析需求 ID、为读者翻译 markdown,或为搜索/导出补充派生上下文。 + +```ts +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import type { OnReadDocumentHookV1 } from 'openspecui/hooks' + +export const onReadDocument: OnReadDocumentHookV1 = async (ctx, read) => { + const result = await read() + if (ctx.document.kind !== 'spec') return result + + const glossaryPath = join(ctx.projectDir, 'openspec', 'glossary.md') + const glossary = await readFile(glossaryPath, 'utf-8') + + return { + ...result, + markdown: `${result.markdown}\n\n---\n\n${glossary}`, + watchFiles: [glossaryPath], + } +} +``` + +`onReadDocument` 在 OpenSpecUI V1 中服务端运行,作用于动态视图、搜索与静态导出的处理后文档读取。 +源文件读取保持原始且可审计,因此编辑、校验与源码检查仍使用原始 OpenSpec 文件。 + +### `onRunWorkflow` + +使用 `onRunWorkflow` 在 OpenSpecUI 把最终 OPSX 调用载荷交给 agent 或命令运行器之前进行调整。 +OpenSpec CLI 仍是 workflow 状态、指令、schema、校验与归档行为的唯一事实来源。 + +```ts +import type { OnRunWorkflowHookV2 } from 'openspecui/hooks' + +export const onRunWorkflow: OnRunWorkflowHookV2 = async (ctx, run) => { + const result = await run() + if (result.kind !== 'agent-prompt') return result + + return { + ...result, + text: `${result.text}\n\nPlanning root: ${ctx.target.planningRoot.path}\nProject policy: include security impact in the final summary.`, + } +} +``` + +如果 hook 抛错,OpenSpecUI 会回退到默认结果并附上诊断信息,而不是阻断界面。 + +## 核心能力 + +- specs/changes/tasks 状态仪表盘 +- Config/Schema 查看与编辑 +- 用于 change action 的 OPSX compose 面板 +- 多标签 PTY 终端(xterm + ghostty-web) +- 动态模式与静态模式搜索 +- 用于文档托管的静态快照导出 +- 用于文档投影与 OPSX 调用定制的项目级 hooks diff --git a/README-zh.md b/README-zh.md index 0b0050e7..8b5a462b 100644 --- a/README-zh.md +++ b/README-zh.md @@ -1,5 +1,5 @@ # OpenSpec UI @@ -20,7 +21,8 @@ OpenSpecUI 是 OpenSpec 工作流的 Web 界面(动态模式 + 静态导出) | OpenSpecUI | OpenSpec CLI 线 | | ------------------ | ---------------------------------------------------- | -| `@latest` / `@^13` | 当前 + 已支持:`>=1.13.0 <1.14.0` | +| `@latest` / `@^14` | 当前 + 已支持:`>=1.14.0 <1.15.0` | +| `@^13`(历史) | 当前 + 已支持:`>=1.13.0 <1.14.0` | | `@^12`(历史) | 当前 + 已支持:`>=1.12.0 <1.13.0` | | `@^11`(历史) | 当前:`>=1.11.0 <1.12.0`;已支持:`>=1.10.0 <1.12.0` | | `@^9` | 当前:`>=1.9.0 <1.10.0`;已支持:`>=1.8.0 <1.10.0` | @@ -32,12 +34,13 @@ OpenSpecUI 是 OpenSpec 工作流的 Web 界面(动态模式 + 静态导出) | `@^2` | `>=1.2.0 <1.3.0` | | `@^1` | `>=1.0.0 <1.2.0` | -OpenSpecUI 的 major 版本通常跟随 OpenSpec CLI 的 minor 线。OpenSpecUI 13 以单系列线适配 OpenSpec -CLI 1.13.x:稳定版 1.13.x 是当前推荐线;`1.14` 不做预先承诺——发布后另行验证决定(13.x 扩窗或新 -major)。OpenSpecUI 12 保留为历史 1.12.x 产品线,OpenSpecUI 11 保留为历史 1.10.x/1.11.x 产品线。 +OpenSpecUI 的 major 版本通常跟随 OpenSpec CLI 的 minor 线。OpenSpecUI 14 以单系列线适配 OpenSpec +CLI 1.14.x:稳定版 1.14.x 是当前推荐线;`1.15` 不做预先承诺——发布后另行验证决定(14.x 扩窗或新 +major)。OpenSpecUI 13 保留为历史 1.13.x 产品线,OpenSpecUI 12 保留为历史 1.12.x 产品线。 历史文档: +- 1.13:[`README-zh-1.13.0.md`](./README-zh-1.13.0.md) - 1.12:[`README-zh-1.12.0.md`](./README-zh-1.12.0.md) - 1.11:[`README-zh-1.11.0.md`](./README-zh-1.11.0.md) - 1.9:[`README-zh-1.9.0.md`](./README-zh-1.9.0.md) @@ -64,22 +67,24 @@ openspecui ## OpenSpec CLI 兼容性 -- OpenSpecUI 13 接受稳定版 OpenSpec CLI `>=1.13.0 <1.14.0`,并推荐 1.13 线。 -- 稳定版 1.13.x 会被识别为当前线;这是单系列窗口。 -- OpenSpec CLI 1.12.x(OpenSpecUI 12 窗口)、更旧的 CLI 线、CLI `>=1.14.0` 以及所有预发布版均不受 OpenSpecUI 13 支持,并会被默认阻断。 +- OpenSpecUI 14 接受稳定版 OpenSpec CLI `>=1.14.0 <1.15.0`,并推荐 1.14 线。 +- 稳定版 1.14.x 会被识别为当前线;这是单系列窗口。 +- OpenSpec CLI 1.13.x(OpenSpecUI 13 窗口)、更旧的 CLI 线、CLI `>=1.15.0` 以及所有预发布版均不受 OpenSpecUI 14 支持,并会被默认阻断。 - 若不兼容的 CLI 可执行文件仍然存在,版本不匹配对话框会提供 **Skip version check**。该绕过只在当前页面运行期有效,刷新或重新打开后清除,也不构成兼容性承诺。 -OpenSpecUI 13 新增:CLI 拥有的 Apply 就绪指引——`instructions apply` 的 warnings(无 delta specs 预警, -预示 `validate` 将失败)在 Change Detail 直接平面呈现,`missingPrerequisites` 构建序链作为可读的下一步 -证据呈现。Agent 交付注册表轮换到 1.13 系列(1.12 物理事实全部前向继承),pin 生成器基线移至 -1.13.0。校验 findings(`--report findings`)、合并冲突信息级 findings、SourceCraft Code Assistant -(`.codeassistant`)、批量状态(`status --all`)、requirement diff 证据(`show --diff`)、 -`init --language`、Zed 与 Antigravity `.agent` → `.agents` 迁移在已接受的 1.13 线上继续可用。 +OpenSpecUI 14 新增:`status` 咨询级 warnings 以折叠摘要条呈现在 Change Detail 直接平面(就地展开逐字 +原文,不 gate 任何操作);Apply 任务证据携带 CLI 拥有的 `sourcePath`/`line` 源位置;`show --type spec +--json` 投影 requirement/scenario 名称;`list --archived`/`--all` 投影逐条目的 `archived` 事实;`version` +命令的类型化 envelope 解码不做网络探测;store-backed status 的 action context 投影 1.14 +`allowedEditRoots` 双分支语义(声明项目在场或缺席)。Agent 交付注册表轮换到 1.14 系列——新增十个工具, +含共享 `.agents` skills 根的 amp 与 gsd(五写入方仲裁)以及 IBM 的 Bob——1.13 物理事实全部前向继承, +pin 生成器基线移至 1.14.0。Apply 就绪指引、校验 findings、批量状态、requirement diff 证据与 +`init --language` 在已接受的 1.14 线上继续可用。 升级 CLI: ```bash -npm install -g @fission-ai/openspec@1.13 +npm install -g @fission-ai/openspec@1.14 ``` ## 常见流程 diff --git a/README.md b/README.md index 077da29f..39014baa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # OpenSpec UI @@ -21,7 +22,8 @@ OpenSpecUI is a web interface for OpenSpec workflows (live mode + static export) | OpenSpecUI | OpenSpec CLI line | | ------------------ | ---------------------------------------------------------- | -| `@latest` / `@^13` | current + supported: `>=1.13.0 <1.14.0` | +| `@latest` / `@^14` | current + supported: `>=1.14.0 <1.15.0` | +| `@^13` (legacy) | current + supported: `>=1.13.0 <1.14.0` | | `@^12` (legacy) | current + supported: `>=1.12.0 <1.13.0` | | `@^11` (legacy) | current: `>=1.11.0 <1.12.0`; supported: `>=1.10.0 <1.12.0` | | `@^9` | current: `>=1.9.0 <1.10.0`; supported: `>=1.8.0 <1.10.0` | @@ -33,14 +35,15 @@ OpenSpecUI is a web interface for OpenSpec workflows (live mode + static export) | `@^2` | `>=1.2.0 <1.3.0` | | `@^1` | `>=1.0.0 <1.2.0` | -OpenSpecUI major versions ordinarily track OpenSpec CLI minor lines. OpenSpecUI 13 adapts OpenSpec -CLI 1.13.x as a single-series line: stable 1.13.x is current and recommended, and `1.14` is not -pre-claimed — when it ships, admission is a separately verified decision (a 13.x window widening or -a new major). OpenSpecUI 12 remains the historical 1.12.x product line, OpenSpecUI 11 the historical -1.10.x/1.11.x line. +OpenSpecUI major versions ordinarily track OpenSpec CLI minor lines. OpenSpecUI 14 adapts OpenSpec +CLI 1.14.x as a single-series line: stable 1.14.x is current and recommended, and `1.15` is not +pre-claimed — when it ships, admission is a separately verified decision (a 14.x window widening or +a new major). OpenSpecUI 13 remains the historical 1.13.x product line, OpenSpecUI 12 the historical +1.12.x line. Legacy docs: +- 1.13: [`README-1.13.0.md`](./README-1.13.0.md) - 1.12: [`README-1.12.0.md`](./README-1.12.0.md) - 1.11: [`README-1.11.0.md`](./README-1.11.0.md) - 1.9: [`README-1.9.0.md`](./README-1.9.0.md) @@ -67,27 +70,30 @@ Direct Project Web defaults to `http://localhost:3100` when that presentation is ## OpenSpec CLI Compatibility -- OpenSpecUI 13 accepts stable OpenSpec CLI `>=1.13.0 <1.14.0` and recommends the 1.13 line. -- Stable 1.13.x is identified as the current line; this is a single-series window. -- OpenSpec CLI 1.12.x (the OpenSpecUI 12 window), older lines, CLI `>=1.14.0`, and every - prerelease are unsupported by OpenSpecUI 13 and blocked by default. +- OpenSpecUI 14 accepts stable OpenSpec CLI `>=1.14.0 <1.15.0` and recommends the 1.14 line. +- Stable 1.14.x is identified as the current line; this is a single-series window. +- OpenSpec CLI 1.13.x (the OpenSpecUI 13 window), older lines, CLI `>=1.15.0`, and every + prerelease are unsupported by OpenSpecUI 14 and blocked by default. - If an incompatible CLI executable is available, the mismatch Dialog offers **Skip version check**. This bypass is held only by the current page runtime, clears on refresh/reopen, and does not create a compatibility promise. -OpenSpecUI 13 adds: CLI-owned Apply readiness guidance — `instructions apply` warnings (the -no-delta-specs advisory that predicts a `validate` failure) render on the Change Detail direct plane, -and the `missingPrerequisites` build-order chain renders as readable next-step evidence. The Agent -delivery registry rotates to the 1.13 series with every 1.12 physical fact carried forward, and the -pinned generator baseline moves to 1.13.0. Validation findings (`--report findings`), merge-conflict -informational findings, SourceCraft Code Assistant (`.codeassistant`), batch status (`status --all`), -requirement diff evidence (`show --diff`), `init --language`, Zed, and the Antigravity `.agent` → -`.agents` migration continue on the admitted 1.13 line. +OpenSpecUI 14 adds: `status` advisory warnings render on the Change Detail direct plane as a collapsed +summary row with in-place verbatim text (never gating actions); Apply task evidence carries CLI-owned +`sourcePath`/`line` source locations; `show --type spec --json` projects requirement/scenario names; +`list --archived`/`--all` project the per-entry `archived` fact; the `version` command's typed +envelope is decoded without network probing; and store-backed status action context projects the +1.14 `allowedEditRoots` two-branch semantics (declaring project present or absent). The Agent +delivery registry rotates to the 1.14 series — ten new tools including amp and gsd on the shared +`.agents` skills root (five-writer arbitration) and IBM's Bob — with every 1.13 physical fact +carried forward, and the pinned generator baseline moves to 1.14.0. Apply readiness guidance, +validation findings, batch status, requirement diff evidence, and `init --language` continue on the +admitted 1.14 line. Upgrade CLI: ```bash -npm install -g @fission-ai/openspec@1.13 +npm install -g @fission-ai/openspec@1.14 ``` ## Common Flows diff --git a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md index db80efff..995cdddc 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md @@ -46,7 +46,7 @@ Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下, ## CP5 — Delivery -- [ ] Changeset major; README law (repo en/zh + CLI package); CLAUDE.md pointer; AGENTS.md decision. +- [x] Changeset major; README law (repo en/zh + CLI package); CLAUDE.md pointer; AGENTS.md decision. - [ ] Full gates (format/lint/typecheck/browser + explicit per-package tests); pre-existing flakes disclosed. - [ ] PR opened; CI green; Codex final review disposition recorded. - [ ] Owner acceptance walkthrough (Owner-only). diff --git a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md index 174f7e82..d0a821a3 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md @@ -224,3 +224,21 @@ Cross-slice check after three parallel agents shared one worktree: core 18 files including the cross-slice fixpoint: Slice 4's reported `change-view.test.tsx` 24/25 failure (admitted fixture 1.13.0 rejected by the rotated window) is closed by Slice 6's literal rotation, now 25/25 — server 4 files green, core+web typecheck clean, no orphan references, lockfile scoped to the new alias. + +### Slice 7 delivery prep + full-gate residue (2026-10-02) + +Delivery docs: `.changeset/openspec-114-line-adaptation.md` (major ×4); repository README en/zh v14 +rows + v13 archived snapshots (`README-1.13.0.md` / `README-zh-1.13.0.md`) + 1.14 feature prose + +`@1.14` upgrade command; CLI package README scoped to the v14 line (release README law); CLAUDE.md +session pointer to `openspec-1.14.0-report.md`; AGENTS.md v14 architecture decision + submodule pin +line (v14 `94ca9c1e`, v1.13.2 `db23097` into historical pins). + +Full-gate residue caught by `pnpm test:ci`: `opsx-kernel-cli-projection.test.ts` was outside every +slice list (hand-written fake CLI, not the pinned fixture matrix). Its admitted/retired fixture +versions still modeled the v13 window (`'1.12.0' | '1.13.0'`), so the rotated compat gate derived +all-false batch/findings capabilities and the batch status transport fell back to per-change serial +spawns — 7 real failures (exactly the gate-leak the file promises to fail loudly). Rotated to +`'1.13.0' | '1.14.0'` (types, call sites, fake guards `startsWith('1.14')`, prose, header); file +green 85/85. Lesson recorded: capability-gated tests with hand-written version fixtures rotate with +the window even when they consume no pinned alias; `test:ci`'s `pnpm -r` recursion stops at the +first failing package, so a core failure masks server/web — full re-run required after any fix. diff --git a/packages/cli/README.md b/packages/cli/README.md index abc97fd1..f87b5040 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,6 +1,6 @@ # OpenSpecUI A visual interface for [OpenSpec](https://openspec.dev) — the visual projection and operator for spec-driven development with AI agents. -OpenSpecUI 13 gives OpenSpec projects a reactive dashboard, an objective change workflow, a config workbench, real terminals, and static export — while the OpenSpec CLI stays the source of truth for every workflow fact. +OpenSpecUI 14 gives OpenSpec projects a reactive dashboard, an objective change workflow, a config workbench, real terminals, and static export — while the OpenSpec CLI stays the source of truth for every workflow fact. ## Requirements -- **OpenSpec CLI**: `>=1.13.0 <1.14.0` — the stable 1.13 line is current and recommended (single-series window). Older lines (including the 1.12.x OpenSpecUI 12 window), prereleases, and `>=1.14.0` are blocked by default. +- **OpenSpec CLI**: `>=1.14.0 <1.15.0` — the stable 1.14 line is current and recommended (single-series window). Older lines (including the 1.13.x OpenSpecUI 13 window), prereleases, and `>=1.15.0` are blocked by default. - **Node.js**: `>= 20.19.0` ```bash -npm install -g @fission-ai/openspec@1.13 +npm install -g @fission-ai/openspec@1.14 ``` ## Quick start @@ -84,10 +85,10 @@ URL-valued App mode is not supported: the daemon always serves the App shell bun ## What's inside - **OPSX change workflow** — kanban lanes over tracked task phases with Continue / Fast-forward / Apply / Verify / Archive operators driven by live CLI evidence; admitted sessions load the whole change status list in one `status --all` spawn -- **Change evidence** — MODIFIED deltas can present the CLI's own requirement diff and warning evidence (`show --diff`) beside the local delta projection, plus validation findings reports (`validate --report findings`) with informational merge-conflict findings +- **Change evidence** — MODIFIED deltas can present the CLI's own requirement diff and warning evidence (`show --diff`) beside the local delta projection, validation findings reports (`validate --report findings`) with informational merge-conflict findings, `status` advisory warnings as a collapsed direct-plane summary row, Apply task `sourcePath`/`line` source locations, spec requirement/scenario names from `show --type spec`, and per-entry `archived` inventory facts from `list --archived`/`--all` - **Dashboard** — active changes with CLI-owned applying progress and a curated Code Git snapshot - **Config workbench** — route-backed owners for project binding, active root, environment globals, and schemas, plus an adaptive setup guide and `init --language` on the admitted CLI line -- **Agent delivery** — the CLI-owned registry projects per-version agent commands and skills, including SourceCraft Code Assistant (from 1.12, `.codeassistant`), Zed, and the Antigravity `.agent` → `.agents` migration +- **Agent delivery** — the CLI-owned registry projects per-version agent commands and skills across fifty tools on the 1.14 series, including the ten 1.14 additions (amp and gsd on the shared `.agents` skills root, IBM Bob, and more), SourceCraft Code Assistant (from 1.12, `.codeassistant`), Zed, and the Antigravity `.agent` → `.agents` migration - **Terminals** — multi-tab PTY sessions (xterm and ghostty-web) with direct agent send - **Git view** — commits, patches, and worktrees with explicit code/planning scope - **Search** — reactive in live mode, worker-backed in static exports diff --git a/packages/core/src/opsx-kernel-cli-projection.test.ts b/packages/core/src/opsx-kernel-cli-projection.test.ts index c4d29f2a..9a035bbb 100644 --- a/packages/core/src/opsx-kernel-cli-projection.test.ts +++ b/packages/core/src/opsx-kernel-cli-projection.test.ts @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-03 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Prove OPSX Change enumeration comes from the executable typed OpenSpec CLI contract. * 2. Prove physical Change directories remain invalidation evidence rather than projected business truth. * 3. Preserve the exact Store-selected CLI payload and process evidence in the retained projection. @@ -21,6 +21,7 @@ * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — change-list nested/warnings projection (update-openspec-cli-1131 Slice 2). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — admitted/retired fixture versions rotate with the v14 window. */ import { readFileSync } from 'node:fs' import { mkdir, mkdtemp, writeFile } from 'node:fs/promises' @@ -536,10 +537,10 @@ const BATCH_STATUS_CHANGES = ['change-a', 'change-b', 'change-c'] * Fake CLI fixture for the capability-gated batch status transport. * * Every invocation argv is appended to `cli-invocations.log` so tests assert real spawn - * argv, not spy calls. `--version` selects the fixture line: 1.13.0 models the admitted - * OpenSpecUI 13 session (batch capability true), while the retired 1.12.0 models a + * argv, not spy calls. `--version` selects the fixture line: 1.14.0 models the admitted + * OpenSpecUI 14 session (batch capability true), while the retired 1.13.0 models a * non-admitted session deriving all-false capabilities. The fake rejects `status --all` - * and `--report findings` outside the 1.13 line so any gate leak fails loudly here + * and `--report findings` outside the 1.14 line so any gate leak fails loudly here * (upstream fidelity of the boundary is proven separately by the pinned official-cli * fixture matrix). Marker files switch the fixture scenario: * - `fail-change-b`: the batch envelope carries a per-change failure entry for change-b @@ -552,7 +553,7 @@ const BATCH_STATUS_CHANGES = ['change-a', 'change-b', 'change-c'] function writeBatchStatusCliFixture( cliPath: string, projectDir: string, - version: '1.12.0' | '1.13.0' + version: '1.13.0' | '1.14.0' ): Promise { return writeFile( cliPath, @@ -650,7 +651,7 @@ if (args[0] === 'list' && args.includes('--json')) { } if (args[0] === 'status' && args.includes('--all') && args.includes('--json')) { - if (!version.startsWith('1.13')) { + if (!version.startsWith('1.14')) { console.error('unknown option --all') process.exit(2) } @@ -723,7 +724,7 @@ describe('OpsxKernel capability-gated batch status transport', () => { } async function prepareBatchKernel( - version: '1.12.0' | '1.13.0', + version: '1.13.0' | '1.14.0', markers: readonly string[] = [] ): Promise { const projectDir = await mkdtemp(join(tmpdir(), 'openspecui-opsx-batch-status-')) @@ -780,8 +781,8 @@ describe('OpsxKernel capability-gated batch status transport', () => { } it('loads the status list with one status --all spawn and per-change projections identical to the serial transport', async () => { - const batch = await prepareBatchKernel('1.13.0') - const serial = await prepareBatchKernel('1.12.0') + const batch = await prepareBatchKernel('1.14.0') + const serial = await prepareBatchKernel('1.13.0') try { await batch.kernel.ensureStatusList() @@ -837,7 +838,7 @@ describe('OpsxKernel capability-gated batch status transport', () => { }) it('keeps a batch per-change failure entry as that change evidence without failing the status-list Work', async () => { - const { kernel, readInvocations, dispose } = await prepareBatchKernel('1.13.0', [ + const { kernel, readInvocations, dispose } = await prepareBatchKernel('1.14.0', [ 'fail-change-b', ]) @@ -901,7 +902,7 @@ describe('OpsxKernel capability-gated batch status transport', () => { // 1.11.0 is inside the retired v11 window: it derives all-false capabilities in the // OpenSpecUI 12 single-series admission, so even a bypassed session must keep the // serial transport and never receive `--all`. - const { kernel, readInvocations, dispose } = await prepareBatchKernel('1.12.0') + const { kernel, readInvocations, dispose } = await prepareBatchKernel('1.13.0') try { const work = await kernel.readStatusListProjection() @@ -922,7 +923,7 @@ describe('OpsxKernel capability-gated batch status transport', () => { } }) - it('keeps single-change status reads on the per-change transport under admitted 1.13 sessions', async () => { + it('keeps single-change status reads on the per-change transport under admitted 1.14 sessions', async () => { const { kernel, readInvocations, dispose } = await prepareBatchKernel('1.13.0') try { @@ -942,7 +943,7 @@ describe('OpsxKernel capability-gated batch status transport', () => { }) it('routes batch transport and root-selection failures through the existing error paths', async () => { - const garbage = await prepareBatchKernel('1.13.0', ['batch-garbage']) + const garbage = await prepareBatchKernel('1.14.0', ['batch-garbage']) try { await expect(garbage.kernel.ensureStatusList()).rejects.toMatchObject({ name: 'CliProjectionCommandError', @@ -952,7 +953,7 @@ describe('OpsxKernel capability-gated batch status transport', () => { await garbage.dispose() } - const rootFailure = await prepareBatchKernel('1.13.0', ['batch-root-failure']) + const rootFailure = await prepareBatchKernel('1.14.0', ['batch-root-failure']) try { await expect(rootFailure.kernel.ensureStatusList()).rejects.toMatchObject({ name: 'CliProjectionCommandError', @@ -977,13 +978,13 @@ describe('OpsxKernel capability-gated batch status transport', () => { describe('OpsxKernel capability-gated validate findings transport', () => { /** - * Fake CLI fixture for the OpenSpec 1.12 `validate --report findings` transport. + * Fake CLI fixture for the OpenSpec 1.12+ `validate --report findings` transport. * * Every invocation argv is appended to `cli-invocations.log`. `--version` selects the - * fixture line: `1.12.0` models the admitted session (findings capability true) while - * the retired `1.11.0` and an unparseable version string model non-admitted sessions - * deriving all-false capabilities. The fake rejects `--report` outside the 1.12 line - * so any capability-gate leak fails loudly here; upstream fidelity of the 1.11 + * fixture line: `1.14.0` models the admitted session (findings capability true) while + * the retired `1.13.0` and an unparseable version string model non-admitted sessions + * deriving all-false capabilities. The fake rejects `--report` outside the 1.14 line + * so any capability-gate leak fails loudly here; upstream fidelity of the retired-line * boundary is proven separately by the pinned official-cli fixture matrix. Marker * files switch the scenario: * - `findings-request-error`: the CLI answers the typed request-failure envelope. @@ -992,7 +993,7 @@ describe('OpsxKernel capability-gated validate findings transport', () => { function writeFindingsCliFixture( cliPath: string, projectDir: string, - version: '1.12.0' | '1.13.0' | 'unparseable' + version: '1.13.0' | '1.14.0' | 'unparseable' ): Promise { return writeFile( cliPath, @@ -1017,7 +1018,7 @@ if (args.includes('--version')) { } if (args[0] === 'validate' && args.includes('--report')) { - if (!version.startsWith('1.13')) { + if (!version.startsWith('1.14')) { console.error('unknown option --report') process.exit(2) } @@ -1094,7 +1095,7 @@ process.exit(1) } async function prepareFindingsKernel( - version: '1.12.0' | '1.13.0' | 'unparseable', + version: '1.13.0' | '1.14.0' | 'unparseable', options: { markers?: readonly string[]; store?: string } = {} ): Promise { const projectDir = await mkdtemp(join(tmpdir(), 'openspecui-opsx-findings-')) @@ -1129,8 +1130,8 @@ process.exit(1) } } - it('loads findings through one gated spawn on an admitted 1.13 session with the Store selector forwarded', async () => { - const { kernel, readInvocations, dispose } = await prepareFindingsKernel('1.13.0', { + it('loads findings through one gated spawn on an admitted 1.14 session with the Store selector forwarded', async () => { + const { kernel, readInvocations, dispose } = await prepareFindingsKernel('1.14.0', { store: 'shared', }) @@ -1166,7 +1167,7 @@ process.exit(1) }) it('maps the archived bulk scope onto the archived validate target', async () => { - const { kernel, readInvocations, dispose } = await prepareFindingsKernel('1.13.0') + const { kernel, readInvocations, dispose } = await prepareFindingsKernel('1.14.0') try { const projection = await kernel.readValidationFindingsProjection('archived') @@ -1188,7 +1189,7 @@ process.exit(1) it('never composes --report findings argv for a non-admitted session', async () => { // The retired 1.11.0 line: even a bypassed session derives all-false capabilities, // so the kernel must answer the typed refusal before any validate spawn happens. - const retired = await prepareFindingsKernel('1.12.0') + const retired = await prepareFindingsKernel('1.13.0') try { await expect(retired.kernel.readValidationFindingsProjection('all')).resolves.toEqual({ kind: 'unavailable', @@ -1214,7 +1215,7 @@ process.exit(1) }) it('keeps the CLI request-failure envelope as decoded evidence instead of throwing', async () => { - const { kernel, dispose } = await prepareFindingsKernel('1.13.0', { + const { kernel, dispose } = await prepareFindingsKernel('1.14.0', { markers: ['findings-request-error'], }) @@ -1240,7 +1241,7 @@ process.exit(1) }) it('routes an unparseable findings document through the existing command error path', async () => { - const { kernel, dispose } = await prepareFindingsKernel('1.13.0', { + const { kernel, dispose } = await prepareFindingsKernel('1.14.0', { markers: ['findings-garbage'], }) diff --git a/packages/web/src/components/change-diff-evidence.test.tsx b/packages/web/src/components/change-diff-evidence.test.tsx index cdcc0a95..3a0cc1a2 100644 --- a/packages/web/src/components/change-diff-evidence.test.tsx +++ b/packages/web/src/components/change-diff-evidence.test.tsx @@ -1,9 +1,9 @@ /** - * Orthogonal intents (updated 2026-09-03 Asia/Shanghai): - * 1. Lock the admitted 1.13-session rendering: MODIFIED diff body, line roles, exact upstream + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): + * 1. Lock the admitted 1.14-session rendering: MODIFIED diff body, line roles, exact upstream * warning, and CLI provenance are directly visible in the Evidence workspace detail pane. * 2. Lock the near-miss contract: a delta carrying both warning and diff renders both. - * 3. Prove retired (1.10/1.11/1.12) and static sessions never issue the diff transport call and + * 3. Prove retired (1.10/1.11/1.12/1.13) and static sessions never issue the diff transport call and * degrade without fabricated evidence. * 4. Prove absent diff fields, command failure, and transport failure render typed evidence * instead of a crash or an invented diff. @@ -14,6 +14,7 @@ * Original request (2026-08-28): "使用移动端的 list-detail 思维……分成两栏,左侧 list,右侧详情。这种结构替代手风琴会更好" * Original request (2026-09-03): "Openspec 1.12.0 刚刚放出来,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进" * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — admitted session fixture rotates to 1.14.0. */ import { isStaticMode } from '@/lib/static-mode' import { cleanup, render, screen, waitFor } from '@testing-library/react' @@ -44,7 +45,7 @@ const rootActionStateMock = vi.hoisted(() => ({ message: null, evidence: [] as string[], context: { - cli: { available: true, version: '1.13.0' }, + cli: { available: true, version: '1.14.0' }, } as { cli: { available: boolean; version: string } } | null, observedAt: 1, }, @@ -110,13 +111,13 @@ describe('ChangeDiffEvidence', () => { message: null, evidence: [], context: { - cli: { available: true, version: '1.13.0' }, + cli: { available: true, version: '1.14.0' }, }, observedAt: 1, } }) - it('renders the MODIFIED delta diff with line roles, warning, and CLI provenance on an admitted 1.13 session', async () => { + it('renders the MODIFIED delta diff with line roles, warning, and CLI provenance on an admitted 1.14 session', async () => { diffEvidenceQuery.mockResolvedValue(executedEvidence()) const { container } = render() @@ -260,7 +261,7 @@ describe('ChangeDiffEvidence', () => { diffEvidenceQuery.mockRejectedValue(new Error('planning root unresolved')) rootActionStateMock.state = { ...rootActionStateMock.state, - context: { cli: { available: true, version: '1.13.0' } }, + context: { cli: { available: true, version: '1.14.0' } }, } rerender() await waitFor(() => expect(onChip).toHaveBeenLastCalledWith({ label: 'error', tone: 'error' })) From ffa53b1359a237e236fc48d875d4be691bdc0b2f Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 2 Oct 2026 05:03:09 +0800 Subject: [PATCH 10/12] docs(openspec): record the full-gate final state for the v14 line --- .../loop/checkpoints.md | 3 ++- .../loop/implementation.md | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md index 995cdddc..53816fa7 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md @@ -47,7 +47,8 @@ Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下, ## CP5 — Delivery - [x] Changeset major; README law (repo en/zh + CLI package); CLAUDE.md pointer; AGENTS.md decision. -- [ ] Full gates (format/lint/typecheck/browser + explicit per-package tests); pre-existing flakes disclosed. +- [x] Full gates (format/lint/typecheck/browser + explicit per-package tests); pre-existing flakes disclosed + (xterm storybook Pixi flake isolated-rerun clean, full browser gate green on re-run). - [ ] PR opened; CI green; Codex final review disposition recorded. - [ ] Owner acceptance walkthrough (Owner-only). - [ ] Archive/release — Owner decision. diff --git a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md index d0a821a3..65e83e6f 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md @@ -242,3 +242,16 @@ spawns — 7 real failures (exactly the gate-leak the file promises to fail loud green 85/85. Lesson recorded: capability-gated tests with hand-written version fixtures rotate with the window even when they consume no pinned alias; `test:ci`'s `pnpm -r` recursion stops at the first failing package, so a core failure masks server/web — full re-run required after any fix. + +### Full-gate final state (2026-10-02) + +- `pnpm format:check` — pass. `pnpm lint:ci` — 0 errors (6 pre-existing warnings). +- `pnpm typecheck` — all packages pass. +- `pnpm test:ci` — green after the two hand-written-fixture rotations (root scripts 20+2 skipped, + cli 2, core 84+1 skipped, server 100+2 skipped, web 195, app 68, xterm 7). +- `pnpm test:browser:ci` — run 2 hit a load-sensitive xterm storybook flake + (`virtual-trackpad-tab` Pixi canvas null on a parallel browser run); isolated rerun of that file + passed, and run 3 of the full browser gate passed clean (exit 0, all suites). Pre-existing + flake disclosed, not papered over. +- SSG static minimal set (`entry-client-static` + `static-data-provider.opsx`) — pass (no + static-surface changes in this line beyond shared projections). From e133e58a4620baa0fe559930ea33fa47702d9893 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 2 Oct 2026 06:34:23 +0800 Subject: [PATCH 11/12] =?UTF-8?q?fix(core,server):=20close=20the=20Round-F?= =?UTF-8?q?=20review=20blockers=20=E2=80=94=20archived=20projection=20rete?= =?UTF-8?q?ntion,=20real-kernel=20status-warnings=20transport,=20governanc?= =?UTF-8?q?e=20pin=20drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-F of target-openspec-cli-114-line (Codex implementation review 7.8/10 REVISE), all three blockers fixed: - F-B1: the opsx-change-list projection entry schema stripped the CLI `archived` fact at the PlanningCliProjectionDataSchema boundary. The member is now optional typed evidence with three-state assertions (--archived true, --all mixed false/true, default absent and never synthesized); the Server service reuses this schema so its boundary parse retains the member. - F-B2: the promised Server-visible status-warnings mirror evidence now exists as a real-kernel transport suite — fake 1.14 executable -> real CliExecutor -> real OpsxKernel projectWorkflowStatus -> real PlanningCliProjectionService — asserting the verbatim skip_design warning on single opsx-status and batch opsx-status-list reads with clean-entry absence. Mutation red proven by removing the kernel copy (2 failed) and restoring. - F-B3: Release README law and Pinned CLI clean-build law rotated to the v14 current line (full pinned SHA); research-plan README row @^14. Focused gates: projection + kernel-warnings + service suites green, core tsc clean. --- AGENTS.md | 4 +- .../loop/checkpoints.md | 3 +- .../loop/implementation.md | 35 ++ .../loop/research-plan.md | 2 +- .../core/src/planning-cli-projection.test.ts | 49 ++- packages/core/src/planning-cli-projection.ts | 12 +- .../planning-cli-projection-service.test.ts | 309 +++++++++++++++++- 7 files changed, 406 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4b67f3cc..8b36240a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -188,7 +188,7 @@ MUST READ: CLAUDE.md - Always run CI-equivalent local checks before opening/updating a PR. - Required local checks (match CI gates): `pnpm format:check`, `pnpm lint:ci`, `pnpm typecheck`, `pnpm test:ci`, `pnpm test:browser:ci` (or a clearly scoped subset when changes are package-local and justified in PR notes). - Every changed TypeScript/TSX physical file, including tests, must retain an accurate timestamped orthogonal-intent/original-request header; a production-only header audit is incomplete. -- Release README law (2026-08-19): every release that updates the repository `README.md` must also update the `openspecui` CLI package README (`packages/cli/README.md`) in the same delivery. The package README is the npm-facing document scoped to the current release line only (v12 today: CLI >=1.12.0 <1.13.0), while the repository README keeps the full version table and legacy archive links. Owner request (2026-08-19): "以后发新版本的时候,除了主仓库的 README.md 要更新,这个 cli 包的 README.md 也要更新。" +- Release README law (2026-08-19): every release that updates the repository `README.md` must also update the `openspecui` CLI package README (`packages/cli/README.md`) in the same delivery. The package README is the npm-facing document scoped to the current release line only (v14 today: CLI >=1.14.0 <1.15.0), while the repository README keeps the full version table and legacy archive links. Owner request (2026-08-19): "以后发新版本的时候,除了主仓库的 README.md 要更新,这个 cli 包的 README.md 也要更新。" - Review handoff law: the reviewer owns research, independent review, OpenSpec artifact corrections, and an executable `GOAL.md`; the assigned worker owns `openspec-apply-change`, implementation, tests, checkpoint updates, commits, and PR delivery. A worker Goal must request applied code and evidence, not another plan-only or review-only pass. - Browser acceptance ownership law (2026-07-20): the owner performs every final end-to-end browser walkthrough and manual acceptance. Agents may build and run only focused Vitest coverage and basic component-level Playwright fixtures. Automated component/fixture results are preparation evidence, never a substitute for the owner's final walkthrough; worker Goals must stop at that boundary and report the exact automated evidence. - Windows native-watcher fixture law (2026-08-08): a successful logical watcher acquisition is not a Windows fixture settlement barrier. After a test acquires a real native watcher, the first external mutation must follow the shared Windows-only settlement helper; fixture cleanup must close shared watcher owners before bounded lock-release removal. This timing belongs to test fixtures and never changes production polling or business deadlines. @@ -564,7 +564,7 @@ proof exist. - 6.14 asynchronous Compose ownership law (2026-07-21): A dirty Compose draft belongs to the Root generation that captured the edit, even when preparation for Root A is still pending while Root B becomes current. B preparation may settle before A; a late A completion must not overwrite B's target, clear the draft, or relabel the draft as B. Same-generation `observedAt` refresh preserves the existing editor/dialog instance. Prove this through the real mounted Compose route and typed preparation boundary; do not bypass a disabled fieldset or call `preparePayload`/terminal mocks manually. Mutation-resistance must remove only the generation/reconciliation transition that retires the stale A completion and show the same test fail. - Independent-observable evidence law (2026-07-19): HTTP and WebSocket readiness timestamps are local observations, not a cross-transport happens-before contract. Tests must not assert `ws.observedAt >= http.observedAt` or use arbitrary sleeps/retries to manufacture ordering; compare each channel's typed payload/readiness and record causal ordering only where the protocol provides an explicit token or sequence. - Pinned browser-fixture law (2026-07-19): Readiness evidence must execute the direct pinned OpenSpec CLI executable with an explicitly isolated `XDG_DATA_HOME`; `npx` runner resolution is network-dependent and can produce a false cold-start timeout. Remove proxy variables from bounded local probes. Classify that timeout as fixture/network evidence, not a server startup or transport defect, and do not add a production workaround for it. -- Pinned CLI clean-build law (updated 2026-09-26): `references/openspec` pins source at the newest line's tag (v13: v1.13.2) while upstream ignores generated `dist`; a local `dist` is non-authoritative evidence. Any clean CI lane that directly executes `references/openspec/bin/openspec.js` must initialize the gitlink, verify the exact pinned SHA recorded in the newest `references/openspec--report.md` (v13: `db2309783547a14e150dbcbfc19120e4028446c3`), install the nested lockfile without outer-workspace coupling or lifecycle scripts, explicitly build, and assert `dist/cli/index.js` before running pinned integration fixtures. Do not add this preparation to Browser shards that never execute the pinned CLI. +- Pinned CLI clean-build law (updated 2026-10-02): `references/openspec` pins source at the newest line's tag (v14: v1.14.0) while upstream ignores generated `dist`; a local `dist` is non-authoritative evidence. Any clean CI lane that directly executes `references/openspec/bin/openspec.js` must initialize the gitlink, verify the exact pinned SHA recorded in the newest `references/openspec--report.md` (v14: `94ca9c1eb15d1b49c06c988419b75c3d95f8b2b5`), install the nested lockfile without outer-workspace coupling or lifecycle scripts, explicitly build, and assert `dist/cli/index.js` before running pinned integration fixtures. Do not add this preparation to Browser shards that never execute the pinned CLI. - Dashboard projection law (2026-07-16): Dashboard Spec, Change, task, and workflow metrics come only from the CLI-selected Planning root. The page names that path, root source, optional Store id, and direct Reference diagnostic counts without inferring Reference health or completeness. Its `Code Git Snapshot` remains Launch code-repository scoped; a distinct Planning repository is shown independently and never substitutes for Code activity. Formal workflow completion may be labeled `workflow-complete`, but only CLI validate/archive outcomes may claim archive readiness. - Change list law (2026-07-16): Active Changes come only from the current writable Planning root; Launch-only and Reference content never enter the list. Workflow state consumes the full formal `TrackedTaskPhase`, not a derived completion boolean: `no-tasks` renders explicitly, only `complete` may contribute to `Workflow Complete`, and neither status claims archive readiness. - Change detail law (2026-07-16): Reactive Status, artifact Instructions, and Apply Instructions use typed CLI contracts with the Root Context-derived explicit Store selector. Live `ChangeStatus.provenance` preserves `planningHome`, `changeRoot`, artifact/resolved/existing paths, next steps, action context, and root source; static provenance is explicitly unavailable. Change detail shows those paths plus direct Reference diagnostics without inferred health/completeness. `Update` remains a first-class action. Archive is never authorized from `status.isComplete`; it opens the CLI dialog, runs strict validation, stops on failure, and preserves validate/archive diagnostics without synthesized retry. diff --git a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md index 53816fa7..45bc3a3a 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md @@ -49,7 +49,8 @@ Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下, - [x] Changeset major; README law (repo en/zh + CLI package); CLAUDE.md pointer; AGENTS.md decision. - [x] Full gates (format/lint/typecheck/browser + explicit per-package tests); pre-existing flakes disclosed (xterm storybook Pixi flake isolated-rerun clean, full browser gate green on re-run). -- [ ] PR opened; CI green; Codex final review disposition recorded. +- [x] PR opened (#297); CI green; Codex final review disposition recorded (Round-F 7.8 REVISE → + F-B1/F-B2/F-B3 fixed with mutation red evidence; awaiting Round-G narrow re-check). - [ ] Owner acceptance walkthrough (Owner-only). - [ ] Archive/release — Owner decision. - [ ] GitHub issues handled with objective links. diff --git a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md index 65e83e6f..33a6b4d5 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md @@ -255,3 +255,38 @@ first failing package, so a core failure masks server/web — full re-run requir flake disclosed, not papered over. - SSG static minimal set (`entry-client-static` + `static-data-provider.opsx`) — pass (no static-surface changes in this line beyond shared projections). + +### Round-F implementation review disposition + fixes (2026-10-02) + +Codex Round-F (herdr v14-change-reviewer, report /tmp/v14-implementation-review.md): **7.8/10 REVISE**, +three blockers — all accepted and fixed in the worktree: + +- **F-B1 `archived` lost at the Planning projection boundary (real bug, independently reproduced by + the reviewer).** The `opsx-change-list` entry schema in `planning-cli-projection.ts` listed only + five members, so `PlanningCliProjectionDataSchema.parse` stripped the CLI-contract `archived` fact. + Fixed by adding `archived: z.boolean().optional()` with the absent-when-default law documented, + plus three projection assertions (true from `--archived`, mixed false/true from `--all`, absent + and unsynthesized on the default active-only list). The Server service reuses the same core schema + (no independent mirror), so the boundary parse now retains the member; the Round-F B2 transport + suite below exercises the real Server parse path. +- **F-B2 Server-visible status-warnings transport evidence missing.** research-plan Slice 2 promised + real-payload → kernel → Server-visible output on single AND batch paths, but only Core kernel and + Web component tests existed. Added a real-kernel describe in + `planning-cli-projection-service.test.ts`: fake 1.14 executable → real `CliExecutor` → real + `OpsxKernel` (`projectWorkflowStatus` included) → real `PlanningCliProjectionService` — asserting + the verbatim executed `skip_design` warning on single `opsx-status` and batch `opsx-status-list` + Server reads, and absent-without-synthesis on clean entries. Mutation red proven: removing the + kernel `warnings` copy fails the two warning-presence tests (2 failed | 1 passed — the absent case + legitimately passes without the copy); restored, all green. +- **F-B3 governance drift.** Release README law "(v12 today…)" → v14 window; Pinned CLI clean-build + law current pin → v14 `v1.14.0` / full SHA `94ca9c1eb15d1b49c06c988419b75c3d95f8b2b5`; research-plan + README row `@^13` → `@^14`. rg audit: no `v12 today` / current-line `v13: v1.13.2` remains in + active governance text. + +Non-blocking notes taken for record (not blocking delivery): schema refinements for `line`/ +`sourcePath`, spec title "three-valued" wording in the config-center delta heading, a Server +`spec.document` show-name forward test, and `StatusWarningsNotice` key stability under duplicate +warning strings. The Owner walkthrough boundary is unchanged. + +Verification: projection suite + kernel warnings suite + service suite green; core tsc clean; +kernel mutation fully restored (`git diff` empty on opsx-kernel.ts). diff --git a/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md b/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md index d1c84aaf..040a83c8 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/research-plan.md @@ -152,7 +152,7 @@ Red/green per group, recorded separately. ## Slice 7 — Release preparation (integrator) -Changeset (major: core/server/web/openspecui); README law (repo en/zh `@^13` row + archived 1.13 copies + +Changeset (major: core/server/web/openspecui); README law (repo en/zh `@^14` row + archived 1.13 copies + CLI package README scoped to v14); CLAUDE.md pointer to the 1.14 report; AGENTS.md architecture decision + submodule pin line. Full gates: format/lint/typecheck/browser + per-package test runs (explicit list, not recursive-only); disclose pre-existing flakes honestly (path-realpath, load-sensitive startup timing). diff --git a/packages/core/src/planning-cli-projection.test.ts b/packages/core/src/planning-cli-projection.test.ts index 4f50c6be..34b89711 100644 --- a/packages/core/src/planning-cli-projection.test.ts +++ b/packages/core/src/planning-cli-projection.test.ts @@ -17,7 +17,8 @@ * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — change-list nested/warnings projection (update-openspec-cli-1131 Slice 2). * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — typed contracts Slice 2 (target-openspec-cli-114-line). - */ + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — per-entry `archived` three-state projection assertions (true / mixed --all / default absent). +*/ import { describe, expect, it } from 'vitest' import { PlanningCliProjectionDataSchema } from './planning-cli-projection.js' @@ -306,4 +307,50 @@ describe('PlanningCliProjectionDataSchema opsx-change-list namespace/warnings', // The namespace set stays the structural fact even without display evidence. expect(parsed.namespaces).toEqual(['area']) }) + + it('preserves the OpenSpec 1.14 per-entry archived fact on the shared projection', () => { + // `list --archived` payload: the archived entry carries `archived: true`. + const payload = changeListPayload() + payload.entries[0]!.archived = true + const parsed = PlanningCliProjectionDataSchema.parse(payload) + + if (parsed.kind !== 'opsx-change-list') throw new Error('expected change-list projection') + expect(parsed.entries[0]?.archived).toBe(true) + }) + + it('preserves mixed archived false/true facts from a list --all payload', () => { + const payload = changeListPayload() + payload.entries.push( + { + name: 'shipped-change', + completedTasks: 3, + totalTasks: 3, + lastModified: '2026-09-30T00:00:00.000Z', + status: 'complete' as const, + archived: false, + }, + { + name: 'archived-change', + completedTasks: 1, + totalTasks: 1, + lastModified: '2026-08-30T00:00:00.000Z', + status: 'complete' as const, + archived: true, + } + ) + payload.value = ['real-change', 'shipped-change', 'archived-change'] + const parsed = PlanningCliProjectionDataSchema.parse(payload) + + if (parsed.kind !== 'opsx-change-list') throw new Error('expected change-list projection') + expect(parsed.entries.map((entry) => entry.archived)).toEqual([undefined, false, true]) + }) + + it('keeps archived absent and unsynthesized on the default active-only list', () => { + // The default `list --json` payload carries no `archived` member at all. + const parsed = PlanningCliProjectionDataSchema.parse(changeListPayload()) + + if (parsed.kind !== 'opsx-change-list') throw new Error('expected change-list projection') + expect(parsed.entries[0]?.archived).toBeUndefined() + expect('archived' in (parsed.entries[0] as Record)).toBe(false) + }) }) diff --git a/packages/core/src/planning-cli-projection.ts b/packages/core/src/planning-cli-projection.ts index 8e441280..b750146c 100644 --- a/packages/core/src/planning-cli-projection.ts +++ b/packages/core/src/planning-cli-projection.ts @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-08-01 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Define selector-exact Planning-root CLI Projection Work contracts. * 2. Define the runtime-environment Environment Global projection contract. * 3. Publish browser-safe lifecycle Pull schemas without importing Node-owned Core modules. @@ -8,9 +8,13 @@ * `entries`/`value` stay actionable-only, `namespaces` is the single structural * namespace-name set for row builders, and `warnings` is passthrough display evidence * (absent when the CLI omitted it, never a synthesized empty array). + * 6. Carry the OpenSpec 1.14 per-entry `archived` inventory fact as optional typed evidence + * (true from `--archived`, mixed false/true from `--all`, absent on the default list — + * never synthesized), so the shared Planning projection is not lossy at its boundary. * * Original request (2026-07-26): "展开全面的接口升级和内核升级和测试升级。" * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — change-list nested/warnings projection (update-openspec-cli-1131 Slice 2). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — the entry schema gains the optional 1.14 `archived` fact. */ import { z } from 'zod' import type { CliJsonValue } from './cli-contracts/command-result.js' @@ -83,6 +87,12 @@ export const PlanningCliProjectionDataSchema = z.discriminatedUnion('kind', [ totalTasks: z.number(), lastModified: z.string(), status: z.enum(['no-tasks', 'complete', 'in-progress']), + /** + * OpenSpec 1.14 per-entry archive inventory fact: `true` from `list --archived`, + * `false`/`true` mixes from `list --all`, and absent on the default active-only + * list (and on pre-1.14 CLIs). Preserved as typed evidence; never synthesized. + */ + archived: z.boolean().optional(), }) ), /** diff --git a/packages/server/src/planning-cli-projection-service.test.ts b/packages/server/src/planning-cli-projection-service.test.ts index a990a377..0c296145 100644 --- a/packages/server/src/planning-cli-projection-service.test.ts +++ b/packages/server/src/planning-cli-projection-service.test.ts @@ -1,5 +1,5 @@ /** - * Orthogonal intents (updated 2026-09-12 Asia/Shanghai): + * Orthogonal intents (updated 2026-10-02 Asia/Shanghai): * 1. Prove selector-exact isolation, including Reference-content invalidation, and same-identity single-flight. * 2. Prove retained A becomes display-only during B and survives a refresh failure. * 3. Prove an initial failure publishes no fabricated Planning CLI snapshot. @@ -7,15 +7,23 @@ * 5. Prove lifecycle Push carries no business data while every Instructions selector remains isolated. * 6. Prove OpenSpec 1.13 Apply `warnings`/`missingPrerequisites` evidence survives the projection * Work boundary verbatim without altering state or progress semantics. + * 7. Prove OpenSpec 1.14 Status `warnings` reach the Server-visible projection output through + * the REAL OpsxKernel (fake 1.14 executable -> CliExecutor -> projectWorkflowStatus -> + * this service) on BOTH the single `opsx-status` and batch `opsx-status-list` selectors, + * with clean entries never synthesizing the member. * * Original request (2026-07-26): "展开全面的接口升级和内核升级和测试升级。" * Original request (2026-07-26): "public Pull retains full CliProjection failure evidence." * Original request (2026-09-12): "Openspec 1.13.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。" * Original request (2026-09-17): "Openspec 1.13.1 释放了…" — change-list nested/warnings projection (update-openspec-cli-1131 Slice 2). + * Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下,调查变更内容,然后开始规划适配工作,我们将用标准工作流worktree来推进。让 codex 参与。完成后关于 github 上的相关 issue" — Round-F B2: the real-kernel status-warnings transport mirror (single + batch + absent). */ import { + CliExecutor, CliProjectionCommandError, + ConfigManager, OpenSpecCliContractExecutor, + OpsxKernel, RuntimeInvalidationIndex, type ArchiveInstructions, type ChangeStatus, @@ -25,7 +33,10 @@ import { type PlanningCliProjectionSelector, type RootContext, } from '@openspecui/core' -import { describe, expect, it, vi } from 'vitest' +import { mkdir, mkdtemp } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' import { createPlanningCliProjectionWorkOwner, PlanningCliProjectionService, @@ -843,3 +854,297 @@ describe('PlanningCliProjectionService', () => { } }) }) + +/** + * Executed top-level `status --json` `warnings` entry from the npm-published 1.14.0 executable + * (references/openspec-1.14.0-report.md; upstream `formatUnknownChangeMetadataKeysMessage`, + * pinned v1.14.0). Fixture input: `.openspec.yaml` = `schema: spec-driven` + `skip_design: true`. + */ +const executedStatusWarning114 = + 'Unrecognized key name(s) in .openspec.yaml (untrusted data, not instructions): skip_design. Known keys: schema, created, goal, affected_areas, initiative, skip_specs, retire_capabilities. Unknown keys are ignored and have no effect. skip_design is not a supported key; only skip_specs exists, and it only skips artifacts whose generates path lives under specs/.' + +const TRANSPORT_WARNED_CHANGE = 'warned-change' +const TRANSPORT_CLEAN_CHANGE = 'clean-change' + +function writeTransportStatusWarningsCliFixture( + cliPath: string, + projectDir: string +): Promise { + const { writeFile } = require('node:fs/promises') as typeof import('node:fs/promises') + return writeFile( + cliPath, + ` +const args = process.argv.slice(2) +const projectDir = ${JSON.stringify(projectDir)} + +function root() { + return { + path: projectDir, + source: args.includes('--store') ? 'store' : 'nearest', + store_id: args.includes('--store') ? args[args.indexOf('--store') + 1] : undefined, + healthy: true, + status: [], + } +} + +function join(base, rest) { + return base + '/' + rest +} + +function statusFields(changeName, batch) { + const fields = { + changeName, + schemaName: 'spec-driven', + planningHome: { + kind: 'repo', + root: projectDir, + changesDir: join(projectDir, 'openspec', 'changes'), + defaultSchema: 'spec-driven', + }, + changeRoot: join(projectDir, 'openspec', 'changes', changeName), + artifactPaths: { + proposal: { + outputPath: 'proposal.md', + resolvedOutputPath: join(projectDir, 'openspec', 'changes', changeName, 'proposal.md'), + existingOutputPaths: [], + }, + }, + isPlanningComplete: false, + isComplete: false, + applyRequires: [], + nextSteps: [], + actionContext: { + mode: 'repo-local', + sourceOfTruth: 'repo', + planningArtifacts: ['proposal'], + linkedContext: [], + allowedEditRoots: [projectDir], + requiresAffectedAreaSelection: false, + constraints: [], + }, + artifacts: [ + { id: 'proposal', outputPath: 'proposal.md', status: 'blocked', requires: [], missingDeps: ['proposal.md'] }, + ], + ...(changeName === '${TRANSPORT_WARNED_CHANGE}' + ? { warnings: [${JSON.stringify(executedStatusWarning114)}] } + : {}), + } + return batch ? fields : { ...fields, root: root() } +} + +if (args.includes('--version')) { + console.log('1.14.0') + process.exit(0) +} + +if (args[0] === 'list' && args.includes('--json')) { + console.log(JSON.stringify({ + changes: ['${TRANSPORT_WARNED_CHANGE}', '${TRANSPORT_CLEAN_CHANGE}'].map((name) => ({ + name, + completedTasks: 0, + totalTasks: 1, + lastModified: '2026-10-02T00:00:00.000Z', + status: 'in-progress', + })), + root: root(), + status: [], + })) + process.exit(0) +} + +if (args[0] === 'status' && args.includes('--all') && args.includes('--json')) { + console.log(JSON.stringify({ + changes: ['${TRANSPORT_WARNED_CHANGE}', '${TRANSPORT_CLEAN_CHANGE}'].map((name) => statusFields(name, true)), + root: root(), + })) + process.exit(0) +} + +if (args[0] === 'status' && args.includes('--change')) { + const changeName = args[args.indexOf('--change') + 1] + console.log(JSON.stringify(statusFields(changeName, false))) + process.exit(0) +} + +console.error('Unsupported args:', args.join(' ')) +process.exit(1) +`.trimStart(), + 'utf8' + ) +} + +describe('PlanningCliProjectionService OpenSpec 1.14 status warnings transport (real kernel)', () => { + const tempDirs: string[] = [] + + async function prepareRealKernelFixture(): Promise<{ + service: PlanningCliProjectionService + runtime: ReturnType + dispose: () => Promise + }> { + const projectDir = await mkdtemp(join(tmpdir(), 'openspecui-server-status-warnings-')) + tempDirs.push(projectDir) + await mkdir(join(projectDir, 'openspec', 'schemas'), { recursive: true }) + await Promise.all( + [TRANSPORT_WARNED_CHANGE, TRANSPORT_CLEAN_CHANGE].map((changeId) => + mkdir(join(projectDir, 'openspec', 'changes', changeId), { recursive: true }) + ) + ) + + const cliPath = join(projectDir, 'fake-openspec.mjs') + await writeTransportStatusWarningsCliFixture(cliPath, projectDir) + + const config = new ConfigManager(projectDir) + await config.writeConfig({ cli: { command: process.execPath, args: [cliPath] } }) + const executor = new CliExecutor(config, projectDir) + const realKernel = new OpsxKernel(projectDir, executor, new RuntimeInvalidationIndex(), {}) + + const runtime = createServerProjectionWorkRuntime() + const invalidation = new RuntimeInvalidationIndex() + const workOwner = createPlanningCliProjectionWorkOwner(runtime) + const contracts = new OpenSpecCliContractExecutor(async () => ({ + success: false, + stdout: '', + stderr: 'Unexpected CLI contract execution.', + exitCode: 1, + })) + const documentService: PlanningCliProjectionServiceOptions['documentService'] = { + readSpec: async () => { + throw new Error('Unexpected Spec read.') + }, + readSpecRaw: async () => { + throw new Error('Unexpected raw Spec read.') + }, + } + const service = new PlanningCliProjectionService({ + rootContext: createRootContext(), + gitBindingToken: 'planning-binding-transport', + // The REAL kernel methods carry the payload through CliExecutor -> + // projectWorkflowStatus -> this service; nothing is stubbed on the status path. + kernel: { + readStatusProjection: (change) => realKernel.readStatusProjection(change), + readChangeListProjection: () => realKernel.readChangeListProjection(), + readStatusListProjection: () => realKernel.readStatusListProjection(), + readInstructionsProjection: realKernel.readInstructionsProjection.bind(realKernel), + readApplyInstructionsProjection: realKernel.readApplyInstructionsProjection.bind( + realKernel + ), + readArchiveInstructionsProjection: + realKernel.readArchiveInstructionsProjection.bind(realKernel), + readConfigBundleProjection: realKernel.readConfigBundleProjection.bind(realKernel), + readTemplatesProjection: realKernel.readTemplatesProjection.bind(realKernel), + readTemplateContentsProjection: realKernel.readTemplateContentsProjection.bind(realKernel), + }, + documentService, + contracts, + invalidation, + storeObservation: { subscribe: () => () => {} }, + workOwner, + }) + return { + service, + runtime, + dispose: async () => { + realKernel.dispose() + await executor.dispose() + runtime.clear() + }, + } + } + + it('carries the executed 1.14 warnings verbatim from the real kernel to the single opsx-status Server output', async () => { + const fixture = await prepareRealKernelFixture() + const selector = { + kind: 'opsx-status', + change: TRANSPORT_WARNED_CHANGE, + } satisfies PlanningCliProjectionSelector + const subscription = fixture.service.subscribe(selector, () => {}) + try { + await vi.waitFor(() => { + expect(fixture.service.read(selector)).toMatchObject({ + state: 'ready', + data: { + kind: 'opsx-status', + value: { + changeName: TRANSPORT_WARNED_CHANGE, + warnings: [executedStatusWarning114], + }, + }, + }) + }) + const snapshot = fixture.service.read(selector) + if (!snapshot || snapshot.state !== 'ready' || snapshot.data?.kind !== 'opsx-status') { + throw new Error('expected ready opsx-status snapshot') + } + expect(snapshot.data.value.warnings?.[0]).toBe(executedStatusWarning114) + expect(snapshot.data.value.warnings?.[0]).toContain('skip_design is not a supported key') + } finally { + subscription.unsubscribe() + await fixture.dispose() + } + }) + + it('carries entry-level 1.14 warnings verbatim on the batch opsx-status-list Server output', async () => { + const fixture = await prepareRealKernelFixture() + const selector = { kind: 'opsx-status-list' } satisfies PlanningCliProjectionSelector + const subscription = fixture.service.subscribe(selector, () => {}) + try { + await vi.waitFor(() => { + const snapshot = fixture.service.read(selector) + expect(snapshot?.state).toBe('ready') + expect(snapshot?.data?.kind).toBe('opsx-status-list') + }) + const snapshot = fixture.service.read(selector) + if (!snapshot || snapshot.state !== 'ready' || snapshot.data?.kind !== 'opsx-status-list') { + throw new Error('expected ready opsx-status-list snapshot') + } + const warned = snapshot.data.value.find( + (status) => status.changeName === TRANSPORT_WARNED_CHANGE + ) + const clean = snapshot.data.value.find( + (status) => status.changeName === TRANSPORT_CLEAN_CHANGE + ) + if (!warned || !clean) throw new Error('expected both transport changes in the status list') + expect(warned.warnings).toEqual([executedStatusWarning114]) + // Clean entries never synthesize the member. + expect(clean.warnings).toBeUndefined() + expect('warnings' in clean).toBe(false) + } finally { + subscription.unsubscribe() + await fixture.dispose() + } + }) + + it('keeps warnings absent on a clean single-change Server output', async () => { + const fixture = await prepareRealKernelFixture() + const selector = { + kind: 'opsx-status', + change: TRANSPORT_CLEAN_CHANGE, + } satisfies PlanningCliProjectionSelector + const subscription = fixture.service.subscribe(selector, () => {}) + try { + await vi.waitFor(() => { + expect(fixture.service.read(selector)).toMatchObject({ + state: 'ready', + data: { + kind: 'opsx-status', + value: { changeName: TRANSPORT_CLEAN_CHANGE }, + }, + }) + }) + const snapshot = fixture.service.read(selector) + if (!snapshot || snapshot.state !== 'ready' || snapshot.data?.kind !== 'opsx-status') { + throw new Error('expected ready opsx-status snapshot') + } + expect(snapshot.data.value.warnings).toBeUndefined() + expect('warnings' in snapshot.data.value).toBe(false) + } finally { + subscription.unsubscribe() + await fixture.dispose() + } + }) + + afterEach(async () => { + const { rm } = await import('node:fs/promises') + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))) + }) +}) From 3a5328c0680cbc42eebfe22dd75a0d3fc3aadfa8 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 2 Oct 2026 06:47:31 +0800 Subject: [PATCH 12/12] docs(openspec): record the Round-G APPROVE (9.2/10) closing the v14 review arc --- .../target-openspec-cli-114-line/loop/checkpoints.md | 4 ++-- .../loop/implementation.md | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md index 45bc3a3a..6e26c840 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/checkpoints.md @@ -49,8 +49,8 @@ Original request (2026-10-02): "Openspec v1.14.0 释放了,你更新一下, - [x] Changeset major; README law (repo en/zh + CLI package); CLAUDE.md pointer; AGENTS.md decision. - [x] Full gates (format/lint/typecheck/browser + explicit per-package tests); pre-existing flakes disclosed (xterm storybook Pixi flake isolated-rerun clean, full browser gate green on re-run). -- [x] PR opened (#297); CI green; Codex final review disposition recorded (Round-F 7.8 REVISE → - F-B1/F-B2/F-B3 fixed with mutation red evidence; awaiting Round-G narrow re-check). +- [x] PR opened (#297); CI green; Codex final review disposition recorded — Round-F 7.8 REVISE → + F-B1/F-B2/F-B3 fixed with mutation red evidence → Round-G 9.2/10 APPROVE (all three closed). - [ ] Owner acceptance walkthrough (Owner-only). - [ ] Archive/release — Owner decision. - [ ] GitHub issues handled with objective links. diff --git a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md index 33a6b4d5..abb0434d 100644 --- a/openspec/changes/target-openspec-cli-114-line/loop/implementation.md +++ b/openspec/changes/target-openspec-cli-114-line/loop/implementation.md @@ -290,3 +290,14 @@ warning strings. The Owner walkthrough boundary is unchanged. Verification: projection suite + kernel warnings suite + service suite green; core tsc clean; kernel mutation fully restored (`git diff` empty on opsx-kernel.ts). + +### Round-G narrow re-check (herdr v14-change-reviewer, 2026-10-02, commit e133e58a) — 9.2/10, APPROVE + +F-B1 closed (shared PlanningCliProjectionDataSchema retains `entries[].archived` with the three-state +law; Server reuses the same schema — no second mirror; reviewer independently re-ran the projection + +kernel suites). F-B2 closed (real-kernel transport suite proves the verbatim warning on single and +batch Server outputs; mutation-red record consistent; no kernel residue). F-B3 closed (governance +current-line facts rotated; rg audit clean; Core tsc independently green). No blockers; disposition +APPROVE — deliverable. Report appended to /tmp/v14-implementation-review.md (Round-G section). + +Review arc: planning A-E 6.2 → 7.4 → 7.8 → 8.1 → 9.1 GO; implementation F 7.8 REVISE → G 9.2 APPROVE.