diff --git a/.changeset/nightly-self-reported-version.md b/.changeset/nightly-self-reported-version.md new file mode 100644 index 00000000..b6e72969 --- /dev/null +++ b/.changeset/nightly-self-reported-version.md @@ -0,0 +1,30 @@ +--- +"@taskless/cli": patch +--- + +A nightly now reports the version it is, not the release it anticipates. + +Installing a nightly wrote the previous release into `.taskless/taskless.json` +— `install.cliVersion: "0.10.2"` — while the skills written beside it, by the +same command in the same run, pinned every invocation to +`@taskless/cli-nightly@0.11.0-…`. The manifest attributed the install to a +version that never performed it, which matters because `install.cliVersion` is +what answers "what installed this?", and that question gets asked precisely +when someone is running a nightly to reproduce unreleased behavior. + +A nightly's version is stamped when the publishable artifact is produced, and +the committed `package.json` is deliberately left untouched — so the build was +reading a file that could not know the answer. It now takes the same stamp that +names the published package, so the version a nightly reports and the version +it sends an agent to are the same string by construction. + +This also corrects `taskless --version`, the CLI version in recipe headers, and +the `cliVersion` telemetry property on nightly builds. Released builds are +unaffected. A nightly that cannot determine its own version now fails the build +rather than quietly reporting the released one. + +The build now also refuses to emit a nightly whose reported version and +embedded invocation disagree. Both derive from the same stamp, so they cannot +diverge today — but that was true of the two values in this bug as well, right +up until one of them started reading `package.json` instead. Deriving from one +source is not the same as being checked against it. diff --git a/openspec/changes/archive/2026-08-24-nightly-self-reported-version/proposal.md b/openspec/changes/archive/2026-08-24-nightly-self-reported-version/proposal.md new file mode 100644 index 00000000..ee98d568 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-nightly-self-reported-version/proposal.md @@ -0,0 +1,63 @@ +## Why + +A nightly reports the version of the release it anticipates, not the version +it is. Installing `@taskless/cli-nightly@0.11.0-20260823220841xd9dee3d` writes +`install.cliVersion: "0.10.2"` into `.taskless/taskless.json`, while the skills +emitted beside it — in the same run, by the same build — pin every command to +the nightly's exact version. + +The cause is an ordering the nightly design chose deliberately. The version is +stamped when the publishable artifact is produced, and the committed manifest +is left alone; but `__VERSION__` is baked from that committed manifest at build +time. So `package.json` is asked a question it cannot answer, and it answers +confidently with the previous release. + +This is quiet rather than loud, which is what makes it worth fixing. Nothing +errors. The manifest simply attributes the install to a version that never +performed it — and `install.cliVersion` exists precisely to answer "what +installed this?", which is the question asked when a nightly is being used to +reproduce unreleased behavior. + +## What Changes + +- **A build reports the version it is published as.** `resolveCliVersion` + returns the stamped nightly version for the `nightly` target and the + committed package version for every other, and the build's `__VERSION__` + define reads it instead of `package.json` directly. +- **The refusal matches the invocation's.** A `nightly` build with no stamp + throws rather than falling back, reusing `resolveNightlyVersion`. The + plausible fallback is the wrong answer that produced this bug. +- **Nothing else moves.** The committed manifest is still untouched by a + nightly build, which the existing requirement demands. + +This corrects `taskless --version`, the `%(CLI_VERSION)s` recipe header, the +`cliVersion` telemetry property, and `install.cliVersion` — all four read the +same define. + +**Delivery is a single PR.** One pure function, its call site, tests, and a +spec delta. + +## Capabilities + +### Modified Capabilities + +- `cli-nightly-builds`: a nightly reports its own published version as its CLI + version, rather than the released version its committed manifest declares. + +## Impact + +- **Modified**: `packages/cli/scripts/build-target.ts` (new `resolveCliVersion`), + `packages/cli/vite.config.ts` (the `__VERSION__` define), + `packages/cli/test/build-target.test.ts`. +- **Unchanged**: `packages/cli/package.json`, and `.github/scripts/nightly-pack.cjs` + — the stamp is already computed once and already passed to the build as + `TASKLESS_NIGHTLY_VERSION`. This change consumes a value that was already + there rather than plumbing a new one. +- **Unchanged**: the `assert-skill-versions` build check, which compares skill + frontmatter against the committed `package.json`. That is a claim about + source files in the repository, not about what a build reports, and a nightly + must not rewrite it. +- **Not fixed retroactively**: manifests already written by a nightly still + record the anticipated release. The next install corrects them. + +**Tracking:** taskless/cli#148 diff --git a/openspec/changes/archive/2026-08-24-nightly-self-reported-version/specs/analytics/spec.md b/openspec/changes/archive/2026-08-24-nightly-self-reported-version/specs/analytics/spec.md new file mode 100644 index 00000000..cf7adba0 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-nightly-self-reported-version/specs/analytics/spec.md @@ -0,0 +1,30 @@ +## MODIFIED Requirements + +### Requirement: All capture calls include standard properties + +Every `capture()` call SHALL include the `cli` property (anonymous UUID), the `cliVersion` property (the version the build reports as its own, baked in at build time), and the `scaffoldVersion` property (the `version` field from `.taskless/taskless.json`, or `0` if the manifest is absent or unreadable). When authenticated, the `groups` parameter SHALL include `{ organization: String(orgId) }`. The `cliVersion` and `scaffoldVersion` values SHALL be resolved once at telemetry initialization and attached to every subsequent `capture()` call without re-reading the source files. + +#### Scenario: Anonymous capture includes standard properties + +- **WHEN** `capture("cli_run")` is called without authentication +- **THEN** the event SHALL include `{ cli: anonymousUuid, cliVersion: , scaffoldVersion: }` +- **AND** the event SHALL NOT include a `groups` parameter + +#### Scenario: Authenticated capture includes standard properties and group + +- **WHEN** `capture("cli_rule_created")` is called with authentication +- **THEN** the event SHALL include `{ cli: anonymousUuid, cliVersion: , scaffoldVersion: }` +- **AND** the `groups` parameter SHALL include `{ organization: String(orgId) }` + +#### Scenario: Scaffold version falls back to 0 when manifest missing + +- **WHEN** `getTelemetry(cwd)` is initialized in a directory with no `.taskless/taskless.json` +- **THEN** every `capture()` call from the returned client SHALL include `scaffoldVersion: 0` + +#### Scenario: CLI version is the version the build reports as its own + +- **WHEN** `getTelemetry()` is initialized +- **THEN** `cliVersion` SHALL be the version the build reports as its own, bundled at build time +- **AND** for every build target but `nightly` that SHALL be the version in `packages/cli/package.json` +- **AND** for a `nightly` it SHALL be the version the nightly is published under, so events are attributed to the build that emitted them rather than to the release it anticipates +- **AND** SHALL be attached to every event emitted through the returned client diff --git a/openspec/changes/archive/2026-08-24-nightly-self-reported-version/specs/cli-nightly-builds/spec.md b/openspec/changes/archive/2026-08-24-nightly-self-reported-version/specs/cli-nightly-builds/spec.md new file mode 100644 index 00000000..d3c83ac1 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-nightly-self-reported-version/specs/cli-nightly-builds/spec.md @@ -0,0 +1,46 @@ +## ADDED Requirements + +### Requirement: A nightly reports the version it is published as + +A nightly build SHALL report, as its own CLI version, the version it is published under — not the released version declared by the package manifest in version control. This applies wherever the build states its version, including the version command, the CLI version embedded in recipe headers, telemetry, and the CLI version recorded when it installs into a project. + +The version SHALL come from the same stamp used to name the published package, so that the version a nightly reports and the version an agent is sent to are the same string by construction rather than by coincidence. + +A nightly build with no stamp available SHALL fail rather than report the committed version. Reporting the released version is the failure this requirement exists to prevent, and a build that cannot name itself has no correct value to substitute. + +Builds other than nightly SHALL report the committed package version, which for those targets is the version they are. + +A nightly build SHALL verify, before emitting, that the version it reports and the version its embedded invocation names are the same. Deriving both from one stamp is not sufficient on its own: the two were derived from a single source and still diverged, because nothing compared them. + +#### Scenario: A nightly records its own version when it installs + +- **WHEN** a nightly installs into a project +- **THEN** the CLI version recorded in the project manifest SHALL be the nightly's published version +- **AND** SHALL match the version pinned in the skills and commands emitted by that same install + +#### Scenario: A nightly states its own version when asked + +- **WHEN** a nightly is asked for its version +- **THEN** it SHALL report its published version rather than the released version its committed manifest declares + +#### Scenario: A nightly build without a version stamp fails + +- **WHEN** a nightly build is attempted with no version stamp available +- **THEN** the build SHALL fail +- **AND** SHALL NOT fall back to the committed package version + +#### Scenario: A nightly that disagrees with itself is not emitted + +- **WHEN** a nightly build's reported version and the invocation it embeds name different versions +- **THEN** the build SHALL fail +- **AND** SHALL NOT emit the artifact + +#### Scenario: A released build reports the committed version + +- **WHEN** a non-nightly build is asked for its version +- **THEN** it SHALL report the version declared by the committed package manifest + +#### Scenario: Reporting its own version does not modify the committed manifest + +- **WHEN** a nightly build has run +- **THEN** the CLI package manifest in version control SHALL still declare the released version diff --git a/openspec/changes/archive/2026-08-24-nightly-self-reported-version/specs/cli/spec.md b/openspec/changes/archive/2026-08-24-nightly-self-reported-version/specs/cli/spec.md new file mode 100644 index 00000000..1fc3be28 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-nightly-self-reported-version/specs/cli/spec.md @@ -0,0 +1,15 @@ +## MODIFIED Requirements + +### Requirement: CLI version is injected at build time + +The CLI's `vite.config.ts` SHALL use the Vite `define` option to replace a `__VERSION__` sentinel with the version the build reports as its own, resolved at build time. For every build target but `nightly` that is the version string in `packages/cli/package.json`. A `nightly` is the exception, because its version is stamped when the publishable artifact is produced and the committed manifest is deliberately left unchanged, so `package.json` names the release the nightly anticipates rather than the nightly itself (see the `cli-nightly-builds` capability). No runtime file reads or import assertions SHALL be used for version resolution. + +#### Scenario: Build replaces version sentinel + +- **WHEN** `pnpm build` is run in `packages/cli/` +- **THEN** all occurrences of `__VERSION__` in source code SHALL be replaced with the literal version string from package.json + +#### Scenario: A nightly build replaces the sentinel with its own stamped version + +- **WHEN** a `nightly` build is run +- **THEN** `__VERSION__` SHALL be replaced with the version the nightly is published under, not the version in package.json diff --git a/openspec/changes/archive/2026-08-24-nightly-self-reported-version/tasks.md b/openspec/changes/archive/2026-08-24-nightly-self-reported-version/tasks.md new file mode 100644 index 00000000..746b82e8 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-nightly-self-reported-version/tasks.md @@ -0,0 +1,26 @@ +Delivery shape: **single PR**. A pure function, one call site, tests, and a spec delta — nothing here can land independently of the rest. + +## 1. Reproduce before fixing + +- [x] 1.1 Build a nightly locally (`TASKLESS_NIGHTLY_VERSION=… pnpm --filter @taskless/cli build:nightly`) and confirm it reports the committed version while pinning the nightly invocation — the two halves of the report, from one artifact +- [x] 1.2 Confirm the write path is not itself at fault: `wizard/index.ts` persists `getCliVersion()`, and reads `previousState.cliVersion` only to render the diff, so nothing is preserving a stale value + +## 2. Fix + +- [x] 2.1 Add `resolveCliVersion(environment, packageVersion)` to `build-target.ts`, delegating to `resolveNightlyVersion` for the nightly target so the no-stamp refusal is shared rather than restated +- [x] 2.2 Read it from the `__VERSION__` define in `vite.config.ts` +- [x] 2.3 Leave `assert-skill-versions` reading `pkg.version` — it asserts about committed source, not about the build + +## 3. Tests + +- [x] 3.1 Nightly reports the stamped version, not the committed one +- [x] 3.2 A nightly with no stamp throws, naming the env var +- [x] 3.3 prod/dev/self and an empty environment all keep the committed version + +## 4. Verification + +- [x] 4.1 Nightly build reports the stamped version in `--version` and in the recipe header +- [x] 4.2 Prod build still reports the committed version +- [x] 4.3 End-to-end: `init` under a nightly build writes the stamped version to `install.cliVersion` +- [x] 4.4 `pnpm --filter @taskless/cli build` then `test`, `pnpm typecheck`, `pnpm lint`, `pnpm openspec validate --all --strict` +- [x] 4.5 Changeset diff --git a/openspec/specs/analytics/spec.md b/openspec/specs/analytics/spec.md index 65b378df..8d8e2989 100644 --- a/openspec/specs/analytics/spec.md +++ b/openspec/specs/analytics/spec.md @@ -107,7 +107,7 @@ The PostHog client SHALL be created with `flushAt: 1` and `flushInterval: 0` bec ### Requirement: All capture calls include standard properties -Every `capture()` call SHALL include the `cli` property (anonymous UUID), the `cliVersion` property (the `@taskless/cli` package version read from `package.json`), and the `scaffoldVersion` property (the `version` field from `.taskless/taskless.json`, or `0` if the manifest is absent or unreadable). When authenticated, the `groups` parameter SHALL include `{ organization: String(orgId) }`. The `cliVersion` and `scaffoldVersion` values SHALL be resolved once at telemetry initialization and attached to every subsequent `capture()` call without re-reading the source files. +Every `capture()` call SHALL include the `cli` property (anonymous UUID), the `cliVersion` property (the version the build reports as its own, baked in at build time), and the `scaffoldVersion` property (the `version` field from `.taskless/taskless.json`, or `0` if the manifest is absent or unreadable). When authenticated, the `groups` parameter SHALL include `{ organization: String(orgId) }`. The `cliVersion` and `scaffoldVersion` values SHALL be resolved once at telemetry initialization and attached to every subsequent `capture()` call without re-reading the source files. #### Scenario: Anonymous capture includes standard properties @@ -126,10 +126,12 @@ Every `capture()` call SHALL include the `cli` property (anonymous UUID), the `c - **WHEN** `getTelemetry(cwd)` is initialized in a directory with no `.taskless/taskless.json` - **THEN** every `capture()` call from the returned client SHALL include `scaffoldVersion: 0` -#### Scenario: CLI version is resolved from package.json +#### Scenario: CLI version is the version the build reports as its own - **WHEN** `getTelemetry()` is initialized -- **THEN** `cliVersion` SHALL be read from `packages/cli/package.json` (bundled at build time or read at runtime) +- **THEN** `cliVersion` SHALL be the version the build reports as its own, bundled at build time +- **AND** for every build target but `nightly` that SHALL be the version in `packages/cli/package.json` +- **AND** for a `nightly` it SHALL be the version the nightly is published under, so events are attributed to the build that emitted them rather than to the release it anticipates - **AND** SHALL be attached to every event emitted through the returned client ### Requirement: CLI events use cli\_ prefix diff --git a/openspec/specs/cli-nightly-builds/spec.md b/openspec/specs/cli-nightly-builds/spec.md index 34990f6d..77603d0b 100644 --- a/openspec/specs/cli-nightly-builds/spec.md +++ b/openspec/specs/cli-nightly-builds/spec.md @@ -237,3 +237,48 @@ The annotation SHALL depend on the publish having succeeded, and SHALL be perfor - **WHEN** a push publishes no nightly - **THEN** no job holding permission to write pull requests SHALL be instantiated for it + +### Requirement: A nightly reports the version it is published as + +A nightly build SHALL report, as its own CLI version, the version it is published under — not the released version declared by the package manifest in version control. This applies wherever the build states its version, including the version command, the CLI version embedded in recipe headers, telemetry, and the CLI version recorded when it installs into a project. + +The version SHALL come from the same stamp used to name the published package, so that the version a nightly reports and the version an agent is sent to are the same string by construction rather than by coincidence. + +A nightly build with no stamp available SHALL fail rather than report the committed version. Reporting the released version is the failure this requirement exists to prevent, and a build that cannot name itself has no correct value to substitute. + +Builds other than nightly SHALL report the committed package version, which for those targets is the version they are. + +A nightly build SHALL verify, before emitting, that the version it reports and the version its embedded invocation names are the same. Deriving both from one stamp is not sufficient on its own: the two were derived from a single source and still diverged, because nothing compared them. + +#### Scenario: A nightly records its own version when it installs + +- **WHEN** a nightly installs into a project +- **THEN** the CLI version recorded in the project manifest SHALL be the nightly's published version +- **AND** SHALL match the version pinned in the skills and commands emitted by that same install + +#### Scenario: A nightly states its own version when asked + +- **WHEN** a nightly is asked for its version +- **THEN** it SHALL report its published version rather than the released version its committed manifest declares + +#### Scenario: A nightly build without a version stamp fails + +- **WHEN** a nightly build is attempted with no version stamp available +- **THEN** the build SHALL fail +- **AND** SHALL NOT fall back to the committed package version + +#### Scenario: A nightly that disagrees with itself is not emitted + +- **WHEN** a nightly build's reported version and the invocation it embeds name different versions +- **THEN** the build SHALL fail +- **AND** SHALL NOT emit the artifact + +#### Scenario: A released build reports the committed version + +- **WHEN** a non-nightly build is asked for its version +- **THEN** it SHALL report the version declared by the committed package manifest + +#### Scenario: Reporting its own version does not modify the committed manifest + +- **WHEN** a nightly build has run +- **THEN** the CLI package manifest in version control SHALL still declare the released version diff --git a/openspec/specs/cli/spec.md b/openspec/specs/cli/spec.md index dd567826..185579e0 100644 --- a/openspec/specs/cli/spec.md +++ b/openspec/specs/cli/spec.md @@ -268,13 +268,18 @@ The CLI SHALL support a `taskless info` subcommand that outputs a JSON object to ### Requirement: CLI version is injected at build time -The CLI's `vite.config.ts` SHALL use the Vite `define` option to replace a `__VERSION__` sentinel with the version string read from `packages/cli/package.json` at build time. No runtime file reads or import assertions SHALL be used for version resolution. +The CLI's `vite.config.ts` SHALL use the Vite `define` option to replace a `__VERSION__` sentinel with the version the build reports as its own, resolved at build time. For every build target but `nightly` that is the version string in `packages/cli/package.json`. A `nightly` is the exception, because its version is stamped when the publishable artifact is produced and the committed manifest is deliberately left unchanged, so `package.json` names the release the nightly anticipates rather than the nightly itself (see the `cli-nightly-builds` capability). No runtime file reads or import assertions SHALL be used for version resolution. #### Scenario: Build replaces version sentinel - **WHEN** `pnpm build` is run in `packages/cli/` - **THEN** all occurrences of `__VERSION__` in source code SHALL be replaced with the literal version string from package.json +#### Scenario: A nightly build replaces the sentinel with its own stamped version + +- **WHEN** a `nightly` build is run +- **THEN** `__VERSION__` SHALL be replaced with the version the nightly is published under, not the version in package.json + ### Requirement: citty is the argument parser The CLI SHALL use `citty` as its sole argument parsing dependency. Each subcommand SHALL be defined as an isolated command object and registered with the main CLI via `defineCommand` and `runMain`. diff --git a/packages/cli/scripts/build-target.ts b/packages/cli/scripts/build-target.ts index dc56dced..6edd632d 100644 --- a/packages/cli/scripts/build-target.ts +++ b/packages/cli/scripts/build-target.ts @@ -90,6 +90,73 @@ export function resolveNightlyVersion(environment: BuildEnvironment): string { return version; } +/** + * The version this build should report as its own — `__VERSION__`, which backs + * `taskless --version`, the `%(CLI_VERSION)s` recipe header, the `cliVersion` + * telemetry property, and the `install.cliVersion` written into + * `.taskless/taskless.json`. + * + * For every target but `nightly` this is the committed package version. A + * nightly is the exception because its version is stamped at pack time and the + * manifest in version control is deliberately left alone — so reading + * `package.json` at build time yields the version of the release the nightly + * ANTICIPATES, not the version it is published as. + * + * Getting this wrong is quiet rather than loud. A nightly installed to exercise + * unreleased behavior records `install.cliVersion` as the last release, so the + * manifest disagrees with the skills emitted beside it — which pin the exact + * nightly — and anyone reading the manifest to answer "what installed this?" + * gets a version that never wrote it. + */ +export function resolveCliVersion( + environment: BuildEnvironment, + packageVersion: string +): string { + return resolveBuildTarget(environment) === "nightly" + ? resolveNightlyVersion(environment) + : packageVersion; +} + +/** + * Refuse to emit a nightly whose two version-bearing defines disagree. + * + * A post-mortem guard, not a hypothetical one. #148 shipped because + * `__VERSION__` and `__TASKLESS_CLI__` are *derived* from the same stamp but + * were not *checked* against each other: the invocation read + * `TASKLESS_NIGHTLY_VERSION` and the version read `package.json`, so one + * artifact simultaneously announced v0.10.2 and sent every agent to + * v0.11.0-…. Nothing failed. The disagreement was only visible by comparing + * two outputs of the same build against each other, which is exactly what + * nobody does. + * + * They are coupled by construction today. This exists so that if a later + * refactor decouples them — a second env var, a cached value, a default + * reintroduced "for local builds" — the build stops instead of shipping the + * same silent mismatch again. The check is cheap and it fires at the only + * moment the two values are both in hand. + * + * Deliberately asks the resolved defines rather than scanning the emitted + * bundle: the build already holds the structured values, and re-deriving them + * from generated text would be the weaker tool (see the styleguide's + * "verify build output in the build"). + */ +export function assertVersionConsistency( + environment: BuildEnvironment, + version: string, + invocation: string +): void { + if (resolveBuildTarget(environment) !== "nightly") return; + if (invocation.endsWith(`@${version}`)) return; + throw new Error( + `nightly build is inconsistent with itself: it reports version ` + + `${JSON.stringify(version)} but its invocation is ` + + `${JSON.stringify(invocation)}. These must name the same version — a ` + + `build that announces one version and sends agents to another is ` + + `taskless/cli#148. Both derive from ${NIGHTLY_VERSION_ENV}; if they no ` + + `longer do, that is the bug.` + ); +} + /** * The CLI invocation baked into emitted skill/command/recipe content, chosen by * the TASKLESS_BUILD_TARGET env var (see package.json build:dev/build:self/ diff --git a/packages/cli/test/build-target.test.ts b/packages/cli/test/build-target.test.ts index 5886eeef..175c1406 100644 --- a/packages/cli/test/build-target.test.ts +++ b/packages/cli/test/build-target.test.ts @@ -1,11 +1,13 @@ import { describe, expect, it } from "vitest"; import { + assertVersionConsistency, NIGHTLY_VERSION_ENV, OUT_DIRS, resolveBuildTarget, resolveCliInvocation, resolveCliNotice, + resolveCliVersion, resolveOutputDirectory, } from "../scripts/build-target"; @@ -46,6 +48,78 @@ describe("build target resolution", () => { }); }); +describe("the version a build reports as its own", () => { + // Only nightly diverges. dev/self/prod all run from a checkout whose + // package.json IS the version they are, so reading it is correct there. + it.each(["prod", "dev", "self"])( + "uses the committed package version for the %s target", + (target) => { + expect( + resolveCliVersion({ TASKLESS_BUILD_TARGET: target }, "0.10.2") + ).toBe("0.10.2"); + } + ); + + it("uses the committed package version when no target is set", () => { + expect(resolveCliVersion({}, "0.10.2")).toBe("0.10.2"); + }); +}); + +// #148 post-mortem. The two defines were DERIVED from one stamp but never +// CHECKED against each other, so a single artifact announced one version and +// sent every agent to another, silently. These pin the guard that makes that +// state unbuildable rather than merely unlikely. +describe("a build must agree with itself about its version", () => { + it("refuses a nightly whose invocation names a different version", () => { + expect(() => + assertVersionConsistency( + nightlyEnvironment, + "0.10.2", + `npx @taskless/cli-nightly@${NIGHTLY_VERSION}` + ) + ).toThrow(/inconsistent with itself/); + }); + + // The exact shape of the shipped bug, named in the message so whoever hits + // this next has the history rather than just the assertion. + it("names the issue and the env var both values derive from", () => { + expect(() => + assertVersionConsistency( + nightlyEnvironment, + "0.10.2", + `npx @taskless/cli-nightly@${NIGHTLY_VERSION}` + ) + ).toThrow(new RegExp(String.raw`148[\s\S]*` + NIGHTLY_VERSION_ENV)); + }); + + it("accepts a nightly whose two values agree", () => { + expect(() => + assertVersionConsistency( + nightlyEnvironment, + NIGHTLY_VERSION, + `npx @taskless/cli-nightly@${NIGHTLY_VERSION}` + ) + ).not.toThrow(); + }); + + // A version pin is meaningless for the other targets: prod names a floating + // package deliberately, and dev/self name a filesystem path. Asserting there + // would fail every ordinary build. + it.each([ + ["prod", "npx @taskless/cli"], + ["dev", "node /repo/packages/cli/dist-dev/index.js"], + ["self", "node packages/cli/dist-self/index.js"], + ])("does not constrain the %s target", (target, invocation) => { + expect(() => + assertVersionConsistency( + { TASKLESS_BUILD_TARGET: target }, + "0.10.2", + invocation + ) + ).not.toThrow(); + }); +}); + describe("the nightly target", () => { // The whole point of the target: a nightly's skills, commands, and recipes // must send an agent to the package the reader installed. Without the @@ -57,6 +131,26 @@ describe("the nightly target", () => { ); }); + // #148: the reported symptom. A nightly installed to exercise unreleased + // behavior wrote `install.cliVersion: 0.10.2` — the release it anticipates — + // while the skills emitted beside it pinned the nightly it actually was. + // The version is stamped at pack time and the committed manifest is + // deliberately left alone, so `package.json` cannot answer this. + it("reports the stamped version as its own, not the committed one", () => { + expect(resolveCliVersion(nightlyEnvironment, "0.10.2")).toBe( + NIGHTLY_VERSION + ); + }); + + // Same refusal as the invocation: a nightly that cannot name its own version + // has nothing correct to report, and the plausible fallback is the wrong + // answer that started #148. + it("refuses to report a version when the stamp is missing", () => { + expect(() => + resolveCliVersion({ TASKLESS_BUILD_TARGET: "nightly" }, "0.10.2") + ).toThrow(NIGHTLY_VERSION_ENV); + }); + // `dist`, unlike dev/self — the tarball is packed with `files: ["dist"]` and // `bin: ./dist/index.js`, so a nightly build overwrites a prod build. it("emits to dist, the same directory as prod", () => { diff --git a/packages/cli/vite.config.ts b/packages/cli/vite.config.ts index 06b5af1b..2cda547a 100644 --- a/packages/cli/vite.config.ts +++ b/packages/cli/vite.config.ts @@ -8,10 +8,12 @@ import tsconfigPaths from "vite-tsconfig-paths"; import { SKILL_CATALOG } from "./src/install/catalog"; import { + assertVersionConsistency, OUT_DIRS, resolveBuildTarget, resolveCliInvocation, resolveCliNotice, + resolveCliVersion, resolveOutputDirectory, } from "./scripts/build-target"; @@ -219,9 +221,15 @@ function assertPromptsGraph(): Plugin { }; } +const cliVersion = resolveCliVersion(process.env, pkg.version); + +// Fails the build rather than emitting an artifact whose version and +// invocation disagree — the shape of taskless/cli#148. +assertVersionConsistency(process.env, cliVersion, cliInvocation); + export default defineConfig({ define: { - __VERSION__: JSON.stringify(pkg.version), + __VERSION__: JSON.stringify(cliVersion), __TASKLESS_CLI__: JSON.stringify(cliInvocation), __TASKLESS_CLI_NOTICE__: JSON.stringify(cliNotice), },