Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .changeset/nightly-self-reported-version.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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: <string>, scaffoldVersion: <number> }`
- **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: <string>, scaffoldVersion: <number> }`
- **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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
8 changes: 5 additions & 3 deletions openspec/specs/analytics/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
45 changes: 45 additions & 0 deletions openspec/specs/cli-nightly-builds/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
theCodeDrift marked this conversation as resolved.

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
7 changes: 6 additions & 1 deletion openspec/specs/cli/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Loading
Loading