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
5 changes: 5 additions & 0 deletions .changeset/onboard-decide-when-rules-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskless/cli": patch
---

Onboarding now settles when the new rules run before it asks to mark itself complete. After the rules are materialized, the onboard recipe tells the user which CI systems and commit-hook tools the repository has, offers to wire `check` into CI (`agent ci`) and into a pre-commit hook (the new `agent hooks` topic), and asks the consent-gated mark-complete question on its own, so a "yes" can no longer be read as an answer to both. `detect --json` gains two additive fields for this, `ci` and `hooks`, read from configuration at the repository root.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-05
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## Context

Onboarding ends with rules on disk. Whether those rules ever run depends on CI or a commit hook calling `check`, and the onboard recipe never raised either. The `ci` topic existed but nothing pointed to it from onboarding, and no topic covered a local hook.

## Goals / Non-Goals

**Goals:**

- The onboard flow asks when the rules run before it asks about `--mark-complete`, and never puts the two questions in one message.
- The agent starts from facts the CLI measured (which CI systems and which hook tools are configured) rather than re-deriving them by listing files.
- A recipe for the pre-commit case that is right about `check`'s actual behavior.

**Non-Goals:**

- Installing a hook manager. The recipe wires `check` into the tool the repository already uses, and asks before introducing one.
- Telling whether an existing CI workflow already runs Taskless. That needs reading workflow contents for intent, which is the agent's job, not a deterministic signal.
- Detecting raw `.git/hooks/*` scripts or `core.hooksPath`. Neither is committed, so neither is shared configuration the scan can stand on.

## Decisions

### `detect` reports `ci` and `hooks` as `{ name, evidence }` lists

Same shape as `linters`, so a consumer already handling one handles all three. `evidence` carries the path or the `package.json` marker that triggered the match, which keeps the "every signal is attributable" property `linters` has.

**CI is matched at the scan root only.** CI systems read their config from the repository root, so a `.gitlab-ci.yml` three directories down is a fixture or a vendored project, not this repository's CI. The monorepo-wide walk is right for linter configs and wrong here.

**Hook tools are matched at the scan root too**, for the same reason: git runs one set of hooks per repository, installed from the root. A root `package.json` dependency or config key counts; a sub-package's does not.

lint-staged is reported beside the hook managers even though it is not one. It is what turns "run on commit" into "run on the staged files", which is the question the onboard step asks, and leaving it out would hide the most common way that question is already answered.

Rejected: a single `automation` field mixing both. The onboard step offers CI and hooks as two separate choices, and the `ci` and `hooks` topics each read one list.

### The hook runs the repository's pinned binary, spelled `<local-taskless>`

A hook should run the version the repository pins as a dev dependency, so the hook, CI and every developer agree. That is a different binary from `%(TASKLESS_CLI)s` (how this recipe was fetched) and from `%(PACKAGE_MANAGER_DLX)s` (a download-and-run launcher, which defeats the pin). The recipe names it with an agent-fill marker, `<local-taskless>`, and lists once what it expands to per package manager. Because the marker is never followed by a literal subcommand, the cross-reference guard that rejects hand-written invocations has nothing to flag and needs no allowlist entry, unlike `ci.md`'s `pnpm taskless check`.

Rejected: a new sprintf placeholder resolved at render time. The CLI cannot know the target repository's package manager at render time any better than the agent can by looking at the lockfile, and a value guessed at render time reads as measured.

### `--no-stash` is presented with its cost

`check` edits no tracked file (its working files go under `.taskless/.run/` and are removed when the run ends), so lint-staged's backup stash protects nothing for a `check`-only task, and that stash goes on the stack every worktree shares. But lint-staged documents that `--no-stash` also implies `--no-hide-partially-staged`, so a partially staged file is checked as it is in the working tree, not as it is staged. The recipe states both and leaves the trade to the user rather than recommending the flag unconditionally.

### Step order in onboard

The new step sits after materialization (there must be rules to run) and before the mark-complete question. The mark-complete question keeps its existing consent wording and gains one constraint: it is asked in a message that asks nothing else.

## Risks / Trade-offs

- [A hook tool's config syntax drifts] → The recipe gives the shape per tool and tells the agent to check the tool's own docs for anything beyond it, rather than encoding version-specific detail.
- [`check $FILES` word-splits paths with spaces] → Same limitation the `ci` recipe already carries. lint-staged and pre-commit hand the file list to the command themselves, so the shell-substitution forms (bare husky, simple-git-hooks) are the ones exposed, and the recipe says so.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## Why

The onboard recipe goes straight from materializing rules to asking whether to mark onboarding complete. Nothing in between asks when the new rules will run, so a project can finish onboarding with rules that nothing ever runs. When the agent improvised the missing question, it put it in the same message as the consent-gated `--mark-complete` question, and the user's "yes" could be read as an answer to either (#441).

## What Changes

- The onboard recipe gains a step between materializing rules and marking complete: **decide when the rules run**. The agent reports what the repository already has for CI and for commit hooks, offers to wire `check` into CI (`agent ci`) and into a pre-commit hook (`agent hooks`), and does whichever the user picks. Only after that does it ask the `--mark-complete` question, in a message with no other question in it.
- The onboard recipe's See Also names `agent ci` and `agent hooks`.
- `detect --json` reports two more deterministic, on-disk signals beside `linters`: `ci`, the CI systems configured at the scan root (the same file table the `ci` recipe uses), and `hooks`, the tools that run commands at commit time (husky, lefthook, pre-commit, simple-git-hooks, lint-staged). The new fields are additive. Nothing existing changes shape.
- A new internal agent topic, `hooks`, covers running `check` on staged files before a commit, in whichever hook tool the repository already uses. It records what the agent in #441 had to work out alone: `check` takes paths and skips ones that don't exist, so a staged-file list can go straight in; `check` edits no tracked file, which is what makes lint-staged's `--no-stash` safe, and the recipe states what that flag gives up; a change under `.taskless/rules/` calls for `test` and a full `check`; and the pinned dev dependency is what keeps the hook, CI and every developer on one version.
- The `ci` recipe reads the CI systems from `detect --json` before falling back to its file table, and names `agent hooks` in See Also.
- The installed skill's topic table gains a row for `agent hooks`.

## Capabilities

### New Capabilities

None. The `hooks` topic is registered under the existing `cli-agent` capability, the way `onboard` was.

### Modified Capabilities

- `cli-onboard`: the recipe requirement gains the "decide when the rules run" step and requires the mark-complete question to be asked on its own.
- `cli-detect`: the signal list grows from linters, languages and rule styles to also include CI systems and commit-hook tools, and the JSON-shape requirement names the new fields.
- `cli-agent`: a requirement registers the `hooks` topic.

## Delivery shape

**Single PR.** One recipe step, two additive `detect` fields, and one new recipe, all landing together with their tests and spec deltas, fit well inside one reviewable diff. They are also only coherent together: an onboard step that sends the agent to `agent hooks` before that topic exists would cite a topic that does not resolve, and `recipe-cross-references.test.ts` fails on exactly that.

## Impact

- `packages/cli/src/agent/onboard.md` (topic v4 → v5), `ci.md` (v3 → v4), `detect.md` (v1 → v2), new `hooks.md` (v1).
- `packages/cli/src/detect/scan.ts` and the `detect` output schema: new `ci` and `hooks` arrays.
- `packages/cli/src/prompts/index.ts`: `hooks` joins `INTERNAL_TOPICS`.
- `skills/taskless/SKILL.md`: new topic row.
- `check.md` (v6 → v7): See Also names `agent hooks`.
- Tests: `detect.test.ts` and `onboard.test.ts`. The recipe cross-reference guard needs no new allowlist entry, because the hook's invocation is written as a marker rather than a literal command.
- No API, auth, or network change. `detect` stays offline and deterministic.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## ADDED Requirements

### Requirement: hooks topic is registered

An agent topic `hooks` SHALL be registered. The CLI SHALL embed `packages/cli/src/agent/hooks.md` at build time via the existing `import.meta.glob` mechanism, and `taskless agent hooks` SHALL print it. The topic SHALL be classified as internal to the CLI rather than exported from `@taskless/cli/prompts`, since it walks an agent through editing a developer's repository.

The recipe SHALL describe running `check` on the staged files before a commit, in the hook tool the repository already uses, and SHALL NOT install a hook manager without asking the user first. It SHALL direct the agent to the `hooks` field of `taskless detect --json` for which tools the repository has.

The recipe SHALL be accurate about `check`'s behavior where a hook depends on it:

- `check` accepts file paths and skips paths that do not exist, so a staged-file list that includes deleted files can be passed directly, and a hook SHALL exit cleanly rather than run a full scan when the staged list is empty.
- `check` edits no tracked file. Where the recipe offers lint-staged's `--no-stash` on that basis, it SHALL also state that the flag makes lint-staged check a partially staged file as it is in the working tree rather than as it is staged.
- A staged change under `.taskless/rules/` SHALL be followed by `taskless test` and a full `check`, since a rule change can affect files that were not staged.

The recipe SHALL have the hook run the repository's own pinned `@taskless/cli` dev dependency rather than a download-and-run launcher, so the hook, CI and every developer run one version.

#### Scenario: The hooks topic returns the recipe

- **WHEN** a user runs `taskless agent hooks`
- **THEN** the CLI SHALL print the contents of `hooks.md` to stdout
- **AND** SHALL exit with code 0

#### Scenario: The hooks topic is internal

- **WHEN** the topic classification in `@taskless/cli/prompts` is read
- **THEN** `hooks` SHALL be listed among the internal topics
- **AND** SHALL NOT be an exported prompt topic

#### Scenario: The recipe states the cost of --no-stash

- **WHEN** `hooks.md` mentions lint-staged's `--no-stash`
- **THEN** it SHALL state that a partially staged file is then checked as it is in the working tree

#### Scenario: A rule change triggers test and a full check

- **WHEN** `hooks.md` is read
- **THEN** it SHALL instruct the agent to run `taskless test` and an unscoped `check` when a staged path is under `.taskless/rules/`
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
## MODIFIED Requirements

### Requirement: Detect scans deterministic repo signals only

The `detect` command SHALL emit only deterministic signals derived from files on
disk: configured linters, detected languages, the styles of the repo's own
existing rules, the CI systems configured for the repository, and the tools that
run commands at commit time. It SHALL NOT perform any LLM inference and SHALL NOT match the
request against any catalog of known packaged linter rules.

Detection follows a languages → linters flow: languages are inferred first, and
a linter's dependency evidence is then read from the manifest of that linter's
own language (a node dependency from `package.json`, a Python dependency from
`pyproject.toml`/`requirements.txt`) rather than conflating ecosystems. A
recognized linter config file on disk is honored regardless of the languages
inferred.

CI systems and commit-hook tools are matched at the scan root only, never in a
sub-package. A CI system reads its configuration from the repository root and git
runs one set of hooks per repository, so a match further down the tree is a
fixture or a vendored project rather than this repository's configuration. Each
is reported as a `name` with the `evidence` that matched, in the same shape as a
linter.

#### Scenario: Linter configs are detected from disk

- **WHEN** the working directory contains a recognized linter config (for
example `.eslintrc*`, `eslint.config.js`, `ruff.toml`, a `[tool.ruff]` block in
`pyproject.toml`, `.rubocop.yml`, `biome.json`, `.oxlintrc.json`, or
`stylelint` config)
- **THEN** `detect --json` SHALL report each configured linter it found

#### Scenario: Languages are reported

- **WHEN** `detect --json` runs in a repository
- **THEN** the output SHALL include the languages inferred from manifest and
marker files present on disk and from the linters detected

#### Scenario: A linter dependency is sourced from its own language's manifest

- **WHEN** a dependency-evidenced linter (for example `ruff`) is named only in a
manifest belonging to a different language (for example `package.json`)
- **THEN** `detect --json` SHALL NOT report that linter from the mismatched
manifest

#### Scenario: oxlint is detected from its config or its dependency

- **WHEN** the working directory contains one of the config files oxlint
discovers on its own (`.oxlintrc.json`, `.oxlintrc.jsonc`, `oxlint.config.ts`,
`oxlint.config.mts`), or names `oxlint` as a dependency in `package.json`
- **THEN** `detect --json` SHALL report `oxlint` as a JavaScript/TypeScript linter
- **AND** a repository configured only for eslint SHALL NOT report `oxlint`

#### Scenario: Configs in monorepo sub-packages are detected

- **WHEN** a linter config or language manifest lives in a sub-package rather
than the repository root (for example `packages/api/.eslintrc.json`)
- **THEN** `detect --json` SHALL detect it and SHALL carry the path it was found
at in the linter's evidence
- **AND** the scan SHALL prune a curated set of ignored directories (for example
`node_modules`, `.git`, build output) and SHALL bound traversal depth

#### Scenario: The repo's own rule styles are surfaced

- **WHEN** the working directory contains existing rule definitions (for example
custom linter rules or `.taskless/rules/`)
- **THEN** `detect --json` SHALL surface a description of those existing rule
styles for downstream authoring

#### Scenario: No packaged-rule catalog matching

- **WHEN** `detect --json` runs
- **THEN** the output SHALL NOT claim a request maps to a specific named packaged
rule (such matching is left to the authoring recipe, not the command)

#### Scenario: CI systems are detected from their root config

- **WHEN** the scan root contains a recognized CI configuration (for example
`.github/workflows/*.yml`, `.gitlab-ci.yml`, `.circleci/config.yml`,
`Jenkinsfile`, `azure-pipelines.yml`, `bitbucket-pipelines.yml`,
`.buildkite/`, `.drone.yml`, or `.travis.yml`)
- **THEN** `detect --json` SHALL report each such CI system under `ci`, with the
matching paths as its evidence

#### Scenario: A CI config below the root is not this repository's CI

- **WHEN** a CI configuration file exists only in a sub-directory (for example
`packages/api/.gitlab-ci.yml`)
- **THEN** `detect --json` SHALL NOT report that CI system

#### Scenario: Commit-hook tools are detected from config or root dependency

- **WHEN** the scan root contains a recognized hook tool's configuration (for
example a `.husky/` directory, `lefthook.yml`, `.pre-commit-config.yaml`, a
`simple-git-hooks` or `lint-staged` config file, or a `simple-git-hooks` or
`lint-staged` key in the root `package.json`), or the root `package.json`
names the tool as a dependency
- **THEN** `detect --json` SHALL report the tool under `hooks`, with what matched
as its evidence

#### Scenario: No CI and no hooks are reported as empty lists

- **WHEN** the scan root carries no recognized CI configuration and no
recognized hook tool
- **THEN** `detect --json` SHALL report `ci` and `hooks` as empty arrays rather
than omitting them

### Requirement: Detect emits a stable JSON shape

When `--json` is set, `detect` SHALL emit a single structured JSON object whose
shape is validated internally against a stable Zod output schema before being
printed, consistent with how other `--json` commands in the CLI (e.g. `info`,
`check`) validate their output. The schema is an internal contract, not a
published artifact, and `detect` does not expose a `--schema` mode.

#### Scenario: JSON output validates against the internal schema

- **WHEN** `detect --json` succeeds
- **THEN** stdout SHALL be a single JSON object that the command has validated
against its internal output schema (linters, languages, existing rule styles,
CI systems, commit-hook tools)
Loading
Loading