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
27 changes: 27 additions & 0 deletions .agents/skills/taskless/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
name: taskless
description: |
Use for any Taskless task. Trigger when the user mentions Taskless by name,
or when their request involves the .taskless/ directory or files in it
(rules, rule-tests, rule-metadata).

Specifically:
- "create/add/write a taskless rule for X"
- "improve/fix/iterate on this taskless rule"
- "delete/remove this taskless rule"
- "run taskless", "taskless check", "validate against taskless rules"
- "taskless login/logout/status", "is taskless connected"
- "add taskless to CI", "wire taskless into github actions"
- "onboard with taskless", "set up taskless for this project"

Also trigger on any request to add/write/create a lint or code rule,
including ones that name a specific tool (eslint, ruff, biome, stylelint,
ast-grep). Naming a tool ENGAGES this skill's routing flow via
`npx @taskless/cli agent route`; it does NOT suppress the skill.
metadata:
type: shim
---

This is a Taskless reference stub. The canonical skill is defined at `.taskless/skills/taskless/SKILL.md`.

Read `.taskless/skills/taskless/SKILL.md` and follow its instructions.
11 changes: 11 additions & 0 deletions .changeset/house-style-in-recipes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@taskless/cli": patch
---

Hold the agent-facing recipes to the house writing style.

`packages/cli/src/agent/*.txt` is bundled into the published CLI and served by `taskless agent <topic>`, so it is text users and agents read on every authoring run. It was the largest prose surface the house-style rules did not cover. `no-em-dashes`, `no-blocklist-phrases` and `no-hedging` now reach it, and the 270 existing em and en dashes are rewritten as periods, commas, colons or parentheses depending on what each one was doing.

No instruction changed meaning. The recipe-content tests, which assert exact phrases from `route.txt`, `create-sg-rule.txt`, `create-vale-rule.txt` and others, all still pass.

Two scoping notes worth knowing for anyone widening further. These files are `.txt`, which Vale treats as plain text: there is no markdown parser, so fenced blocks and code spans are **not** skipped the way they are in a `.md` file, and command examples are checked as prose. And `create-vale-rule.txt` and `verify-rule.txt` are excluded from `no-hedging`, because both teach rule authoring through a worked example named `no-simply` and the token appears throughout as an identifier rather than as hedging.
26 changes: 13 additions & 13 deletions .conventions/STYLEGUIDE-CODE.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ interface GitHubComment {

### Export Types Referenced by Public API Signatures

**DO NOT** remove `export` from types that are transitively referenced by exported functions, values, or other exported types even if tools like knip report them as "unused exports." With `declaration: true` in `tsconfig`, TypeScript requires all types in exported signatures to be exported themselves.
**DO NOT** remove `export` from types that are transitively referenced by exported functions, values, or other exported types, even if tools like knip report them as "unused exports." With `declaration: true` in `tsconfig`, TypeScript requires all types in exported signatures to be exported themselves.

Before removing an `export` from a type, check whether any exported function or value references it in its signature (parameters, return types, or fields of other exported types).

Expand All @@ -150,7 +150,7 @@ interface LayerResult { ... } // breaks declaration emit for VerifyResult

- Knip tracks direct import usage, not transitive type reachability through exported signatures
- Removing these exports causes `declaration: true` to fail with "exported function has or is using private name" errors
- The fix is tedious each type must be re-exported individually, often across multiple review cycles
- The fix is tedious: each type must be re-exported individually, often across multiple review cycles

## Cross-Worker Durable Object Access

Expand Down Expand Up @@ -201,7 +201,7 @@ import type { UserDO, GitHubOrganizationDO } from "@taskless/storage";

### Verify Build Output In The Build, Not By Parsing It

**A failing build is still a valid test of the build.** When an invariant is about a build artifact, enforce it where the artifact is produced. If a bundle must not contain something, the build should refuse to emit it, rather than emitting it and leaving a test to go looking afterwards. An invariant enforced at production time cannot be violated; one enforced afterwards can only be detected.
**A failing build is still a valid test of the build.** When an invariant is about a build artifact, enforce it where the artifact is produced. If a bundle must not contain something, the build should refuse to emit it, rather than emitting it and leaving a test to go looking afterwards. An invariant enforced at production time cannot be violated; one enforced afterwards can only be detected.

**DO NOT** reconstruct a fact about generated output by parsing that output.

Expand Down Expand Up @@ -240,7 +240,7 @@ for (const specifier of specifiers) {
}
```

**Tests that _use_ a built artifact are fine.** Importing the built entry and asserting on its behavior, or spawning the built CLI and asserting on its output, are ordinary tests. The rule is not "tests must not touch build output" — it is that tests must not re-derive what the build already knew.
**Tests that _use_ a built artifact are fine.** Importing the built entry and asserting on its behavior, or spawning the built CLI and asserting on its output, are ordinary tests. The rule is not "tests must not touch build output". It is that tests must not re-derive what the build already knew.

```typescript
// ✅ Fine - uses the artifact, asserts on behavior
Expand All @@ -252,27 +252,27 @@ const { stdout } = await execFileAsync("node", [builtCli, "help"]);
expect(stdout).toContain("Usage:");
```

**Do not add a dependency in order to test an assertion.** If a test needs a parser to make sense of an artifact, that is the signal the check is in the wrong place the generator already has the structured data. Reach for a new devDependency only when several tests need it and nothing in the existing toolchain can answer the question.
**Do not add a dependency in order to test an assertion.** If a test needs a parser to make sense of an artifact, that is the signal the check is in the wrong place: the generator already has the structured data. Reach for a new devDependency only when several tests need it and nothing in the existing toolchain can answer the question.

**Worked example.** `packages/cli/test/prompts.test.ts` asserted that the built `dist/prompts.js` chunk graph never reaches the CLI entry or a host capability, by regex-scanning the built JavaScript for `from "…"` to reconstruct the import graph. A built chunk embeds every help recipe as a string literal, and the `engine-selection` recipe contains the phrase `a different axis from "which engine"` so the scan reported `dist/prompts.js graph imports which engine`. Prose was read as an import.
**Worked example.** `packages/cli/test/prompts.test.ts` asserted that the built `dist/prompts.js` chunk graph never reaches the CLI entry or a host capability, by regex-scanning the built JavaScript for `from "…"` to reconstruct the import graph. A built chunk embeds every help recipe as a string literal, and the `engine-selection` recipe contains the phrase `a different axis from "which engine"`, so the scan reported `dist/prompts.js graph imports which engine`. Prose was read as an import.

The fixes that did not work, and why:

| Attempt | Why it was rejected |
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Filter candidates by specifier shape (`/^(?:node:)?[@\w./-]+$/`) | Passed only because that phrase contains a space. Measured against the real bundle the regex yields `["which engine"]` and the filter drops it but `differs from "static-tier"` is a bare hyphenated name with no whitespace and would have been reported. The guard held by luck of punctuation. |
| Add `es-module-lexer` as a devDependency | Parsed the graph correctly, but bought a dependencyand a second major version, since vite already pulls 1.7.0 transitivelyto serve a single test. |
| Anchor the regex to line-start | Matched the lexer exactly on today's bundles, but required `from` on the same line as `import`. A future bundler that wrapped a long import would silently stop detecting real imports trading a loud false positive for a quiet false negative in the guard whose entire job is catching a leak. |
| Attempt | Why it was rejected |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Filter candidates by specifier shape (`/^(?:node:)?[@\w./-]+$/`) | Passed only because that phrase contains a space. Measured against the real bundle the regex yields `["which engine"]` and the filter drops it, but `differs from "static-tier"` is a bare hyphenated name with no whitespace and would have been reported. The guard held by luck of punctuation. |
| Add `es-module-lexer` as a devDependency | Parsed the graph correctly, but bought a dependency, and a second major version since vite already pulls 1.7.0 transitively, to serve a single test. |
| Anchor the regex to line-start | Matched the lexer exactly on today's bundles, but required `from` on the same line as `import`. A future bundler that wrapped a long import would silently stop detecting real imports, trading a loud false positive for a quiet false negative in the guard whose entire job is catching a leak. |

The resolution: rollup's `OutputChunk` already exposes `imports` and `dynamicImports` the exact resolved graph. The check moved into a vite plugin that fails the build, and the test was deleted.
The resolution: rollup's `OutputChunk` already exposes `imports` and `dynamicImports`, the exact resolved graph. The check moved into a vite plugin that fails the build, and the test was deleted.

The same reasoning forbids adding a YAML parser to assert on generated config, or an HTML parser to assert on rendered output. In each case the generator knows the answer and the test is guessing at it.

**Rationale:**

- An invariant enforced at production time cannot be violated; one enforced afterwards can only be detected
- Parsing generated text reconstructs information the generator already had, using a weaker tool
- A check that needs a parser is a check in the wrong place move it to where the structured data lives
- A check that needs a parser is a check in the wrong place; move it to where the structured data lives
- A build that fails is a faster, earlier signal than a test that fails, and it cannot be skipped
- Regexes over generated output are brittle in the worst direction: they break on content that merely resembles code, and they quietly stop matching when the generator's formatting changes

Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/stack-breadcrumb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ name: Stack Breadcrumb
on:
pull_request:
# Tree SHAPE only — no `synchronize` (a head push never changes membership).
types: [opened, reopened, edited, closed]
# `ready_for_review` is not in the default set and is named deliberately: a
# draft becoming ready is the moment the PR joins the reviewable stack, and
# without it the breadcrumb keeps describing the PR as a draft until some
# other event happens to fire.
types: [opened, reopened, edited, ready_for_review, closed]
repository_dispatch:
types: [stack-reconcile]
workflow_dispatch:
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,48 @@ jobs:
- name: Test workflow scripts
run: node --test .github/scripts/*.test.cjs

# House style, enforced rather than documented. The rules live in
# `.taskless/` and are scoped to the documents people and agents actually
# read: every README, CLAUDE.md, `.conventions/*.md`, this directory's
# workflows, and comments under `packages/cli/src/`.
#
# A step here, not a standalone `taskless.yml`. The canonical recipe
# (`taskless agent ci`) writes a separate workflow so that onboarding
# never touches a pipeline it does not own, but the two things this repo
# needs are only available inside this job. Branch protection requires
# `Validate` and nothing else, so a separate workflow would report and
# block nothing, which is the whole of what #104 asked for; and `check`
# runs this repo's own build, which the `Build` step above has already
# produced. Full scan rather than the recipe's diff scan, for the reason
# `Validate specs` gives below: rot accumulates in the files a PR does
# not touch, and the scoped corpus is small enough that a diff scan buys
# nothing.
#
# Placed after the test steps rather than immediately after `Build`, so
# that the existing signal order (lint, types, build, tests) stays intact
# for anyone used to reading these logs. It only needs to be after
# `Build`; nothing above it depends on it.
#
# `pnpm cli` is the workspace build, NOT a published release. That is a
# deliberate divergence from `ci.txt` and is ENFORCED, not requested:
# the `ci-uses-workspace-cli` rule fails this very workflow if the
# invocation is changed back, and carries the reasoning in its `note:`.
# `Build` above ran `pnpm build` on this same commit, so `dist/` here
# can be neither stale nor missing, and an absent `dist/` would fail
# this step loudly rather than pass it empty.
#
# This blocks. `check` exits non-zero on any error-severity finding and
# on an engine that failed or timed out, so an em dash added to a covered
# document turns the build red. Warning-severity rules report without
# failing, which is what `severity: warning` in a rule means.
#
# No authentication and no secrets: static rules (Vale and ast-grep) run
# unauthenticated. Runtime rules under `.taskless/rules/runtime/` are
# skipped without a token; that directory is empty today, and wiring
# `TASKLESS_TOKEN` is the separate decision to make when it is not.
- name: Check house style
run: pnpm cli check

# Repo-wide, not changed-files-only: spec rot accumulates in the specs a
# PR does not touch, so a scoped check would never surface it.
- name: Validate specs
Expand Down
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ worktrees/

# The demo project: deliberately-wrong source and prose fixtures.
example/

# Taskless rule fixtures: deliberately-wrong prose and source.
.taskless/
4 changes: 3 additions & 1 deletion .taskless/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.env.local.json
sgconfig.yml
/sgconfig.yml
/.vale.ini
/.sgconfig.yml
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
id: ci-uses-workspace-cli
valid:
- |
steps:
- name: Check house style
run: pnpm cli check
- |
steps:
- name: Install
run: npx some-other-tool --version
- |
steps:
- name: Nightly smoke test
run: npx @taskless/cli-nightly@0.10.2 check
- |
steps:
- name: Multi-line, workspace build
run: |
pnpm build
pnpm cli check
- |
steps:
- name: pnpm exec runs the LOCAL binary, not a published build
run: pnpm exec @taskless/cli check
- |
steps:
- name: --filter addresses the workspace package, not a published one
run: pnpm --filter @taskless/cli build:nightly
invalid:
- |
steps:
- name: Check house style
run: npx @taskless/cli check
- |
steps:
- name: Check house style
run: npx @taskless/cli@latest check
- |
steps:
- name: Multi-line, published release
run: |
pnpm install
npx @taskless/cli check
- |
steps:
- name: The invocation CLAUDE.md names by name
run: pnpm dlx @taskless/cli@latest check
- |
steps:
- name: Another runner, same published build
run: yarn dlx @taskless/cli check
- |
steps:
- name: Global install names the package too
run: |
npm i -g @taskless/cli
taskless check
- |
steps:
- name: Long global flag
run: npm install --global @taskless/cli
- |
steps:
- name: pnpm long global flag
run: pnpm add --global @taskless/cli
- |
steps:
- name: yarn word form
run: yarn global add @taskless/cli
Loading
Loading