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
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
# Dependencies
node_modules/

# Build output (prod, dev, and self targets each emit to their own directory)
# Build output (the prod and self targets each emit to their own directory)
**/dist/
**/dist-dev/
**/dist-self/
*.tsbuildinfo
.turbo/
Expand Down
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ without building the repository yourself.

**A nightly is also how you use this CLI from another repository.** To exercise
unreleased behavior somewhere other than this checkout, merge it to `main` and
install the nightly that follows.
install the nightly that follows. No build target emits an artifact runnable
outside this checkout: `pnpm build:self` bakes in a repo-root-relative path and
only works from here. That is deliberate, and it is the reason a nightly is the
answer rather than a workaround. Unmerged work is exercised in this repository
or not at all.

```bash
npx @taskless/cli-nightly@latest --version
Expand Down
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,27 @@ npx @taskless/cli@latest info
## Local development

The CLI bakes its own invocation string into the skill, command, and recipe
content it installs. Three build targets pick that string, all driven by the
content it installs. Build targets pick that string, all driven by the
`TASKLESS_BUILD_TARGET` env var via Vite `define` (same source files, no edits):

Each target also emits to its own directory so the three never overwrite one
another. Prod → `dist/`, dev → `dist-dev/`, self → `dist-self/` (all
gitignored):
Each target also emits to its own directory so they never overwrite one another.
Prod → `dist/`, self → `dist-self/` (both gitignored):

| Command | Output dir | Baked invocation | Use for |
| ----------------- | ------------ | ------------------------------------------- | ------------------------------------------------------------------------------------ |
| `pnpm build` | `dist/` | `npx @taskless/cli` | Production / published builds (default). |
| `pnpm build:dev` | `dist-dev/` | `node <abs>/packages/cli/dist-dev/index.js` | Validating this build from **another** repo (absolute path resolves anywhere). |
| `pnpm build:self` | `dist-self/` | `node packages/cli/dist-self/index.js` | Dogfooding **in this repo** (path is repo-root-relative; run the CLI from the root). |
| Command | Output dir | Baked invocation | Use for |
| ----------------- | ------------ | -------------------------------------- | ------------------------------------------------------------------------------------ |
| `pnpm build` | `dist/` | `npx @taskless/cli` | Production / published builds (default). |
| `pnpm build:self` | `dist-self/` | `node packages/cli/dist-self/index.js` | Dogfooding **in this repo** (path is repo-root-relative; run the CLI from the root). |

`pnpm build:self` builds the CLI with the relative invocation and then runs
`taskless init --no-interactive` to install into this repo, so `.claude` gets
real reference stubs that delegate to the canonical `.taskless/` content, exactly
like any other install. (This replaces the former raw-symlink `link-skills`
step, so local dogfooding always matches a true install.)

> The `dev`/`self` invocations are local paths and must never be published:
> only `pnpm build` (or `pnpm package`) produces a release artifact.
> The `self` invocation is a local path and must never be published: only
> `pnpm build` (or `pnpm package`) produces a release artifact. It is also
> relative to this repo root, so no build target here yields a CLI you can run
> from another repository. That case is served by a published nightly.

### Running the local build

Expand Down
1 change: 0 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export default tseslint.config(
ignores: [
"node_modules/",
"**/dist/",
"**/dist-dev/",
"**/dist-self/",
"**/*.config.js",
"**/*.config.ts",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
## Why

The `dev` build target exists for exactly one job: emit a CLI whose baked
invocation is an absolute path (`node /abs/path/packages/cli/dist-dev/index.js`)
so that an unmerged build in this checkout can be exercised from **another**
repository. Nothing else distinguishes it from `self`, which does the same
dogfooding inside this repository with a repo-relative path.

That job now has a better answer. Other repositories consume
`@taskless/cli-nightly`, a published, version-pinned artifact that resolves on
any machine and that the `nightly` target already emits correctly. An absolute
path baked into shipped skill and recipe content is the weaker mechanism: it is
machine-specific, it names a path that may not exist, and it needs a build
notice banner to explain itself.

Keeping `dev` costs a fourth branch in every place the target set is spelled
out: the resolver, the output directories, the invocation switch, the notice
builder, two package manifests, the turbo cache, the eslint ignores, two
tsconfigs, the README table, and the specified build-output contract.

## What Changes

- **The `dev` target is removed.** `build:dev` in both manifests, `OUT_DIRS.dev`,
the `dev` branches of `resolveCliInvocation` and `resolveCliNotice`, and every
mention of `dist-dev` in tooling configuration.
- **`TASKLESS_BUILD_TARGET=dev` is rejected, not ignored.** An unknown target
still falls back to `prod`. A stale `dev` does not: it names a retired target
and gets an error saying so. Falling back would hand the caller a prod build
in `dist/` while they went looking for `dist-dev/index.js`, which is a
confusing failure at a path nobody will grep for.
- **`self`, `prod`, and `nightly` are untouched.** Their output directories,
invocations, notices, and version handling are unchanged.
- **The accepted trade is documented.** `CONTRIBUTING.md` states plainly that no
build target emits an artifact runnable outside this checkout, and that this
is deliberate.

The trade: unmerged work can no longer be exercised from another repository.
Inside this repository `build:self` still covers it; elsewhere the work merges
to `main` and the nightly that follows carries it. This is intended.

**Delivery is a single PR.** The target is one concept spelled out in a dozen
places, and removing half of them leaves a build that does not run. Spec,
implementation, and archive land together.

## Capabilities

### Modified Capabilities

- `infrastructure`: the set of cached build outputs no longer includes
`dist-dev/**`, because no build emits it.

## Impact

- **Modified**: `package.json`, `packages/cli/package.json`,
`packages/cli/scripts/build-target.ts`,
`packages/cli/test/build-target.test.ts`, `turbo.json`, `eslint.config.js`,
`.gitignore`, `tsconfig.json`, `packages/cli/tsconfig.json`,
`packages/cli/tsconfig.prompts.json`, `packages/cli/vite.config.ts`,
`packages/cli/src/util/invocation.ts`, `README.md`, `CONTRIBUTING.md`.
- **Unchanged**: every code path for `prod`, `self`, and `nightly`; the
published package's behavior; `openspec/changes/archive/**`, whose references
to `build:dev` record what was true when they landed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## MODIFIED Requirements

### Requirement: Build pipeline runs across all packages

The root `pnpm build` command SHALL invoke `turbo run build`, which runs the `build` script in every workspace package that defines one. Build outputs (`dist/**`, `dist-self/**`) SHALL be cached.

The cached output set SHALL name only directories a build target emits. `dist-dev/**` is no longer among them: the `dev` build target has been removed, and no build writes that directory.

#### Scenario: Root build command runs CLI build

- **WHEN** `pnpm build` is run at the repo root
- **THEN** Turborepo SHALL execute `build` in `@taskless/cli`
- **AND** the CLI `dist/` output SHALL be produced

#### Scenario: Cached build skips re-execution

- **WHEN** `pnpm build` is run twice without source changes
- **THEN** the second run SHALL be a cache hit and complete near-instantly

#### Scenario: No cached output is declared for a removed target

- **WHEN** `turbo.json` is inspected
- **THEN** the `build` task outputs SHALL NOT include `dist-dev/**`

## ADDED Requirements

### Requirement: The CLI has three build targets

The CLI SHALL support exactly three build targets, selected by `TASKLESS_BUILD_TARGET`: `prod` (the default, emitting `dist/` with the published `npx @taskless/cli` invocation), `self` (emitting `dist-self/` with a repo-root-relative path, for dogfooding inside this repository), and `nightly` (emitting `dist/` with `npx @taskless/cli-nightly@<version>`).

There SHALL NOT be a build target that emits an invocation runnable from outside this repository's checkout. Exercising unreleased behavior elsewhere is served by a published nightly, which resolves on any machine and is pinned to the build whose instructions it carries, rather than by an absolute filesystem path baked into shipped content.

An unset or unrecognized `TASKLESS_BUILD_TARGET` SHALL resolve to `prod`. The retired value `dev` SHALL be an exception: it SHALL fail the build with an error naming the target as removed, because a caller who sets it is looking for an output directory that will not be written, and a silent fall back to `prod` would surface only as a missing file at a path nothing else mentions.

#### Scenario: The self target stays repo-relative

- **WHEN** a build runs with `TASKLESS_BUILD_TARGET=self`
- **THEN** the output SHALL be written to `dist-self/`
- **AND** the baked invocation SHALL be a path relative to the repo root

#### Scenario: A retired target fails loudly

- **WHEN** a build runs with `TASKLESS_BUILD_TARGET=dev`
- **THEN** the build SHALL fail with an error stating the target has been removed
- **AND** SHALL NOT fall back to the `prod` target

#### Scenario: An unknown target still defaults to prod

- **WHEN** a build runs with `TASKLESS_BUILD_TARGET` unset or set to an unrecognized value other than `dev`
- **THEN** the build SHALL resolve to the `prod` target
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Delivery shape: **single PR**. One build target spelled out across a dozen files; a partial removal leaves a build that does not run. Spec, implementation, and archive land together.

## 1. Build machinery

- [x] 1.1 Drop `OUT_DIRS.dev` and the `dev` case from `resolveCliInvocation`
- [x] 1.2 Reject `TASKLESS_BUILD_TARGET=dev` in `resolveBuildTarget` rather than falling back to prod
- [x] 1.3 Drop the `dev` branch of `resolveCliNotice`
- [x] 1.4 Remove `build:dev` from `package.json` and `packages/cli/package.json`

## 2. Tooling configuration

- [x] 2.1 Remove `dist-dev/**` from `turbo.json` outputs
- [x] 2.2 Remove `**/dist-dev/` from `eslint.config.js` and `.gitignore`
- [x] 2.3 Remove `dist-dev` from the root and CLI `tsconfig.json` excludes

## 3. Prose

- [x] 3.1 Update comments in `vite.config.ts`, `tsconfig.prompts.json`, and `src/util/invocation.ts`
- [x] 3.2 Update the README build-target table and surrounding text
- [x] 3.3 Strengthen the `CONTRIBUTING.md` nightlies paragraph to state the trade plainly

## 4. Tests

- [x] 4.1 Replace the dev-target assertions with a rejection test
- [x] 4.2 Narrow the per-target parameterised lists to prod/self/nightly

## 5. Verification

- [x] 5.1 `pnpm build`, `pnpm build:self`, `pnpm typecheck`, `pnpm lint`, `pnpm test`
- [x] 5.2 `pnpm openspec validate --all --strict`
- [x] 5.3 `pnpm cli check` shows no new Vale findings
- [x] 5.4 No changeset: a development build target is removed and the published package's behavior does not change
34 changes: 33 additions & 1 deletion openspec/specs/infrastructure/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ The repository SHALL have `turbo` as a root devDependency and a `turbo.json` con

### Requirement: Build pipeline runs across all packages

The root `pnpm build` command SHALL invoke `turbo run build`, which runs the `build` script in every workspace package that defines one. Build outputs (`dist/**`, `dist-dev/**`, `dist-self/**`) SHALL be cached.
The root `pnpm build` command SHALL invoke `turbo run build`, which runs the `build` script in every workspace package that defines one. Build outputs (`dist/**`, `dist-self/**`) SHALL be cached.

The cached output set SHALL name only directories a build target emits. `dist-dev/**` is no longer among them: the `dev` build target has been removed, and no build writes that directory.

#### Scenario: Root build command runs CLI build

Expand All @@ -119,6 +121,11 @@ The root `pnpm build` command SHALL invoke `turbo run build`, which runs the `bu
- **WHEN** `pnpm build` is run twice without source changes
- **THEN** the second run SHALL be a cache hit and complete near-instantly

#### Scenario: No cached output is declared for a removed target

- **WHEN** `turbo.json` is inspected
- **THEN** the `build` task outputs SHALL NOT include `dist-dev/**`

### Requirement: Test pipeline runs across all packages

The root `pnpm test` command SHALL invoke `turbo run test`, which runs the `test` script in every workspace package that defines one. The test pipeline SHALL depend on the build pipeline so that built artifacts are available.
Expand Down Expand Up @@ -431,3 +438,28 @@ Publishing workflows SHALL NOT be serialized merely because they publish. Where

- **WHEN** two runs of a publishing flow evaluate the same commit or version
- **THEN** at most one artifact SHALL be published, and the other run SHALL neither fail nor publish a duplicate

### Requirement: The CLI has three build targets

The CLI SHALL support exactly three build targets, selected by `TASKLESS_BUILD_TARGET`: `prod` (the default, emitting `dist/` with the published `npx @taskless/cli` invocation), `self` (emitting `dist-self/` with a repo-root-relative path, for dogfooding inside this repository), and `nightly` (emitting `dist/` with `npx @taskless/cli-nightly@<version>`).

There SHALL NOT be a build target that emits an invocation runnable from outside this repository's checkout. Exercising unreleased behavior elsewhere is served by a published nightly, which resolves on any machine and is pinned to the build whose instructions it carries, rather than by an absolute filesystem path baked into shipped content.

An unset or unrecognized `TASKLESS_BUILD_TARGET` SHALL resolve to `prod`. The retired value `dev` SHALL be an exception: it SHALL fail the build with an error naming the target as removed, because a caller who sets it is looking for an output directory that will not be written, and a silent fall back to `prod` would surface only as a missing file at a path nothing else mentions.

#### Scenario: The self target stays repo-relative

- **WHEN** a build runs with `TASKLESS_BUILD_TARGET=self`
- **THEN** the output SHALL be written to `dist-self/`
- **AND** the baked invocation SHALL be a path relative to the repo root

#### Scenario: A retired target fails loudly

- **WHEN** a build runs with `TASKLESS_BUILD_TARGET=dev`
- **THEN** the build SHALL fail with an error stating the target has been removed
- **AND** SHALL NOT fall back to the `prod` target

#### Scenario: An unknown target still defaults to prod

- **WHEN** a build runs with `TASKLESS_BUILD_TARGET` unset or set to an unrecognized value other than `dev`
- **THEN** the build SHALL resolve to the `prod` target
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"scripts": {
"build": "pnpm build:compile",
"build:compile": "turbo run build",
"build:dev": "pnpm --filter @taskless/cli build:dev",
"build:nightly": "pnpm --filter @taskless/cli build:nightly",
"build:self": "run-s build:self:compile build:self:install",
"build:self:compile": "pnpm --filter @taskless/cli build:self",
Expand Down
1 change: 0 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
},
"scripts": {
"build": "vite build && tsc -p tsconfig.prompts.json",
"build:dev": "TASKLESS_BUILD_TARGET=dev vite build",
"build:nightly": "TASKLESS_BUILD_TARGET=nightly vite build && tsc -p tsconfig.prompts.json",
"build:self": "TASKLESS_BUILD_TARGET=self vite build",
"generate:api": "openapi-typescript https://app.taskless.io/cli/api/__schema -o src/generated/api.d.ts",
Expand Down
Loading
Loading