diff --git a/.agents/skills/taskless/SKILL.md b/.agents/skills/taskless/SKILL.md new file mode 100644 index 00000000..f705205b --- /dev/null +++ b/.agents/skills/taskless/SKILL.md @@ -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. diff --git a/.changeset/house-style-in-recipes.md b/.changeset/house-style-in-recipes.md new file mode 100644 index 00000000..0e5fb09e --- /dev/null +++ b/.changeset/house-style-in-recipes.md @@ -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 `, 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. diff --git a/.conventions/STYLEGUIDE-CODE.md b/.conventions/STYLEGUIDE-CODE.md index bb149951..477219b5 100644 --- a/.conventions/STYLEGUIDE-CODE.md +++ b/.conventions/STYLEGUIDE-CODE.md @@ -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). @@ -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 @@ -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. @@ -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 @@ -252,19 +252,19 @@ 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 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. | +| 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. @@ -272,7 +272,7 @@ The same reasoning forbids adding a YAML parser to assert on generated config, o - 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 diff --git a/.github/workflows/stack-breadcrumb.yml b/.github/workflows/stack-breadcrumb.yml index 4dc6e67b..9f3c4e1e 100644 --- a/.github/workflows/stack-breadcrumb.yml +++ b/.github/workflows/stack-breadcrumb.yml @@ -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: diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 315895d7..0fa2b4cc 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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 diff --git a/.prettierignore b/.prettierignore index c2a0f1df..48984929 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,3 +13,6 @@ worktrees/ # The demo project: deliberately-wrong source and prose fixtures. example/ + +# Taskless rule fixtures: deliberately-wrong prose and source. +.taskless/ diff --git a/.taskless/.gitignore b/.taskless/.gitignore index b55464c7..f67703dc 100644 --- a/.taskless/.gitignore +++ b/.taskless/.gitignore @@ -1,2 +1,4 @@ .env.local.json -sgconfig.yml +/sgconfig.yml +/.vale.ini +/.sgconfig.yml diff --git a/.taskless/rules/runtime/.gitkeep b/.taskless/rules/runtime/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.taskless/rules/sg/ci-uses-workspace-cli/.tests/ci-uses-workspace-cli-20260826-test.yml b/.taskless/rules/sg/ci-uses-workspace-cli/.tests/ci-uses-workspace-cli-20260826-test.yml new file mode 100644 index 00000000..eaa1d821 --- /dev/null +++ b/.taskless/rules/sg/ci-uses-workspace-cli/.tests/ci-uses-workspace-cli-20260826-test.yml @@ -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 diff --git a/.taskless/rules/sg/ci-uses-workspace-cli/ci-uses-workspace-cli.yml b/.taskless/rules/sg/ci-uses-workspace-cli/ci-uses-workspace-cli.yml new file mode 100644 index 00000000..511bb0fb --- /dev/null +++ b/.taskless/rules/sg/ci-uses-workspace-cli/ci-uses-workspace-cli.yml @@ -0,0 +1,73 @@ +id: ci-uses-workspace-cli +language: Yaml +severity: error +message: A workflow step must run the workspace CLI (`pnpm cli`), not a published release. +note: | + This repository IS the Taskless CLI, so a workflow that runs `npx + @taskless/cli` checks the repository's rules with a PUBLISHED build rather + than the one at HEAD. A commit that breaks rule evaluation would then pass + its own CI and only fail a release later, in someone else's project. + + `validate.yml` runs `pnpm build` before its check step, so the workspace + build is already present and cannot be stale or missing. + + `ci.txt`, the onboarding recipe, tells consumers to use `npx + @taskless/cli`. That is correct for a project that merely depends on the + CLI, and wrong here. This rule is the difference between the two, enforced + rather than written down: the comment it replaces asked a reader not to + "fix" the invocation back, which is a request, not a boundary. + + Matched on the FETCH VERBS, not on `npx` alone and not on the package + name alone. `npx` is one of several ways to reach a published build: + `pnpm dlx @taskless/cli@latest` is the invocation CLAUDE.md names by + name, `yarn dlx` is the same thing, and `npm i -g` reaches it in two + steps whose install line still names the package. Anchoring to `npx` + let every one of those through, including the exact string this + repository documents as the thing `pnpm cli` replaces. + + Anchoring to the bare package name is too wide in the other direction, + and measurably so: it fires on `pnpm --filter @taskless/cli + build:nightly` in `release-cli-nightly.yml`, which is the WORKSPACE + package addressed as a filter, not a published install. `pnpm exec + @taskless/cli` is out for the same reason — it runs the local binary. + What distinguishes a published build is being FETCHED, so the verbs are + what the rule matches. + + The verb list covers the long flag and yarn's word form, not just the + short one. `-g\s+` cannot fire inside `--global`, because what follows + `-g` there is `lobal` rather than whitespace, so `npm install --global + @taskless/cli` would have slipped through a `-g`-only pattern. `yarn + global add @taskless/cli` carries none of `npx`, `dlx` or a flag at all, + which is why `global add` is its own alternative. Both are ordinary ways + to install a published package, and both are pinned as invalid fixtures. + + `@taskless/cli-nightly` is deliberately not matched. The nightly IS a + published artifact by design, and the release workflows reference it on + purpose. The pattern ends before a `-` so the nightly's name cannot + match. + + Only a `run:` value matches, never a comment: `npx @taskless/cli` appears + in prose in `validate.yml` and `release-cli-nightly.yml`, explaining this + very decision, and a rule that flagged its own rationale would be worse + than no rule. +files: + - ".github/workflows/*.yml" + - ".github/workflows/*.yaml" +rule: + all: + - kind: block_mapping_pair + - has: + field: key + kind: flow_node + regex: '^run$' + - has: + field: value + stopBy: end + any: + # A `run:` value is a flow_node when written inline and a + # block_scalar under `|` or `>`. Both spellings are ordinary in + # these workflows, so both have to match. + - kind: flow_node + regex: '((npx|dlx|-g|--global)\s+|global\s+add\s+)@taskless/cli($|[@\s])' + - kind: block_scalar + regex: '((npx|dlx|-g|--global)\s+|global\s+add\s+)@taskless/cli($|[@\s])' diff --git a/.taskless/rules/sg/no-eval/.tests/no-eval-20260824-test.yml b/.taskless/rules/sg/no-eval/.tests/no-eval-20260824-test.yml new file mode 100644 index 00000000..9adfa7f2 --- /dev/null +++ b/.taskless/rules/sg/no-eval/.tests/no-eval-20260824-test.yml @@ -0,0 +1,9 @@ +id: no-eval +valid: + - const config = JSON.parse(raw); + - const handler = handlers[name]; + - const fn = () => compute(input); +invalid: + - eval(userInput); + - const fn = Function("return " + expression); + - const fn = new Function("a", "b", "return a + b"); diff --git a/.taskless/rules/no-eval.yml b/.taskless/rules/sg/no-eval/no-eval.yml similarity index 94% rename from .taskless/rules/no-eval.yml rename to .taskless/rules/sg/no-eval/no-eval.yml index 2ab2906a..be9e01eb 100644 --- a/.taskless/rules/no-eval.yml +++ b/.taskless/rules/sg/no-eval/no-eval.yml @@ -1,5 +1,5 @@ id: no-eval -language: typescript +language: TypeScript severity: error message: Do not use eval() or Function() to evaluate strings as code. These are security risks that enable code injection attacks. note: Use safer alternatives like JSON.parse() for data, or restructure code to avoid dynamic evaluation. diff --git a/.taskless/rules/sg/no-index-imports/.tests/no-index-imports-20260824-test.yml b/.taskless/rules/sg/no-index-imports/.tests/no-index-imports-20260824-test.yml new file mode 100644 index 00000000..7d434ec5 --- /dev/null +++ b/.taskless/rules/sg/no-index-imports/.tests/no-index-imports-20260824-test.yml @@ -0,0 +1,10 @@ +id: no-index-imports +valid: + - import { runWizard } from "./wizard/wizard"; + - import { getRecipe } from "./recipes.js"; + - import { getSandbox } from "@cloudflare/sandbox"; + - import { PostHog } from "posthog-node"; +invalid: + - import { runWizard } from "./index"; + - import { getRecipe } from "../src/prompts/index"; + - import { buildInstallPlan } from "../install/index.js"; diff --git a/.taskless/rules/sg/no-index-imports/no-index-imports.yml b/.taskless/rules/sg/no-index-imports/no-index-imports.yml new file mode 100644 index 00000000..8c1ede3c --- /dev/null +++ b/.taskless/rules/sg/no-index-imports/no-index-imports.yml @@ -0,0 +1,20 @@ +id: no-index-imports +language: TypeScript +severity: warning +message: Import directly from the source file, not from a barrel index. +note: | + Barrel exports hide where a symbol is defined, make tree-shaking less + predictable, and invite circular imports. Import the module that declares + the symbol instead of the `index` that re-exports it. + + Third-party packages that publish a barrel as their public API are fine — + this rule only matches relative specifiers. +ignores: + - "**/test/**" + - "**/*.test.ts" +rule: + kind: string_fragment + regex: '^\.{1,2}(/[^/]+)*/index(\.js|\.ts)?$' + inside: + kind: import_statement + stopBy: end diff --git a/.taskless/rules/sg/no-pii-in-telemetry/.tests/no-pii-in-telemetry-20260824-test.yml b/.taskless/rules/sg/no-pii-in-telemetry/.tests/no-pii-in-telemetry-20260824-test.yml new file mode 100644 index 00000000..4e745f2e --- /dev/null +++ b/.taskless/rules/sg/no-pii-in-telemetry/.tests/no-pii-in-telemetry-20260824-test.yml @@ -0,0 +1,25 @@ +id: no-pii-in-telemetry +valid: + - | + posthog.capture({ + distinctId, + event: "cli_rule_create", + properties: { cli: xdgUuid, anonymous: false }, + groups: { organization: orgId }, + }); + - | + posthog.identify({ distinctId, properties: { cli: xdgUuid } }); + - | + const user = { email: account.email, displayName: account.name }; +invalid: + - | + posthog.capture({ + distinctId, + event: "cli_auth_login_completed", + properties: { cli: xdgUuid, email: account.email }, + }); + - | + posthog.identify({ + distinctId, + properties: { cli: xdgUuid, displayName: account.name }, + }); diff --git a/.taskless/rules/sg/no-pii-in-telemetry/no-pii-in-telemetry.yml b/.taskless/rules/sg/no-pii-in-telemetry/no-pii-in-telemetry.yml new file mode 100644 index 00000000..a0b537df --- /dev/null +++ b/.taskless/rules/sg/no-pii-in-telemetry/no-pii-in-telemetry.yml @@ -0,0 +1,22 @@ +id: no-pii-in-telemetry +language: TypeScript +severity: error +message: Do not send PII in a telemetry call. Identify with internal IDs only. +note: | + PostHog identity uses `jwt.sub`, `jwt.orgId`, and the XDG anonymous UUID. + Email addresses, display names, and real names must never reach + `capture()`, `identify()`, or `groupIdentify()`. + + See .conventions/posthog.md — Privacy. +rule: + kind: pair + has: + field: key + kind: property_identifier + regex: '^(email|userEmail|displayName|fullName|firstName|lastName|username)$' + inside: + stopBy: end + any: + - pattern: $CLIENT.capture($$$) + - pattern: $CLIENT.identify($$$) + - pattern: $CLIENT.groupIdentify($$$) diff --git a/.taskless/rules/sg/no-regex-over-build-output/.tests/no-regex-over-build-output-20260824-test.yml b/.taskless/rules/sg/no-regex-over-build-output/.tests/no-regex-over-build-output-20260824-test.yml new file mode 100644 index 00000000..78145a29 --- /dev/null +++ b/.taskless/rules/sg/no-regex-over-build-output/.tests/no-regex-over-build-output-20260824-test.yml @@ -0,0 +1,31 @@ +id: no-regex-over-build-output +valid: + - | + it("renders from the built artifact", async () => { + const builtEntry = join(root, "dist/prompts.js"); + const { getPrompt } = await import(pathToFileURL(builtEntry).href); + expect(getPrompt("engine-selection")).toBe(sourceRecipe); + }); + - | + it("spawns the built CLI", async () => { + const builtCli = join(root, "dist/index.js"); + const { stdout } = await execFileAsync("node", [builtCli, "help"]); + expect(stdout).toContain("Usage:"); + }); + - | + function importSpecifiers(source: string): string[] { + const found = [...source.matchAll(/\bfrom\s*["']([^"']+)["']/g)]; + return found.map((match) => match[1]!); + } +invalid: + - | + it("imports nothing forbidden", async () => { + const source = readFileSync(join(root, "dist/prompts.js"), "utf8"); + const specifiers = [...source.matchAll(/\bfrom\s*["']([^"']+)["']/g)]; + expect(specifiers).toEqual([]); + }); + - | + it("bundles no node builtins", async () => { + const bundle = await readFile("dist/index.js", "utf8"); + expect(bundle.match(/require\(["']node:fs["']\)/)).toBeNull(); + }); diff --git a/.taskless/rules/sg/no-regex-over-build-output/no-regex-over-build-output.yml b/.taskless/rules/sg/no-regex-over-build-output/no-regex-over-build-output.yml new file mode 100644 index 00000000..e8c8685a --- /dev/null +++ b/.taskless/rules/sg/no-regex-over-build-output/no-regex-over-build-output.yml @@ -0,0 +1,36 @@ +id: no-regex-over-build-output +language: TypeScript +severity: warning +message: Do not re-derive a fact about build output by regex-scanning it. +note: | + A test that reads from `dist/` and then runs a regex over the contents is + reconstructing something the build already knew, with a weaker tool. Move + the invariant into the build — a rollup/vite plugin can ask the resolved + chunk graph directly and fail the build. + + Using a built artifact is fine: import it and assert on behavior, or spawn + the built CLI and assert on its output. This rule fires only on parsing it. + + Scoped to the enclosing function, not the file: a helper that regexes + hand-written source is sound even when the same file elsewhere loads a + built artifact. + + See .conventions/STYLEGUIDE-CODE.md — "Verify Build Output In The Build". +files: + - "**/test/**" + - "**/*.test.ts" +rule: + any: + - pattern: $SRC.matchAll($RE) + - pattern: $SRC.match($RE) + inside: + stopBy: end + any: + - kind: function_declaration + - kind: arrow_function + - kind: function_expression + - kind: method_definition + has: + stopBy: end + kind: string_fragment + regex: 'dist/' diff --git a/.taskless/rules/sg/pr-workflow-no-branches-filter/.tests/pr-workflow-no-branches-filter-20260824-test.yml b/.taskless/rules/sg/pr-workflow-no-branches-filter/.tests/pr-workflow-no-branches-filter-20260824-test.yml new file mode 100644 index 00000000..5edc5906 --- /dev/null +++ b/.taskless/rules/sg/pr-workflow-no-branches-filter/.tests/pr-workflow-no-branches-filter-20260824-test.yml @@ -0,0 +1,31 @@ +id: pr-workflow-no-branches-filter +valid: + - | + on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + - | + on: + push: + branches: [main] + paths: + - ".github/scripts/vale-manifest.json" + - | + on: + workflow_run: + workflows: [Validate] + types: [completed] +invalid: + - | + on: + pull_request: + branches: [main] + - | + on: + push: + branches: [main] + pull_request: + branches: [main] + types: [opened, synchronize, reopened] diff --git a/.taskless/rules/sg/pr-workflow-no-branches-filter/pr-workflow-no-branches-filter.yml b/.taskless/rules/sg/pr-workflow-no-branches-filter/pr-workflow-no-branches-filter.yml new file mode 100644 index 00000000..972f68f9 --- /dev/null +++ b/.taskless/rules/sg/pr-workflow-no-branches-filter/pr-workflow-no-branches-filter.yml @@ -0,0 +1,40 @@ +id: pr-workflow-no-branches-filter +language: Yaml +severity: error +message: A pull_request trigger must not carry a branches filter. +note: | + The filter matches the PR's base ref, but GitHub also resolves a stacked + PR's *eventual* target and sometimes matches on that instead. So + `branches: [main]` does run on mid-stack PRs — until it stops, with no + error and nothing turning red. + + Measured on the #71→#93→#94→#95→#100→#102→#103→#106 stack: every PR up to + #102 got a `Validate` run and #103 and #106 got none, across 16 + `pull_request` events that filter-less workflows handled fine. #103 was a + ~93-file change that reached "ready for review" having never been linted, + typechecked, or tested in CI. + + A workflow that must run everywhere carries no `branches:` filter at all. + A workflow whose correctness depends on "is this the PR that merges to + main" must determine that inside the job — from the base ref, or by + resolving stack position — not from the `on:` filter. + + A `branches:` filter under `push:` is unaffected and correct. + + See CLAUDE.md — "branches: filters do not tell you where a workflow runs". +files: + - ".github/workflows/*.yml" + - ".github/workflows/*.yaml" +rule: + kind: block_mapping_pair + has: + field: key + kind: flow_node + regex: '^branches$' + inside: + stopBy: end + kind: block_mapping_pair + has: + field: key + kind: flow_node + regex: '^pull_request$' diff --git a/.taskless/rules/sg/pr-workflow-ready-for-review/.tests/pr-workflow-ready-for-review-20260824-test.yml b/.taskless/rules/sg/pr-workflow-ready-for-review/.tests/pr-workflow-ready-for-review-20260824-test.yml new file mode 100644 index 00000000..bf3da82c --- /dev/null +++ b/.taskless/rules/sg/pr-workflow-ready-for-review/.tests/pr-workflow-ready-for-review-20260824-test.yml @@ -0,0 +1,30 @@ +id: pr-workflow-ready-for-review +valid: + - | + on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + - | + on: + push: + branches: [main] + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + - | + on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] +invalid: + - | + on: + pull_request: + types: [opened, synchronize, reopened] + - | + on: + pull_request: + types: [opened, reopened, edited, closed] + - | + on: + pull_request: diff --git a/.taskless/rules/sg/pr-workflow-ready-for-review/pr-workflow-ready-for-review.yml b/.taskless/rules/sg/pr-workflow-ready-for-review/pr-workflow-ready-for-review.yml new file mode 100644 index 00000000..bc159309 --- /dev/null +++ b/.taskless/rules/sg/pr-workflow-ready-for-review/pr-workflow-ready-for-review.yml @@ -0,0 +1,57 @@ +id: pr-workflow-ready-for-review +language: Yaml +severity: warning +message: A pull_request types list should name ready_for_review. +note: | + `ready_for_review` is not in the default set (`opened`, `synchronize`, + `reopened`), so a workflow that names `types:` at all must name it + explicitly or a draft marked ready gets no fresh run until something + happens to push again. + + That is the state #103 sat in: a ~93-file change reached "ready for + review" having never been linted, typechecked, or tested in CI. + + A workflow that reacts to PR metadata rather than to PR readiness may + legitimately omit it. Say so in the workflow if you do. + + See CLAUDE.md — "A workflow that must run everywhere carries no + branches: filter at all". +files: + - ".github/workflows/*.yml" + - ".github/workflows/*.yaml" +rule: + any: + # An explicit `types:` that omits `ready_for_review`. + - kind: block_mapping_pair + has: + field: key + kind: flow_node + regex: '^types$' + not: + has: + field: value + stopBy: end + kind: flow_node + regex: 'ready_for_review' + inside: + stopBy: end + kind: block_mapping_pair + has: + field: key + kind: flow_node + regex: '^pull_request$' + # NO `types:` at all. GitHub then applies its default set — `opened`, + # `synchronize`, `reopened` — which omits `ready_for_review` exactly as + # an incomplete explicit list does. Anchoring only on a `types:` node + # cannot see this: there is no node to anchor to. + - kind: block_mapping_pair + has: + field: key + kind: flow_node + regex: '^pull_request$' + not: + has: + field: value + stopBy: end + kind: flow_node + regex: '^types$' diff --git a/.taskless/rules/vale/.gitkeep b/.taskless/rules/vale/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.taskless/rules/vale/comments-record-not-forecast/.tests/fail/probe.ts b/.taskless/rules/vale/comments-record-not-forecast/.tests/fail/probe.ts new file mode 100644 index 00000000..089431ac --- /dev/null +++ b/.taskless/rules/vale/comments-record-not-forecast/.tests/fail/probe.ts @@ -0,0 +1,6 @@ +// The docs describe the current Vale, and 3.18.0 is the known incoming bump. +export const VALE_VERSION = "3.17.1"; + +// None of these is reachable today, though once we upgrade the second tier +// will start routing differently. +export const TIERS = []; diff --git a/.taskless/rules/vale/comments-record-not-forecast/.tests/pass/probe.ts b/.taskless/rules/vale/comments-record-not-forecast/.tests/pass/probe.ts new file mode 100644 index 00000000..028109b4 --- /dev/null +++ b/.taskless/rules/vale/comments-record-not-forecast/.tests/pass/probe.ts @@ -0,0 +1,7 @@ +// Measured on the pinned 3.18.0 binary: a bare non-comment line in bare.pyi +// yields no finding, so the file is plaintext rather than markup. +export const VALE_VERSION = "3.18.0"; + +// Re-probed after the bump rather than renumbered. The claim is the binary's, +// not the release notes'. +export const TIERS = []; diff --git a/.taskless/rules/vale/comments-record-not-forecast/.vale.ini b/.taskless/rules/vale/comments-record-not-forecast/.vale.ini new file mode 100644 index 00000000..48d29942 --- /dev/null +++ b/.taskless/rules/vale/comments-record-not-forecast/.vale.ini @@ -0,0 +1,6 @@ +# Vale reads .ts in its comments-only tier: the comment text is linted and the +# code body is invisible, so this rule can never fire on an identifier. +[packages/cli/src/**/*.ts] +tskl) rule = comments-record-not-forecast +BasedOnStyles = +comments-record-not-forecast.comments-record-not-forecast = YES diff --git a/.taskless/rules/vale/comments-record-not-forecast/comments-record-not-forecast.yml b/.taskless/rules/vale/comments-record-not-forecast/comments-record-not-forecast.yml new file mode 100644 index 00000000..6594683c --- /dev/null +++ b/.taskless/rules/vale/comments-record-not-forecast/comments-record-not-forecast.yml @@ -0,0 +1,15 @@ +extends: existence +message: "'%s' forecasts. Record what was measured, and date the claim." +level: warning +ignorecase: true +tokens: + - 'the known incoming' + - 'the incoming bump' + - 'the upcoming release' + - 'in a future release' + - 'when we bump' + - 'once we upgrade' + - 'once we bump' + - 'we anticipate' + - 'is expected to become' + - 'will likely become' diff --git a/.taskless/rules/vale/docs-npx-cli/.tests/fail/README.md b/.taskless/rules/vale/docs-npx-cli/.tests/fail/README.md new file mode 100644 index 00000000..7a5c9e99 --- /dev/null +++ b/.taskless/rules/vale/docs-npx-cli/.tests/fail/README.md @@ -0,0 +1,13 @@ +# Getting started + +Run pnpm dlx @taskless/cli to install Taskless into this project. + +The inline form `pnpm dlx @taskless/cli@latest info` is a violation too: +commands in docs are almost always in code spans, so the rule has to see +them. + +```bash +pnpm dlx @taskless/cli@latest check +``` + +You can also invoke pnpm cli info to check the version. diff --git a/.taskless/rules/vale/docs-npx-cli/.tests/pass/README.md b/.taskless/rules/vale/docs-npx-cli/.tests/pass/README.md new file mode 100644 index 00000000..fb323cfc --- /dev/null +++ b/.taskless/rules/vale/docs-npx-cli/.tests/pass/README.md @@ -0,0 +1,12 @@ +# Getting started + +Run npx @taskless/cli to install Taskless into this project. + +The inline form `npx @taskless/cli@latest info` is correct. + +```bash +npx @taskless/cli@latest check +``` + +Other package managers are fine for unrelated work, such as pnpm install +or `pnpm build`, and are not what this rule is about. diff --git a/.taskless/rules/vale/docs-npx-cli/.vale.ini b/.taskless/rules/vale/docs-npx-cli/.vale.ini new file mode 100644 index 00000000..d1acb470 --- /dev/null +++ b/.taskless/rules/vale/docs-npx-cli/.vale.ini @@ -0,0 +1,22 @@ +# READMEs are read by external consumers, who do not have this repo's scripts. +# CLAUDE.md and .conventions/ deliberately document the local `pnpm cli` path. +# +# There is no per-case escape hatch for this rule, and that is a property of its +# scope rather than an oversight. `scope: [raw, ...]` in the rule is what reaches +# a fenced code block at all, and Vale evaluates a raw scope against the +# unparsed markup, so its in-file directives never apply: measured on Vale +# 3.18.0, both `` and a blanket +# `` are ignored here, while dropping `raw` makes both work and +# costs every fenced-block finding. A document that has to describe the local +# script therefore names it (see the root README) instead of reproducing the +# invocation, and the rule keeps its full reach. +[**/README.md] +tskl) rule = docs-npx-cli +BasedOnStyles = +docs-npx-cli.docs-npx-cli = YES + +# Test fixtures are inputs to the CLI's own suite, not documentation. +[**/test/fixtures/**/README.md] +tskl) rule = docs-npx-cli +BasedOnStyles = +docs-npx-cli.docs-npx-cli = NO diff --git a/.taskless/rules/vale/docs-npx-cli/docs-npx-cli.yml b/.taskless/rules/vale/docs-npx-cli/docs-npx-cli.yml new file mode 100644 index 00000000..a8f54022 --- /dev/null +++ b/.taskless/rules/vale/docs-npx-cli/docs-npx-cli.yml @@ -0,0 +1,8 @@ +extends: substitution +message: "Use '%s' instead of '%s' — docs use the normalized invocation for external consumers" +level: error +ignorecase: false +scope: [raw, code, text] +swap: + 'pnpm dlx @taskless/cli': npx @taskless/cli + 'pnpm cli': npx @taskless/cli diff --git a/.taskless/rules/vale/no-blocklist-phrases/.tests/fail/README.md b/.taskless/rules/vale/no-blocklist-phrases/.tests/fail/README.md new file mode 100644 index 00000000..32c5cd04 --- /dev/null +++ b/.taskless/rules/vale/no-blocklist-phrases/.tests/fail/README.md @@ -0,0 +1,9 @@ +# Notes + +You're absolutely right that the config is confusing. + +Good catch on the missing flag. + +The version pin is load-bearing, so leave it alone. + +To be honest, the bottom line is that we landed on the second option. diff --git a/.taskless/rules/vale/no-blocklist-phrases/.tests/pass/README.md b/.taskless/rules/vale/no-blocklist-phrases/.tests/pass/README.md new file mode 100644 index 00000000..37b27241 --- /dev/null +++ b/.taskless/rules/vale/no-blocklist-phrases/.tests/pass/README.md @@ -0,0 +1,9 @@ +# Notes + +The config is confusing, and the missing flag is a real bug. + +The version pin is what keeps the format table accurate, so leave it alone. + +The second option is what we chose, for the reasons below. + +PR #103 landed without a CI run, and the plane landed on time. diff --git a/.taskless/rules/vale/no-blocklist-phrases/.vale.ini b/.taskless/rules/vale/no-blocklist-phrases/.vale.ini new file mode 100644 index 00000000..79130540 --- /dev/null +++ b/.taskless/rules/vale/no-blocklist-phrases/.vale.ini @@ -0,0 +1,38 @@ +# Scoped to READMEs. Broadening to source comments, recipe text, and openspec +# is a separate decision with a large remediation attached. +[**/README.md] +tskl) rule = no-blocklist-phrases +BasedOnStyles = +no-blocklist-phrases.no-blocklist-phrases = YES + +# Agent-facing instructions and the house conventions. Read as often as the +# READMEs are, by both people and agents, and small enough to keep conforming. +[CLAUDE.md] +tskl) rule = no-blocklist-phrases +BasedOnStyles = +no-blocklist-phrases.no-blocklist-phrases = YES + +[.conventions/*.md] +tskl) rule = no-blocklist-phrases +BasedOnStyles = +no-blocklist-phrases.no-blocklist-phrases = YES + +# Test fixtures are inputs to the CLI's own suite, not documentation. + +# Agent-facing recipe text. Read by an agent on every authoring run, and the +# surface where a lapse propagates into generated rules, so it is held to the +# same standard as the docs a person reads. +# +# These 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. Everything in the file is prose as far as the rule is +# concerned, including command examples. +[packages/cli/src/agent/*.txt] +tskl) rule = no-blocklist-phrases +BasedOnStyles = +no-blocklist-phrases.no-blocklist-phrases = YES + +[**/test/fixtures/**/README.md] +tskl) rule = no-blocklist-phrases +BasedOnStyles = +no-blocklist-phrases.no-blocklist-phrases = NO diff --git a/.taskless/rules/vale/no-blocklist-phrases/no-blocklist-phrases.yml b/.taskless/rules/vale/no-blocklist-phrases/no-blocklist-phrases.yml new file mode 100644 index 00000000..80945456 --- /dev/null +++ b/.taskless/rules/vale/no-blocklist-phrases/no-blocklist-phrases.yml @@ -0,0 +1,33 @@ +extends: existence +message: "'%s' is on the house blocklist. Say the thing plainly instead." +level: error +ignorecase: true +tokens: + # Reflexive agreement openers + - "you[''’]re absolutely right" + - "you[''’]re right" + - 'great point' + - 'good catch' + # Borrowed voice + - 'it hits different' + - 'the one thing I keep coming back to' + - 'I found the smoking gun' + - 'bottom line' + - 'load-bearing' + - 'belt and suspenders' + # Filler intensifiers + - 'and honestly' + # Performative candor + - 'the honest truth' + - "let[''’]s be honest" + - 'to be honest' + - 'the hard truth' + - 'real talk' + # "land" for a decision or agreement. The literal senses are fine (a plane + # lands, a PR lands), so only the decision collocations are listed. A bare + # 'landed on' was measured firing on "the plane landed on time"; 'we landed' + # already covers "we landed on the second option", so it earned nothing. + - 'we landed' + - 'what we landed' + - 'the decision landed' + - 'glad it landed' diff --git a/.taskless/rules/vale/no-em-dashes/.tests/fail/README.md b/.taskless/rules/vale/no-em-dashes/.tests/fail/README.md new file mode 100644 index 00000000..46655820 --- /dev/null +++ b/.taskless/rules/vale/no-em-dashes/.tests/fail/README.md @@ -0,0 +1,5 @@ +# Setup + +The installer writes two files — the config and the manifest. + +An en dash used the same way – like this – is the same problem. diff --git a/.taskless/rules/vale/no-em-dashes/.tests/pass/README.md b/.taskless/rules/vale/no-em-dashes/.tests/pass/README.md new file mode 100644 index 00000000..dac90f85 --- /dev/null +++ b/.taskless/rules/vale/no-em-dashes/.tests/pass/README.md @@ -0,0 +1,6 @@ +# Setup + +The installer writes two files: the config and the manifest. + +A hyphenated compound like `well-formed` is fine, and so is a range +written as 3-5 items. diff --git a/.taskless/rules/vale/no-em-dashes/.vale.ini b/.taskless/rules/vale/no-em-dashes/.vale.ini new file mode 100644 index 00000000..018d6afa --- /dev/null +++ b/.taskless/rules/vale/no-em-dashes/.vale.ini @@ -0,0 +1,38 @@ +# Scoped to READMEs, where the repository is already clean. Broadening this +# to source comments and openspec is a separate, much larger decision. +[**/README.md] +tskl) rule = no-em-dashes +BasedOnStyles = +no-em-dashes.no-em-dashes = YES + +# Agent-facing instructions and the house conventions. Read as often as the +# READMEs are, by both people and agents, and small enough to keep conforming. +[CLAUDE.md] +tskl) rule = no-em-dashes +BasedOnStyles = +no-em-dashes.no-em-dashes = YES + +[.conventions/*.md] +tskl) rule = no-em-dashes +BasedOnStyles = +no-em-dashes.no-em-dashes = YES + +# Test fixtures are inputs to the CLI's own suite, not documentation. + +# Agent-facing recipe text. Read by an agent on every authoring run, and the +# surface where a lapse propagates into generated rules, so it is held to the +# same standard as the docs a person reads. +# +# These 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. Everything in the file is prose as far as the rule is +# concerned, including command examples. +[packages/cli/src/agent/*.txt] +tskl) rule = no-em-dashes +BasedOnStyles = +no-em-dashes.no-em-dashes = YES + +[**/test/fixtures/**/README.md] +tskl) rule = no-em-dashes +BasedOnStyles = +no-em-dashes.no-em-dashes = NO diff --git a/.taskless/rules/vale/no-em-dashes/no-em-dashes.yml b/.taskless/rules/vale/no-em-dashes/no-em-dashes.yml new file mode 100644 index 00000000..7fb3f792 --- /dev/null +++ b/.taskless/rules/vale/no-em-dashes/no-em-dashes.yml @@ -0,0 +1,7 @@ +extends: existence +message: "Don't use em dashes. Use a period, comma, colon, or parentheses." +level: error +nonword: true +tokens: + - '—' + - '–' diff --git a/.taskless/rules/vale/no-hedging/.tests/fail/README.md b/.taskless/rules/vale/no-hedging/.tests/fail/README.md new file mode 100644 index 00000000..333938bd --- /dev/null +++ b/.taskless/rules/vale/no-hedging/.tests/fail/README.md @@ -0,0 +1,7 @@ +# Setup + +Simply run the installer and you are done. + +The remaining configuration is obviously a matter of taste. + +Of course, the token has to be exported first. diff --git a/.taskless/rules/vale/no-hedging/.tests/pass/README.md b/.taskless/rules/vale/no-hedging/.tests/pass/README.md new file mode 100644 index 00000000..4bc1996c --- /dev/null +++ b/.taskless/rules/vale/no-hedging/.tests/pass/README.md @@ -0,0 +1,8 @@ +# Setup + +Run the installer, then export the token before the first check. + +The remaining configuration is a matter of taste; the defaults are listed +below so you can see what changes. + +A variable named `obviously_stale` is an identifier, not prose. diff --git a/.taskless/rules/vale/no-hedging/.vale.ini b/.taskless/rules/vale/no-hedging/.vale.ini new file mode 100644 index 00000000..7018a33e --- /dev/null +++ b/.taskless/rules/vale/no-hedging/.vale.ini @@ -0,0 +1,59 @@ +# Prose docs a reader outside the team will hit. Code spans and fenced +# blocks are not prose, so this rule never sees a command or an identifier. +[**/README.md] +tskl) rule = no-hedging +BasedOnStyles = +no-hedging.no-hedging = YES + +[CLAUDE.md] +tskl) rule = no-hedging +BasedOnStyles = +no-hedging.no-hedging = YES + +[.conventions/*.md] +tskl) rule = no-hedging +BasedOnStyles = +no-hedging.no-hedging = YES +[packages/cli/src/agent/*.txt] +tskl) rule = no-hedging +BasedOnStyles = +no-hedging.no-hedging = YES + + +# Test fixtures are inputs to the CLI's own suite, not documentation. + +# Agent-facing recipe text. Read by an agent on every authoring run, and the +# surface where a lapse propagates into generated rules, so it is held to the +# same standard as the docs a person reads. +# +# These 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. Everything in the file is prose as far as the rule is +# concerned, including command examples. +# Placed AFTER the general recipe section on purpose. Vale precedence here is +# positional and a later matcher wins, which `create-vale-rule` documents at +# step 2, so an exclusion that sits above the rule it narrows is relying on +# something the repository's own recipe says is not true. Measured on 3.18.0 +# both orders currently exclude these two files, so this is robustness rather +# than a bug fix: it costs nothing and stops depending on which of the two +# behaviours Vale actually implements. +# +# `create-vale-rule` and `verify-rule` teach rule authoring THROUGH a worked +# example called `no-simply`, so the token appears dozens of times as an +# identifier and as sample output rather than as hedging. Vale cannot tell a +# word being used from a word being quoted, and rewording the example to dodge +# its own subject would make the recipe worse. Excluded here rather than left +# to emit ~34 known-benign warnings on every run, which is how a warning +# becomes something people stop reading. +# +# The other recipes stay in scope: their few remaining hits are sample rule +# requests, which is a small enough signal-to-noise cost to keep the coverage. +[packages/cli/src/agent/{create-vale-rule,verify-rule}.txt] +tskl) rule = no-hedging +BasedOnStyles = +no-hedging.no-hedging = NO + +[**/test/fixtures/**/README.md] +tskl) rule = no-hedging +BasedOnStyles = +no-hedging.no-hedging = NO diff --git a/.taskless/rules/vale/no-hedging/no-hedging.yml b/.taskless/rules/vale/no-hedging/no-hedging.yml new file mode 100644 index 00000000..7c6c208c --- /dev/null +++ b/.taskless/rules/vale/no-hedging/no-hedging.yml @@ -0,0 +1,10 @@ +extends: existence +message: "Avoid '%s' — it hides the step the reader is stuck on" +level: warning +ignorecase: true +tokens: + - simply + - obviously + - of course + - trivially + - it should be clear diff --git a/.taskless/sgconfig.yml b/.taskless/sgconfig.yml deleted file mode 100644 index 2dd8e538..00000000 --- a/.taskless/sgconfig.yml +++ /dev/null @@ -1,2 +0,0 @@ -ruleDirs: - - rules diff --git a/.taskless/taskless.json b/.taskless/taskless.json index 01bce72b..d0d82140 100644 --- a/.taskless/taskless.json +++ b/.taskless/taskless.json @@ -1,13 +1,27 @@ { - "version": 2, + "version": 5, "install": { "targets": { + ".taskless": { + "skills": [ + "taskless" + ], + "commands": [ + "tskl.md" + ], + "mode": "canonical" + }, ".claude": { - "skills": ["taskless"], - "commands": ["tskl.md"] + "skills": [ + "taskless" + ], + "commands": [ + "tskl.md" + ], + "mode": "reference" } }, - "installedAt": "2026-05-11T16:38:34.273Z", - "cliVersion": "0.6.0" + "cliVersion": "0.11.0-20260824213902xf26a7b0", + "onboarded": true } } diff --git a/CLAUDE.md b/CLAUDE.md index 0a98091e..55ee4838 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,11 +15,11 @@ When creating or modifying files, you **MUST** follow these conventions: When running Taskless CLI commands in this repo, use `pnpm cli` instead of `pnpm dlx @taskless/cli@latest`. This runs the locally built CLI at `./packages/cli/dist/index.js`. -**`pnpm cli` runs the last build, not the working tree.** `dist/` is a build artifact and nothing rebuilds it for you, so a stale `dist/` serves stale behavior — including stale `agent ` recipes, which are embedded into the bundle at build time rather than fetched over the network. Run `pnpm build` first whenever the answer depends on current source. +**`pnpm cli` runs the last build, not the working tree.** `dist/` is a build artifact and nothing rebuilds it for you, so a stale `dist/` serves stale behavior, including stale `agent ` recipes, which are embedded into the bundle at build time rather than fetched over the network. Run `pnpm build` first whenever the answer depends on current source. -This is not hypothetical. An agent followed `pnpm cli agent create-sg-rule` from a `dist/` built 26 commits earlier and got topic **v2** while HEAD served **v3**. The revision it missed was the one documenting that `language:` takes ast-grep's own spelling, so four new rules were authored with an off-list lowercase `typescript`. That one happened to reach the right parser — a name ast-grep does not recognize at all aborts config parsing and takes every other rule's report down with it, silently. +This is not hypothetical. An agent followed `pnpm cli agent create-sg-rule` from a `dist/` built 26 commits earlier and got topic **v2** while HEAD served **v3**. The revision it missed was the one documenting that `language:` takes ast-grep's own spelling, so four new rules were authored with an off-list lowercase `typescript`. That one happened to reach the right parser. A name ast-grep does not recognize at all aborts config parsing and takes every other rule's report down with it, silently. -**The installed Taskless skill pins a published nightly**, recorded as `install.cliVersion` in `.taskless/taskless.json`, and every command in `.taskless/skills/taskless/SKILL.md` carries that pin. The pin and `pnpm cli` disagree exactly when `dist/` is behind HEAD, and neither is automatically right: the pin is a real build of some commit, `pnpm cli` is this tree only after you rebuild it. Rebuild, then prefer `pnpm cli` — it is the only one that can reflect uncommitted work. Note that the nightly package is blocked by a deny rule here, so `pnpm build` is the practical way to get current recipes, not a fallback. +**The installed Taskless skill pins a published nightly**, recorded as `install.cliVersion` in `.taskless/taskless.json`, and every command in `.taskless/skills/taskless/SKILL.md` carries that pin. The pin and `pnpm cli` disagree exactly when `dist/` is behind HEAD, and neither is automatically right: the pin is a real build of some commit, `pnpm cli` is this tree only after you rebuild it. Rebuild, then prefer `pnpm cli`: it is the only one that can reflect uncommitted work. Note that the nightly package is blocked by a deny rule here, so `pnpm build` is the practical way to get current recipes, not a fallback. When running OpenSpec commands in this repo, use `pnpm openspec` instead of a bare `openspec`. The bare command is not on `PATH` here and is blocked by a deny rule. @@ -38,7 +38,7 @@ When running OpenSpec commands in this repo, use `pnpm openspec` instead of a ba git config --get-all remote.origin.fetch # must be +refs/heads/*:refs/remotes/origin/* ``` - If either is wrong, repair it once — both are local settings, nothing is committed: + If either is wrong, repair it once. Both are local settings, nothing is committed: ```bash git fetch --unshallow @@ -46,7 +46,7 @@ When running OpenSpec commands in this repo, use `pnpm openspec` instead of a ba git fetch origin ``` - Until then: `--force-with-lease` fails with `stale info` on every branch (there is no remote-tracking ref to lease against, so people fall back to a bare `--force`), `git push -u` cannot store an upstream, `gh pr create` needs an explicit `--head `, and `git branch -r` shows only `main`. The dangerous one is quieter — `git rebase main` is only correct while the merge base sits inside the shallow window, so as `main` advances a rebase can reconstruct the wrong base without saying so. + Until then: `--force-with-lease` fails with `stale info` on every branch (there is no remote-tracking ref to lease against, so people fall back to a bare `--force`), `git push -u` cannot store an upstream, `gh pr create` needs an explicit `--head `, and `git branch -r` shows only `main`. The dangerous one is quieter. `git rebase main` is only correct while the merge base sits inside the shallow window, so as `main` advances a rebase can reconstruct the wrong base without saying so. ## PR Issue References @@ -62,7 +62,7 @@ Reference issues as a **trailing line at the bottom of the PR body**, not inline - A bare `-NNN` resolves without a URL for **any** Linear team, not just `TSKL-`. `TSKL-` is Product and `OSS-` is the open-source team; verified with `OSS-23`, which the integration linked and moved to In Review on PR creation. - `Fixes` for the issue this PR resolves; `Refs` for a parent or related issue that stays open. -- Mentioning an issue in prose (`Found while investigating TSKL-5678.`) is **not** a reference — a PR can cite an issue mid-body with no trailing directive at all. +- Mentioning an issue in prose (`Found while investigating TSKL-5678.`) is **not** a reference: a PR can cite an issue mid-body with no trailing directive at all. - Only use a reference you can verify from user input, the branch name, commits, PR discussion, or tracker output. Never invent an issue number. ### Editing an existing PR @@ -86,8 +86,8 @@ Both flags can be passed in one call. See also **Stacked PRs → Other gotchas** The two rules that cause the most damage when missed: -- **A worktree gets its own empty `node_modules`.** `git worktree add` is not finished until `pnpm install` has run inside it. Without that, `git commit` fails in `lint-staged` (no `prettier`/`eslint`), and every `pnpm` script fails. A missing `prettier` here once cost an agent an hour of dead-end workarounds. There is no `pnpm worktree` command — `git worktree` is the tool. -- **NEVER point an agent at the main repo path** (e.g. `/Users//code/taskless/skills`). It will `cd` there and run git commands and edits in the **main** checkout, defeating isolation — it can create and check out a branch in your working tree, silently switching your session off its own branch. Tell the agent to work in **its assigned worktree** (`$PWD`) and pass only relative paths plus GitHub identifiers (`owner/repo`). +- **A worktree gets its own empty `node_modules`.** `git worktree add` is not finished until `pnpm install` has run inside it. Without that, `git commit` fails in `lint-staged` (no `prettier`/`eslint`), and every `pnpm` script fails. A missing `prettier` here once cost an agent an hour of dead-end workarounds. There is no `pnpm worktree` command; `git worktree` is the tool. +- **NEVER point an agent at the main repo path** (e.g. `/Users//code/taskless/skills`). It will `cd` there and run git commands and edits in the **main** checkout, defeating isolation. It can create and check out a branch in your working tree, silently switching your session off its own branch. Tell the agent to work in **its assigned worktree** (`$PWD`) and pass only relative paths plus GitHub identifiers (`owner/repo`). ## Stacked PRs @@ -97,23 +97,23 @@ When PRs stack, the **stack-breadcrumb workflow** (`.github/workflows/stack-brea The proposal states which of these the change is, and why. Decide it while writing the proposal, not when the diff has already grown too big to review. -| Shape | When | How it lands | -| ---------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Single PR** | The whole change fits one reviewable diff. | Spec, implementation, and the archive land together. | -| **Stacked, merging forward** | Each unit is independently safe in production. | Each PR merges to `main` in turn; the last one archives the change. | -| **Stacked, merging down** | The units are only correct together — an intermediate state would ship a broken or half-migrated product. | Merge each PR **down** into its parent from the tip, then one protected merge of the bottom branch to `main`. The change reaches `main` atomically. | +| Shape | When | How it lands | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Single PR** | The whole change fits one reviewable diff. | Spec, implementation, and the archive land together. | +| **Stacked, merging forward** | Each unit is independently safe in production. | Each PR merges to `main` in turn; the last one archives the change. | +| **Stacked, merging down** | The units are only correct together, and an intermediate state would ship a broken or half-migrated product. | Merge each PR **down** into its parent from the tip, then one protected merge of the bottom branch to `main`. The change reaches `main` atomically. | -**Prefer stacking, and aim to keep an individual diff under ~300 lines.** A 900-line PR does not get reviewed, it gets approved. Tests count toward the total but never split from the code they cover — if a unit is oversized because of its tests, that is usually a sign the unit itself should be smaller. +**Prefer stacking, and aim to keep an individual diff under ~300 lines.** A 900-line PR does not get reviewed, it gets approved. Tests count toward the total but never split from the code they cover. If a unit is oversized because of its tests, that is usually a sign the unit itself should be smaller. -The deciding question between forward and down is only this: **can each unit reach production on its own without breaking anything?** If landing unit 1 alone would leave `check` broken, tests failing, or a migration half-applied, the answer is no and the stack merges down. Do not assume forward because it is tidier — verify it, since "each unit is safe" is a claim about behavior, not intent. +The deciding question between forward and down is only this: **can each unit reach production on its own without breaking anything?** If landing unit 1 alone would leave `check` broken, tests failing, or a migration half-applied, the answer is no and the stack merges down. Do not assume forward because it is tidier. Verify it, since "each unit is safe" is a claim about behavior, not intent. -Note how this interacts with archiving: a change is archived exactly once, on whichever PR is the tip. No PR check asks about that — an unarchived change directory is the normal state of a pull request, so a PR-time gate can only guess at stack position, and it guessed wrong often enough to be ignored. The only check is on `main` (a step in `validate.yml`, push events only), which goes red while `main` carries an unarchived change directory. A stack that merges **down** keeps `main` clean throughout; a stack that merges **forward** leaves `main` red until its final slice archives the change. Nothing is blocked by that red — branch protection reads each PR's own `Validate` — but it is a standing reminder that the stack is unfinished. +Note how this interacts with archiving: a change is archived exactly once, on whichever PR is the tip. No PR check asks about that. An unarchived change directory is the normal state of a pull request, so a PR-time gate can only guess at stack position, and it guessed wrong often enough to be ignored. The only check is on `main` (a step in `validate.yml`, push events only), which goes red while `main` carries an unarchived change directory. A stack that merges **down** keeps `main` clean throughout; a stack that merges **forward** leaves `main` red until its final slice archives the change. Nothing is blocked by that red (branch protection reads each PR's own `Validate`), but it is a standing reminder that the stack is unfinished. ### One changeset, at the bottom of the stack, grown as the stack grows -`changeset.yml` looks for a `.changeset/*.md` added or modified **anywhere between `main` and the PR's head** — the whole stack, since a child branch contains its ancestors' commits. It **warns and never fails**: a missing changeset is a judgement call about whether the change ships a release note, and the workflow is not in a position to make it. +`changeset.yml` looks for a `.changeset/*.md` added or modified **anywhere between `main` and the PR's head**, meaning the whole stack, since a child branch contains its ancestors' commits. It **warns and never fails**: a missing changeset is a judgement call about whether the change ships a release note, and the workflow is not in a position to make it. -That is a deliberate retreat from a gate. A per-PR requirement had to reason about stack position to tell a real omission from a file that simply lives further down, and the `skip-changeset` label ended up being applied to silence a red check rather than to record "this ships no release note." The label survives, but it now suppresses a warning, so it can no longer be used to force a merge through. +That is a deliberate retreat from a gate. A per-PR requirement had to reason about stack position to tell a real omission from a file that merely lives further down, and the `skip-changeset` label ended up being applied to silence a red check rather than to record "this ships no release note." The label survives, but it now suppresses a warning, so it can no longer be used to force a merge through. The placement rules are unchanged, because they are about review quality rather than about passing a check: @@ -122,14 +122,14 @@ The placement rules are unchanged, because they are about review quality rather ### `branches:` filters do not tell you where a workflow runs -**`branches: [main]` does not reliably mean either "only the PR whose base is `main`" or "every PR in the stack."** The filter matches the PR's base ref, but GitHub also resolves a stacked PR's _eventual_ target and sometimes matches on that instead, so a filtered workflow runs on mid-stack PRs — observed on #73, #80, and #81, all with `openspec/partition-engine-*` bases. +**`branches: [main]` does not reliably mean either "only the PR whose base is `main`" or "every PR in the stack."** The filter matches the PR's base ref, but GitHub also resolves a stacked PR's _eventual_ target and sometimes matches on that instead, so a filtered workflow runs on mid-stack PRs. Observed on #73, #80, and #81, all with `openspec/partition-engine-*` bases. -**Do not depend on that resolution. It is undocumented and it stops without warning.** On the #71→#93→#94→#95→#100→#102→#103→#106 stack, every PR up to #102 got a `Validate` run and **#103 and #106 got none** — across 16 `pull_request` events that filter-less workflows handled fine. #103 was a ~93-file change that reached "ready for review" having never been linted, typechecked, or tested in CI. Depth correlates (#102 is six hops from `main`, #103 seven) but nothing confirms a cap, and it was not a date cutoff: #102 kept getting runs after #103 had already stopped. A filter that works for six PRs and quietly fails on the seventh is worse than one that never worked, because nobody re-checks it. +**Do not depend on that resolution. It is undocumented and it stops without warning.** On the #71→#93→#94→#95→#100→#102→#103→#106 stack, every PR up to #102 got a `Validate` run and **#103 and #106 got none**, across 16 `pull_request` events that filter-less workflows handled fine. #103 was a ~93-file change that reached "ready for review" having never been linted, typechecked, or tested in CI. Depth correlates (#102 is six hops from `main`, #103 seven) but nothing confirms a cap, and it was not a date cutoff: #102 kept getting runs after #103 had already stopped. A filter that works for six PRs and quietly fails on the seventh is worse than one that never worked, because nobody re-checks it. Two rules follow, and they pull in opposite directions: -- **A workflow that must run everywhere carries no `branches:` filter at all.** Lint, typecheck, and tests have no interest in where a PR eventually merges. `validate.yml`, `changeset.yml`, and `stack-breadcrumb.yml` all carry no filter, which is why they kept running on #103. If you add such a workflow, also name `ready_for_review` in `types:` — it is not in the default set (`opened`/`synchronize`/`reopened`), so without it a draft marked ready gets no fresh run until someone happens to push again. -- **A workflow whose correctness depends on "is this the PR that merges to `main`" must determine that itself** — from the base ref, or by resolving stack position — and cannot lean on the `on:` filter to scope it. Better still, ask a question that does not depend on stack position at all: `changeset.yml` diffs against `main` rather than against its base, and the archive check moved off pull requests entirely. +- **A workflow that must run everywhere carries no `branches:` filter at all.** Lint, typecheck, and tests have no interest in where a PR eventually merges. `validate.yml`, `changeset.yml`, and `stack-breadcrumb.yml` all carry no filter, which is why they kept running on #103. If you add such a workflow, also name `ready_for_review` in `types:`. It is not in the default set (`opened`/`synchronize`/`reopened`), so without it a draft marked ready gets no fresh run until someone happens to push again. +- **A workflow whose correctness depends on "is this the PR that merges to `main`" must determine that itself**, from the base ref or by resolving stack position, and cannot lean on the `on:` filter to scope it. Better still, ask a question that does not depend on stack position at all: `changeset.yml` diffs against `main` rather than against its base, and the archive check moved off pull requests entirely. The shared point: the `on:` filter is not a reliable answer to "where does this PR land." Let the workflow run, and decide inside it. @@ -137,11 +137,11 @@ The shared point: the `on:` filter is not a reliable answer to "where does this Put the changeset at the base and every branch above inherits it, since a child contains its ancestors' commits. -**Write it on the base branch before you cut the children.** Inheritance only runs forward in time: a child branched before the file existed does not carry it, and "grown as the stack grows" has nothing to grow. What makes this easy to miss is that the natural moment to write a release note is when you finish a unit — which is exactly the moment you are standing on a child branch, several branches above the base. A changeset stranded on the tip still reaches `main` when a stack merges down, but on a forward-merging stack it means every PR below it lands with no release note. +**Write it on the base branch before you cut the children.** Inheritance only runs forward in time: a child branched before the file existed does not carry it, and "grown as the stack grows" has nothing to grow. What makes this easy to miss is that the natural moment to write a release note is when you finish a unit, which is exactly the moment you are standing on a child branch, several branches above the base. A changeset stranded on the tip still reaches `main` when a stack merges down, but on a forward-merging stack it means every PR below it lands with no release note. -**Grow it incrementally when the stack merges forward.** Each PR extends the changeset with its own scope rather than the base describing the whole future change up front. A reviewer reading the changeset then sees only what has actually landed, and is not asked to evaluate a release note that promises more than the diff in front of them. When you extend it, edit the same file on the branch you are working on — never add a second changeset per PR, or one change becomes several release notes for what merges to `main` exactly once. +**Grow it incrementally when the stack merges forward.** Each PR extends the changeset with its own scope rather than the base describing the whole future change up front. A reviewer reading the changeset then sees only what has actually landed, and is not asked to evaluate a release note that promises more than the diff in front of them. When you extend it, edit the same file on the branch you are working on. Never add a second changeset per PR, or one change becomes several release notes for what merges to `main` exactly once. -**When the stack merges down, that reasoning does not apply.** Nothing reaches `main` until everything does — a single protected merge carries the whole stack — so a changeset describing the complete change is accurate at the only moment it is ever read, and no reviewer is asked to approve more than what lands. Growing it per unit is still friendlier to review, but there it is a preference, not a correctness constraint. +**When the stack merges down, that reasoning does not apply.** Nothing reaches `main` until everything does (a single protected merge carries the whole stack), so a changeset describing the complete change is accurate at the only moment it is ever read, and no reviewer is asked to approve more than what lands. Growing it per unit is still friendlier to review, but there it is a preference, not a correctness constraint. In both shapes the file belongs **on the bottom branch**. Nothing enforces that any more, so it is on you: a forward-merging stack publishes from `main` as each slice lands, and only a changeset that is already there gets read. @@ -153,7 +153,7 @@ Merge each PR **down** into its parent's branch, from the tip to the bottom: - Bring the bottom branch up to date with `main`, let `Validate` pass, then do the **single** protected merge to `main`. - Result: one CI cycle instead of N, and every PR gets a real **Merged** badge (not "closed/absorbed"). -**Merge the down-merges one at a time, not in a loop.** Merging a child immediately invalidates the parent PR's mergeability until GitHub recomputes — `gh pr merge` fails with "Pull Request is not mergeable", and the API reports `rebaseable: null`. In a tight loop this makes merges land **out of order**, which strands the tip's commits part-way down the stack (e.g. `skill`/`eval` never propagate past `help`). Merge each PR, wait for the next to report a boolean `rebaseable`, then continue. +**Merge the down-merges one at a time, not in a loop.** Merging a child immediately invalidates the parent PR's mergeability until GitHub recomputes: `gh pr merge` fails with "Pull Request is not mergeable", and the API reports `rebaseable: null`. In a tight loop this makes merges land **out of order**, which strands the tip's commits part-way down the stack (e.g. `skill`/`eval` never propagate past `help`). Merge each PR, wait for the next to report a boolean `rebaseable`, then continue. **Verify by content, not by ancestry.** Rebase-and-merge replays commits under new SHAs, so the tip's original commits are never ancestors of the branch that absorbed them, and the obvious check reports a false `STRANDED`: @@ -165,7 +165,7 @@ git merge-base --is-ancestor origin/ origin/ git diff --stat origin/ origin/ # empty = fully absorbed ``` -An empty diff with differing SHAs is the _expected_ healthy state after a rebase merge, not evidence of a problem. If the diff is genuinely non-empty, reconcile from the tip — a tip branch contains the whole stack — then re-check the diff and push. +An empty diff with differing SHAs is the _expected_ healthy state after a rebase merge, not evidence of a problem. If the diff is genuinely non-empty, reconcile from the tip (a tip branch contains the whole stack), then re-check the diff and push. ### Never `--delete-branch` mid-stack @@ -173,7 +173,7 @@ An empty diff with differing SHAs is the _expected_ healthy state after a rebase ### Rebase is the only merge method, and a stack pays for it -`main` keeps a linear history, so the repository allows **rebase-and-merge only** — squash and merge-commit are both disabled. Confirm rather than assume, since this changed: +`main` keeps a linear history, so the repository allows **rebase-and-merge only**; squash and merge-commit are both disabled. Confirm rather than assume, since this changed: ```bash gh api repos/{owner}/{repo} --jq '"squash=\(.allow_squash_merge) merge=\(.allow_merge_commit) rebase=\(.allow_rebase_merge)"' @@ -182,7 +182,7 @@ gh api repos/{owner}/{repo} --jq '"squash=\(.allow_squash_merge) merge=\(.allow_ `gh pr merge --merge` and `--squash` both fail. Use `gh pr merge --rebase`. -**This is the expensive case for a stack, and there is no cheaper option available.** Rebase-and-merge replays the branch onto `main` as _new commits with new SHAs_. Every child then contains the pre-rebase versions of its ancestors' commits, so the child is not merely behind — its history diverged. After each merge you must rebase the next branch onto the updated `main` and force-push it. The old guidance to prefer merge-commits so children stay clean no longer applies; that door is closed. +**This is the expensive case for a stack, and there is no cheaper option available.** Rebase-and-merge replays the branch onto `main` as _new commits with new SHAs_. Every child then contains the pre-rebase versions of its ancestors' commits, so the child is not merely behind: its history diverged. After each merge you must rebase the next branch onto the updated `main` and force-push it. The old guidance to prefer merge-commits so children stay clean no longer applies; that door is closed. Practically, landing a stack now looks like: @@ -196,8 +196,8 @@ git push origin --force-with-lease=:$(git rev-parse origin/)
:` fails with `stale info` when `` is not what the remote currently holds — which includes the case where _you_ rebased the branch a moment ago and reached for its old tip. `$(git rev-parse origin/)` after a `git fetch` is the value that works. The failure looks like the shallow-clone symptom in the git section above and is not: check whether the SHA is simply out of date before concluding anything about the clone. +- **Right after a merge, `rebaseable` reads `null`** while GitHub recomputes. Poll until it is a boolean rather than treating `null` as "not mergeable". Reading it as a failure is what stranded commits mid-stack before. +- **Read the lease SHA from the remote, not from memory.** `--force-with-lease=:` fails with `stale info` when `` is not what the remote currently holds, which includes the case where _you_ rebased the branch a moment ago and reached for its old tip. `$(git rev-parse origin/)` after a `git fetch` is the value that works. The failure looks like the shallow-clone symptom in the git section above and is not: check whether the SHA is just out of date before concluding anything about the clone. ### Rebase-and-merge lands unsigned commits on `main` @@ -207,11 +207,11 @@ Commits are signed locally (`git commit -S`, mandatory above), but **GitHub rewr git log --format='%G? %h %s' -5 origin/main # N, N, N, … ``` -Nothing is wrong and nothing needs fixing on `main`. Know it so that `%G?` on a merged commit is not mistaken for a signing failure, and so a fresh commit reading `N` **before** it reaches `main` is recognised as the real problem it is — that one means `-S` was missed. +Nothing is wrong and nothing needs fixing on `main`. Know it so that `%G?` on a merged commit is not mistaken for a signing failure, and so a fresh commit reading `N` **before** it reaches `main` is recognised as the real problem it is: that one means `-S` was missed. ### Recovery if a child PR gets closed by base-branch deletion -This happens when the **parent** PR is merged with `--delete-branch`: deleting the parent's head branch (which is the child's base) closes the **child** PR. Two PRs are involved — the merged parent (``) and the closed child (``); `` is the deleted base, i.e. the parent's head branch. +This happens when the **parent** PR is merged with `--delete-branch`: deleting the parent's head branch (which is the child's base) closes the **child** PR. Two PRs are involved, the merged parent (``) and the closed child (``); `` is the deleted base, i.e. the parent's head branch. 1. Restore the deleted base branch from **GitHub's own copy of the parent's head**, `refs/pull//head`. GitHub keeps that ref after the branch is deleted and after the PR is merged, and it points at the pre-merge tip: @@ -226,9 +226,9 @@ This happens when the **parent** PR is merged with `--delete-branch`: deleting t git rev-parse "$MERGE_SHA^2" # WRONG under rebase-and-merge ``` - `^2` needs the merge commit to _have_ two parents, which is true only of a merge-commit merge. Rebase replays the branch as linear single-parent commits, so `^2` fails with "unknown revision" — and this repository is rebase-only, so it fails always. `refs/pull//head` is correct under every merge method, which is the better reason to prefer it. + `^2` needs the merge commit to _have_ two parents, which is true only of a merge-commit merge. Rebase replays the branch as linear single-parent commits, so `^2` fails with "unknown revision", and this repository is rebase-only, so it fails always. `refs/pull//head` is correct under every merge method, which is the better reason to prefer it. - Take the ref from `` — the PR that actually merged — not from the closed child, whose head is a different branch. + Take the ref from ``, the PR that actually merged, not from the closed child, whose head is a different branch. 2. Reopen the child via **REST** (GraphQL `gh pr reopen` fails on the Projects-classic deprecation): `gh api --method PATCH repos///pulls/ -f state=open` diff --git a/README.md b/README.md index 11e1df90..a09782c1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ skills/ commands/ tskl/tskl.md # Single /tskl router command packages/ - cli/ # @taskless/cli — recipes live in cli/src/agent/ + cli/ # @taskless/cli, recipes live in cli/src/agent/ scripts/ sync-skill-versions.ts # Syncs metadata.version to CLI version .claude-plugin/ # Claude Code Plugin Marketplace manifest @@ -35,7 +35,6 @@ Available `taskless agent` topics: `route`, `create-sg-rule`, `create-vale-rule` The `@taskless/cli` package provides a CLI agent for Taskless workflows. It's recommended to always call the `latest` tag unless you know you need a specific version: ```bash -pnpm dlx @taskless/cli@latest info npx @taskless/cli@latest info ``` @@ -46,7 +45,7 @@ content it installs. Three 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 +another. Prod → `dist/`, dev → `dist-dev/`, self → `dist-self/` (all gitignored): | Command | Output dir | Baked invocation | Use for | @@ -56,14 +55,26 @@ gitignored): | `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 +`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 — +> The `dev`/`self` invocations are local paths and must never be published: > only `pnpm build` (or `pnpm package`) produces a release artifact. +### Running the local build + +The root `package.json` defines a `cli` script pointing at +`./packages/cli/dist/index.js`. That script runs the CLI built from this working +tree instead of a published release, which is what `CLAUDE.md` points +contributors and agents at while they are working in this repo. Nothing rebuilds +`dist/` for you, so run `pnpm build` first when you want current behavior. + +This section names the script rather than spelling out its shell invocation, because +the `docs-npx-cli` rule holds every command in a README to the published +`npx @taskless/cli` form for readers who do not have this repo checked out. + ## Releasing taskless/cli Releases use [Changesets](https://github.com/changesets/changesets) with Turborepo for orchestration. @@ -79,7 +90,7 @@ pnpm test # Run all tests, confirm no errors git add -A # Stage all changes git commit -m "chore: Releases vx.y.z" # Commit with new version number git push origin main # Push the release commit -pnpm release # Dry run — prints publish command when ready +pnpm release # Dry run, prints publish command when ready pnpm release:production # Publish to npm (prompts for 2FA OTP) ``` @@ -109,7 +120,7 @@ npx @taskless/cli-nightly@latest --version # or: npm i -g @taskless/cli-nightl when both are installed globally.** That is not a supported configuration: a nightly is a drop-in for the release it anticipates, not a companion to it. Use one or the other globally, or install the nightly into a project. -- Versions look like `0.11.0-20260818123456x05b3c88` — the release the nightly +- Versions look like `0.11.0-20260818123456x05b3c88`. The release the nightly anticipates, the UTC build time, and the commit it was built from. Every one of them is a prerelease, and the newest always carries the `latest` tag, so installing with no version gives you the most recent nightly. @@ -129,6 +140,6 @@ In v0.7+, new agent-facing instructions are added as **recipes**, not skills. To ### Distribution channels -- **`taskless init`** — CLI installs the consolidated skill to `.claude/skills/taskless/` and the command to `.claude/commands/tskl/` -- **Claude Code Plugin Marketplace** — `.claude-plugin/marketplace.json` and `plugin.json` -- **Vercel Skills CLI** — `npx skills add` discovers skills from `skills/` directory +- **`taskless init`**: CLI installs the consolidated skill to `.claude/skills/taskless/` and the command to `.claude/commands/tskl/` +- **Claude Code Plugin Marketplace**: `.claude-plugin/marketplace.json` and `plugin.json` +- **Vercel Skills CLI**: `npx skills add` discovers skills from `skills/` directory diff --git a/eslint.config.js b/eslint.config.js index 113646db..2444286a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,6 +28,12 @@ export default tseslint.config( // Zero-dependency CommonJS workflow scripts (covered by their own // node:test suite); the app's TS/ESM-oriented rules don't apply. ".github/scripts/", + // Taskless rule fixtures. A rule's `.tests/` holds inputs written to be + // flagged, and a rule about source comments needs `.ts` fixtures + // specifically — Vale picks its comments-only tier by extension. They are + // not part of any tsconfig, so the type-aware rules fail to parse them. + // `taskless verify` and `taskless test` are what keep them honest. + ".taskless/", // The demo project. Its source is deliberately wrong — `example.cjs` // calls `eval` so a rule has something to find — and its fixtures are // prose written to be flagged. Linting it fails on content nobody wrote diff --git a/packages/cli/README.md b/packages/cli/README.md index 357e95e6..1ed8935b 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -11,11 +11,7 @@ CLI companion for [Taskless](https://taskless.io). Designed to work with agent s ## Install ```bash -# npm npx @taskless/cli - -# pnpm -pnpm dlx @taskless/cli ``` Run with no arguments in a terminal to launch the installer, which detects the @@ -26,7 +22,7 @@ each of them. For scripted installs, skip the prompts: npx @taskless/cli init --no-interactive ``` -New to Taskless? Run `npx @taskless/cli onboard` after installing — it walks your +New to Taskless? Run `npx @taskless/cli onboard` after installing. It walks your agent through your codebase and suggests a starter set of rules. ## How to Use via Agents @@ -40,7 +36,7 @@ asked for, then follows it. /tskl add taskless to CI ``` -Plain language works too — "write a taskless rule for X", "run taskless check", +Plain language works too: "write a taskless rule for X", "run taskless check", "taskless login" all engage the skill. You rarely need to run the CLI yourself. To see what the agent sees, run `npx @taskless/cli agent` for the topic index, or @@ -60,7 +56,7 @@ npx @taskless/cli check --json # machine-readable Paths that no longer exist are dropped silently, so raw `git diff` output can be piped in without pre-filtering. Static rules need no login and make no network -calls, so CI needs no secrets. Runtime rules — which execute code — only run once +calls, so CI needs no secrets. Runtime rules (which execute code) only run once the server has verified their signature; otherwise they are reported as skipped and never change the exit code. @@ -70,27 +66,27 @@ system you already use rather than replacing it. ## Why Teams Choose Taskless - **Constraints, not suggestions.** Rules are real files in your repo, enforced - by ast-grep, Vale, and runtime checks — the same result every run, for every + by ast-grep, Vale, and runtime checks: the same result every run, for every agent and every human. - **The same rules in the editor and in CI.** One command, one exit code. - **Works with the agent you already have.** One skill installs into Claude Code, - Cursor, and OpenCode — plus the `/tskl` command wherever the tool supports slash - commands — with a plain `.agents/` fallback when none is detected. + Cursor, and OpenCode, plus the `/tskl` command wherever the tool supports slash + commands, with a plain `.agents/` fallback when none is detected. - **Nothing to run locally.** No daemon, no install step in CI, no auth for the checks that matter most. ## Docs -- [docs.taskless.io](https://docs.taskless.io) — guides and reference -- [taskless.io](https://taskless.io) — the product -- [github.com/taskless/cli](https://github.com/taskless/cli) — source and issues +- [docs.taskless.io](https://docs.taskless.io): guides and reference +- [taskless.io](https://taskless.io): the product +- [github.com/taskless/cli](https://github.com/taskless/cli): source and issues
Other ### Telemetry -The CLI reports anonymous usage — which command ran, whether it succeeded, how +The CLI reports anonymous usage (which command ran, whether it succeeded, how long it took, and counts of findings. It never sends rule content, prompts, or matched source. Disable it by setting either environment variable: diff --git a/packages/cli/src/agent/auth.txt b/packages/cli/src/agent/auth.txt index cd057854..001aacf2 100644 --- a/packages/cli/src/agent/auth.txt +++ b/packages/cli/src/agent/auth.txt @@ -2,10 +2,10 @@ ## Goal Manage Taskless authentication. Three branches: -- **Login** — start the device-code flow and wait for the user to +- **Login**: start the device-code flow and wait for the user to approve in their browser. -- **Logout** — remove the saved token. -- **Status** — check whether a token is present and whose identity +- **Logout**: remove the saved token. +- **Status**: check whether a token is present and whose identity it represents. ## Preconditions @@ -29,7 +29,7 @@ Pick the branch matching the user's intent. printed. 4. Report success. Suggest `%(TASKLESS_CLI)s info` to verify identity. -The `--anonymous` flag is rejected on `auth login` — it errors with +The `--anonymous` flag is rejected on `auth login`, it errors with "auth commands cannot be anonymous". Don't pass it. ### Logout @@ -52,7 +52,7 @@ The `--anonymous` flag is rejected on `auth login` — it errors with - "Not logged in." (with hint to run `auth login`) - "Logged in as ()." - "Logged in, but unable to verify identity." (token expired or - revoked — suggest re-login) + revoked, suggest re-login) 3. Report to the user. ## Errors @@ -60,7 +60,7 @@ The `--anonymous` flag is rejected on `auth login` — it errors with `auth login` and `auth logout` accept `--json`. On error, the standardized `{ ok: false, code, message }` envelope is written to stdout (and human text on stderr is suppressed). On success in -`--json` mode, the commands exit 0 silently — no success envelope is +`--json` mode, the commands exit 0 silently, no success envelope is emitted. The status path (`%(TASKLESS_CLI)s auth` with no subcommand) accepts `--json` for forward-compat but currently has no error paths to report. @@ -73,5 +73,5 @@ report. ## See Also -- `%(TASKLESS_CLI)s agent info` — see auth state and skill versions -- `%(TASKLESS_CLI)s agent route` — first action that requires auth +- `%(TASKLESS_CLI)s agent info`: see auth state and skill versions +- `%(TASKLESS_CLI)s agent route`: first action that requires auth diff --git a/packages/cli/src/agent/check.txt b/packages/cli/src/agent/check.txt index d308cbfc..5f0129d4 100644 --- a/packages/cli/src/agent/check.txt +++ b/packages/cli/src/agent/check.txt @@ -13,7 +13,7 @@ in CI (diff-only scan), or after rule create/improve to validate. `.taskless/rules/runtime/`. (If none exist, the CLI exits 0 with a friendly message suggesting `%(TASKLESS_CLI)s rule create`.) - No auth required. Static rules always run; whether runtime rules run - depends on auth state — see "What runs". + depends on auth state. See "What runs". ## What runs @@ -23,15 +23,15 @@ in CI (diff-only scan), or after rule create/improve to validate. and **always run**, in every mode, with no network call. The offline linter posture. - **Runtime rules** (`.taskless/rules/runtime//`) execute a - `check.ts` — arbitrary code — so they run ONLY when that code is + `check.ts` (arbitrary code), so they run ONLY when that code is verified: - - **Logged in** (token or API key) — each rule's `check.ts` is + - **Logged in** (token or API key): each rule's `check.ts` is reconciled against the Taskless service; rules the server blessed (`run`) execute, and the rest are withheld and reported (advisory). - **Logged out, `--anonymous`, no GitHub remote, or service - unavailable** — runtime rules are **skipped** (reported, never run). + unavailable**, runtime rules are **skipped** (reported, never run). Static rules still run. - - **`--dangerously-run-scripts`** — runs every runtime rule trusting + - **`--dangerously-run-scripts`**: runs every runtime rule trusting local signatures, with no network call, behind a prominent warning. This is the only way to run runtime rules unverified. @@ -43,10 +43,10 @@ authoritative allow-list is the server's; the CI backstop (`%(TASKLESS_CLI)s agent ci`) is the enforcement point for runtime rules. ## Flags -- `--json` — machine output (`{ success, results, skipped? }`). -- `--anonymous` — run only static rules; skip runtime rules. -- `--dangerously-run-scripts` — run runtime `check.ts` unverified. -- `--timeout ` — per-runtime-check wall-clock bound (default 10). +- `--json`: machine output (`{ success, results, skipped? }`). +- `--anonymous`: run only static rules; skip runtime rules. +- `--dangerously-run-scripts`: run runtime `check.ts` unverified. +- `--timeout `: per-runtime-check wall-clock bound (default 10). ## Steps @@ -105,15 +105,15 @@ authoritative allow-list is the server's; the CI backstop with a non-empty `results` array means there are only warning/info/hint findings (exit code 0); `success: true` with an empty `results` array means the codebase is clean. Findings are - never reported via the `{ ok: false, code, message }` envelope — + never reported via the `{ ok: false, code, message }` envelope: the envelope only appears when the scan itself fails (e.g. `SCAN_FAILED`) and the normal results payload is absent. ## Exit codes -- `0` — All checks passed, no rules configured, or all supplied +- `0`: All checks passed, no rules configured, or all supplied paths missing -- `1` — Errors detected or scan failed +- `1`: Errors detected or scan failed ## Errors @@ -125,5 +125,5 @@ When `--json` is set, failures emit `{ ok: false, code, message }`: ## See Also -- `%(TASKLESS_CLI)s agent route` — add a rule if none exist -- `%(TASKLESS_CLI)s agent ci` — wire `check` into a CI pipeline +- `%(TASKLESS_CLI)s agent route`: add a rule if none exist +- `%(TASKLESS_CLI)s agent ci`: wire `check` into a CI pipeline diff --git a/packages/cli/src/agent/ci.txt b/packages/cli/src/agent/ci.txt index b27afd98..61ca69e2 100644 --- a/packages/cli/src/agent/ci.txt +++ b/packages/cli/src/agent/ci.txt @@ -3,7 +3,7 @@ ## Goal Wire `%(TASKLESS_CLI)s check` into the user's existing CI so rules run automatically on pushes and pull requests. Integrate with what they -already have — never replace or edit their main pipeline. +already have, never replace or edit their main pipeline. This recipe teaches two patterns (full scan and diff scan) that translate to any CI system. Common systems are listed as hints; if @@ -12,11 +12,11 @@ you recognize one not on the list, apply the same patterns. ## Preconditions - `.taskless/` directory exists and contains at least one rule. (If no rules exist, instruct the user to fetch - `%(TASKLESS_CLI)s agent route` first — wiring CI with zero rules + `%(TASKLESS_CLI)s agent route` first, wiring CI with zero rules produces an always-green check that gives false confidence.) - A local `%(TASKLESS_CLI)s check` succeeds (or fails with real findings the user is OK with seeing in CI's first run). -- No auth required for CI — `check` is unauthenticated by default. +- No auth required for CI: `check` is unauthenticated by default. (Optionally tokenized as a server-enforced backstop; see step 7.) ## Steps @@ -42,9 +42,9 @@ which CI they use. If multiple match, ask which should run Taskless. ### 2. Agree on the scan pattern -- **Full scan** — `%(TASKLESS_CLI)s check`. Scans everything. Best for runs +- **Full scan**: `%(TASKLESS_CLI)s check`. Scans everything. Best for runs on the main/default branch. -- **Diff scan** — `%(TASKLESS_CLI)s check $(git diff --name-only ...)`. +- **Diff scan**: `%(TASKLESS_CLI)s check $(git diff --name-only ...)`. Faster for PR builds. Per-CI diff target var: | CI | Target branch variable | @@ -85,7 +85,7 @@ Canonical paths: |---------------------|---------------------------------------------------------------------| | GitHub Actions | `.github/workflows/taskless.yml` (standalone, no include needed) | | GitLab CI | `.taskless/ci/gitlab.yml` (user adds `include`) | -| CircleCI | `.taskless/ci/circleci-job.yml` (no include — user copies job) | +| CircleCI | `.taskless/ci/circleci-job.yml` (no include, user copies job) | | Jenkins | `.taskless/ci/taskless.Jenkinsfile` (user `load()`s) | | Azure Pipelines | `.taskless/ci/azure-taskless.yml` (user references via `template:`) | | Bitbucket Pipelines | `.taskless/ci/bitbucket-pipelines.yml` (user merges manually) | @@ -93,12 +93,12 @@ Canonical paths: ### 5. GitHub Actions reference template -The reference template — translate the same shape (checkout with +The reference template, translate the same shape (checkout with full history, set up Node, conditional check) for other CIs. Substitute `%(PACKAGE_MANAGER_DLX)s` with the CI runner's own complete -invocation — `npx @taskless/cli`, `pnpm dlx @taskless/cli`, -`yarn dlx @taskless/cli`, or `bunx @taskless/cli` — picking the launcher +invocation, `npx @taskless/cli`, `pnpm dlx @taskless/cli`, +`yarn dlx @taskless/cli`, or `bunx @taskless/cli`, picking the launcher from `pnpm-lock.yaml`/`yarn.lock`/`bun.lockb`. It is a whole command on its own, exactly as the template uses it; never combine it with `%(TASKLESS_CLI)s`. Which launcher the repository you are wiring up should @@ -168,14 +168,14 @@ config works out of the box with no secrets and scans all local rules. Static ast-grep rules always run in CI with no secrets. **Runtime rules** (`.taskless/rules/runtime/`, which execute a `check.ts`) only -run when their code is server-verified — so an unauthenticated CI job +run when their code is server-verified, so an unauthenticated CI job runs the static rules and skips the runtime ones. Exposing a `TASKLESS_TOKEN` secret turns CI into the **backstop** for runtime rules: an authenticated `check` reconciles each runtime rule's `check.ts` against the Taskless service and runs exactly the server-blessed set, withholding any that drift or were never issued. -This is the enforcement point for runtime rules — local developer runs +This is the enforcement point for runtime rules, local developer runs skip them unless `--dangerously-run-scripts` is passed. To wire it, set the token as an env var on the check step (GitHub Actions): @@ -190,7 +190,7 @@ the token as an env var on the check step (GitHub Actions): Add this only when the user wants server-enforced rules in CI; the unauthenticated default remains fully supported. Also mention auth if the user explicitly asks to run authenticated commands (e.g. -`rule create`/`rule improve`) in CI — uncommon. +`rule create`/`rule improve`) in CI, uncommon. ### 8. Package manager caveats @@ -205,11 +205,11 @@ the user explicitly asks to run authenticated commands (e.g. ### 9. Report back Show: -1. The path written and a 10–15 line excerpt. +1. The path written and a 10-15 line excerpt. 2. For CIs needing manual wiring, the exact `include:` / reference line for their main config. 3. `git status` so they can review before committing. -4. A note that the first CI run exercises rules — if existing +4. A note that the first CI run exercises rules, if existing matches exist, CI will fail until fixed or suppressed. ## Errors @@ -224,5 +224,5 @@ Show: ## See Also -- `%(TASKLESS_CLI)s agent check` — the command being wired into CI -- `%(TASKLESS_CLI)s agent route` — required if no rules exist yet +- `%(TASKLESS_CLI)s agent check`: the command being wired into CI +- `%(TASKLESS_CLI)s agent route`: required if no rules exist yet diff --git a/packages/cli/src/agent/create-legacy-rule.txt b/packages/cli/src/agent/create-legacy-rule.txt index 3a642e64..d12a7b0e 100644 --- a/packages/cli/src/agent/create-legacy-rule.txt +++ b/packages/cli/src/agent/create-legacy-rule.txt @@ -2,7 +2,7 @@ ## You are here This is `create-legacy-rule`. It helps you write a rule for a linter the -repository already runs — ESLint, Ruff, RuboCop, Stylelint — in that +repository already runs (ESLint, Ruff, RuboCop, Stylelint) in that tool's own dialect, so that tool enforces it. If that is not the kind of check you need, re-run `%(TASKLESS_CLI)s agent route` and follow its decision rather than adapting this recipe. @@ -11,7 +11,7 @@ and follow its decision rather than adapting this recipe. Author a rule in a linter the repository ALREADY uses, expressed in that tool's own dialect (an ESLint rule, a Ruff rule selection, a RuboCop cop, a Stylelint rule, etc.). Taskless does not maintain a catalog of linter -rules — you source the knowledge from the repo first and the web second, +rules, you source the knowledge from the repo first and the web second, then write the rule where that tool expects it. ## Preconditions @@ -46,19 +46,19 @@ then write the rule where that tool expects it. consistent with the repo's existing entries from step 2. 5. **Report, and be explicit about who runs it.** Show the file(s) you - changed. Make clear that the user's OWN toolchain runs this rule — + changed. Make clear that the user's OWN toolchain runs this rule: `%(TASKLESS_CLI)s check` does NOT execute external linters. Tell the user how to run their linter to see it fire (e.g. their existing lint script). ## Important Notes -- Do not invent linter rules from memory — verify against the repo's +- Do not invent linter rules from memory: verify against the repo's usage and the tool's current docs. - This path is author-only. Taskless does not aggregate or run external linters; it writes the rule in the tool's dialect and hands off. ## See Also -- `%(TASKLESS_CLI)s agent route` — re-decide the destination if this no longer fits -- `%(TASKLESS_CLI)s agent create-sg-rule` — author a local ast-grep rule instead -- `%(TASKLESS_CLI)s agent create-remote-rule` — generate via the service (login) +- `%(TASKLESS_CLI)s agent route`: re-decide the destination if this no longer fits +- `%(TASKLESS_CLI)s agent create-sg-rule`: author a local ast-grep rule instead +- `%(TASKLESS_CLI)s agent create-remote-rule`: generate via the service (login) diff --git a/packages/cli/src/agent/create-remote-rule.txt b/packages/cli/src/agent/create-remote-rule.txt index 4e73ed28..a5985acb 100644 --- a/packages/cli/src/agent/create-remote-rule.txt +++ b/packages/cli/src/agent/create-remote-rule.txt @@ -13,14 +13,14 @@ submit it, and report what came back. The service generates the rule and writes the rule files; your job is everything on either side of that call. -This recipe is the whole path — the boundary and the procedure. Do not +This recipe is the whole path, the boundary and the procedure. Do not go looking for a second topic to perform the submission. ## Preconditions - `.taskless/` directory exists. - The user is logged in. This path requires auth. - The project has a GitHub owner. **Check this yourself before collecting - anything** — see below. + anything**, see below. - The request reached here through `route`, or through a local attempt that failed and a user who confirmed the escalation. @@ -68,9 +68,9 @@ Two ways to legitimately be here: it is expressible and the user was offered the choice and took it. - **A local attempt failed and the user confirmed.** If you came from `create-sg-rule`'s failure path, that recipe has already deleted the - broken candidate and asked. If it has not been asked, ask now — a + broken candidate and asked. If it has not been asked, ask now, a silent fall-through from a failed local attempt to a paid service call - is not acceptable, even when the service would obviously do better. + is not acceptable, even when the service would do better. ## Steps @@ -85,7 +85,7 @@ Two ways to legitimately be here: 2. **Check for a rule that already covers this.** Scan `.taskless/rules/sg/` and read each rule's `message`, `note`, and `rule` fields. If one overlaps, show the user and ask whether they - would rather iterate on it — `%(TASKLESS_CLI)s agent improve-rule` refines an + would rather iterate on it, `%(TASKLESS_CLI)s agent improve-rule` refines an existing rule and is usually the better answer than a second rule that half-overlaps the first. @@ -93,7 +93,7 @@ Two ways to legitimately be here: ask: - What exact pattern should be flagged? Get concrete examples. - In what language? - - Where is the pattern acceptable — are there contexts to exclude? + - Where is the pattern acceptable: are there contexts to exclude? If you arrived from a failed local attempt, reuse the cases you already gathered rather than asking again. @@ -120,7 +120,7 @@ Two ways to legitimately be here: ``` %(TASKLESS_CLI)s rule create --from .taskless/.tmp-rule-request.json --json ``` - This may take 30–60 seconds while the service generates the rule. + This may take 30-60 seconds while the service generates the rule. 8. **Clean up.** Delete `.taskless/.tmp-rule-request.json` whether the call succeeded or failed. @@ -150,7 +150,7 @@ Multi-line code goes in one string with literal newlines. the service generate it; a hand-written rule attached to a generation request is neither reviewed nor used. - The service owns rule-type selection. Today it generates ast-grep - rules under `.taskless/rules/sg/` — the same shape `create-sg-rule` + rules under `.taskless/rules/sg/`, the same shape `create-sg-rule` produces locally. ## Errors @@ -171,8 +171,8 @@ With `--json`, failures emit `{ ok: false, code, message }`: ## See Also -- `%(TASKLESS_CLI)s agent route` — the routing decision that leads here -- `%(TASKLESS_CLI)s agent auth` — log in before generating -- `%(TASKLESS_CLI)s agent create-sg-rule` — author a rule locally instead -- `%(TASKLESS_CLI)s agent improve-rule` — iterate on a rule that already exists -- `%(TASKLESS_CLI)s agent check` — validate the generated rule +- `%(TASKLESS_CLI)s agent route`: the routing decision that leads here +- `%(TASKLESS_CLI)s agent auth`: log in before generating +- `%(TASKLESS_CLI)s agent create-sg-rule`: author a rule locally instead +- `%(TASKLESS_CLI)s agent improve-rule`: iterate on a rule that already exists +- `%(TASKLESS_CLI)s agent check`: validate the generated rule diff --git a/packages/cli/src/agent/create-runtime-rule.txt b/packages/cli/src/agent/create-runtime-rule.txt index ab89b817..821a87ed 100644 --- a/packages/cli/src/agent/create-runtime-rule.txt +++ b/packages/cli/src/agent/create-runtime-rule.txt @@ -3,7 +3,7 @@ ## You are here This is `create-runtime-rule`. It helps you write a runtime rule: a check that runs your own code, because answering it needs more than one -file — the repository graph, git metadata, build output, a resolved +file, the repository graph, git metadata, build output, a resolved config chain. If that is not the kind of check you need, re-run `%(TASKLESS_CLI)s agent route` and follow its decision rather than adapting this recipe. @@ -19,7 +19,7 @@ tiers are not, and what the user has to do before one can run. ## Preconditions - `.taskless/` directory exists. - The user is **not** logged in. If `%(TASKLESS_CLI)s info --json` reports - `loggedIn: true`, you are in the wrong recipe — re-run + `loggedIn: true`, you are in the wrong recipe, re-run `%(TASKLESS_CLI)s agent route`. ## What a runtime rule is @@ -35,7 +35,7 @@ kinds of file: `verify` fails a runtime rule with no capture rule in `captures/`, because `check.ts` would then never be invoked. -The capture rules do the cheap work — if nothing matches, `check.ts` is +The capture rules do the cheap work, if nothing matches, `check.ts` is never invoked. `check.ts` does the part no static engine can: hold two files at once, read git, resolve an alias, look outside the repository. @@ -46,7 +46,7 @@ That is also exactly why it is gated. **Because it executes code, not because of what it can express.** `sg` and `vale` rules are inert data. Whatever is in them, the worst a -malicious rule achieves is a wrong finding — `%(TASKLESS_CLI)s check` runs them +malicious rule achieves is a wrong finding, `%(TASKLESS_CLI)s check` runs them with no login, no network, and nothing to verify, because there is nothing to verify. @@ -54,7 +54,7 @@ A runtime rule's `check.ts` is a program that runs on the developer's machine with the developer's permissions. A rule file that arrives in a pull request is code that arrives in a pull request. So `check`: -1. **Signs** each `check.ts` — a signature over the exact bytes on disk. +1. **Signs** each `check.ts`, a signature over the exact bytes on disk. 2. **Reconciles** those signatures with the Taskless service, which answers with the set it will vouch for. 3. **Runs only what came back blessed.** Anything else is reported as @@ -66,14 +66,14 @@ Without a login there is nobody to ask, so there is no answer, so nothing runs. This is a property of executing code. It says nothing about whether a -runtime rule is more or less capable, and it is not a quality tier — +runtime rule is more or less capable, and it is not a quality tier: `sg` and `vale` rules are not less trusted, they are unexecuted. ## What happens today, logged out Nothing breaks. `%(TASKLESS_CLI)s check` still runs every static rule; each runtime rule it finds is listed as skipped with the reason -`not authenticated — runtime rules were not verified and did not run`. +`not authenticated, runtime rules were not verified and did not run`. So a runtime rule you write now is inert until the user logs in. Say that plainly rather than letting them discover it from a silent check. @@ -81,8 +81,8 @@ that plainly rather than letting them discover it from a silent check. ## Steps 1. **Tell the user what their rule needs, and why it is gated.** Name - the evidence — "this has to compare two files", "this reads git - history" — and then the consequence: it has to run code, so it needs + the evidence, "this has to compare two files", "this reads git + history", and then the consequence: it has to run code, so it needs an account. Do not present this as a limitation of the rule. 2. **Point them at login.** Obtaining access is `auth`'s job, and this @@ -93,7 +93,7 @@ that plainly rather than letting them discover it from a silent check. ``` Follow that recipe with the user. If they do not want an account, - stop here — say the rule cannot run without one, and offer to + stop here, say the rule cannot run without one, and offer to reconsider whether a narrower version of the request could be answered by a static rule instead. That is a new routing decision, not a fallback you take on their behalf. @@ -116,6 +116,6 @@ that plainly rather than letting them discover it from a silent check. ## See Also -- `%(TASKLESS_CLI)s agent auth` — log in, log out, check status -- `%(TASKLESS_CLI)s agent route` — re-decide once the login state changes -- `%(TASKLESS_CLI)s agent check` — see which rules ran and which were skipped +- `%(TASKLESS_CLI)s agent auth`: log in, log out, check status +- `%(TASKLESS_CLI)s agent route`: re-decide once the login state changes +- `%(TASKLESS_CLI)s agent check`: see which rules ran and which were skipped diff --git a/packages/cli/src/agent/create-sg-rule.txt b/packages/cli/src/agent/create-sg-rule.txt index 509ce50d..2f41a63e 100644 --- a/packages/cli/src/agent/create-sg-rule.txt +++ b/packages/cli/src/agent/create-sg-rule.txt @@ -48,7 +48,7 @@ whole rule. reference at https://ast-grep.github.io/guide/rule-config.html for valid fields and operators (`pattern`, `kind`, `regex`, `any`/`all`/`has`/`inside`/`not`) and meta-variable syntax. This - recipe does not embed the schema — read it from upstream rather than + recipe does not embed the schema. Read it from upstream rather than writing a rule from memory. 2. **Gather and confirm the pattern.** Make sure you have concrete @@ -59,17 +59,17 @@ whole rule. 3. **Check for an existing rule that already covers this.** Scan `.taskless/rules/sg/` and read each rule's `message`, `note`, and `rule` fields. If one overlaps, show the user and ask whether they - would rather improve it — `%(TASKLESS_CLI)s agent improve-rule --anonymous` + would rather improve it, `%(TASKLESS_CLI)s agent improve-rule --anonymous` iterates a rule locally. 4. **Author the rule in the canonical shape.** Write the rule to `.taskless/rules/sg//.yml`, where `` is kebab-case and names both the directory and the file. At minimum: - - `id` — kebab-case, matching the filename (e.g. `no-eval`) - - `language` — the target language, in ast-grep's spelling (below) - - `severity` — `error`, `warning`, `info`, or `hint` - - `message` — a concise single-line explanation - - `rule` — the ast-grep rule object + - `id`: kebab-case, matching the filename (e.g. `no-eval`) + - `language`: the target language, in ast-grep's spelling (below) + - `severity`: `error`, `warning`, `info`, or `hint` + - `message`: a concise single-line explanation + - `rule`: the ast-grep rule object Optional but useful: `note` (multi-line guidance, supports markdown), `fix` (auto-fix pattern), `ignores` (file patterns to skip). @@ -91,7 +91,7 @@ whole rule. Two specific traps: - **Do not copy from `detect --json`.** It reports the - *repository's* languages in a different vocabulary — it says + *repository's* languages in a different vocabulary. It says `C++` where the list above says `Cpp`. - **`Tsx` and `TypeScript` are two parsers, not aliases.** A rule over `.tsx` files that declares `TypeScript` does not match JSX @@ -112,7 +112,7 @@ whole rule. 5. **Check any variadic pattern against the separator trap.** A `$$$` next to a comma does not mean "zero or more". The `,` in the pattern is itself an AST node, and under ast-grep's default `smart` - strictness every node in the pattern must match — so a call with no + strictness every node in the pattern must match, so a call with no comma cannot match a pattern that has one. Measured against the ast-grep this CLI ships (v%(AST_GREP_VERSION)s), given the four calls `foo()`, `foo(1)`, `foo(1,2)`, and `foo(1,2,3)`: @@ -120,14 +120,14 @@ whole rule. | pattern | what it matches | |--------------------|----------------------------------------------| | `foo($$$)` | all four, `foo()` included | -| `foo($A, $$$)` | `foo(1,2)` and `foo(1,2,3)` — never `foo(1)` | +| `foo($A, $$$)` | `foo(1,2)` and `foo(1,2,3)`, never `foo(1)` | | `foo($$$, $A)` | `foo(1)` alone | | `foo($A, $$$, $B)` | `foo(1,2)` alone | - A standalone `$$$` needs none of this — it is the comma beside it + A standalone `$$$` needs none of this. It is the comma beside it that narrows the pattern. The two remedies are not the same: - - **Trailing `$$$`** — write the pattern as an object with + - **Trailing `$$$`**: write the pattern as an object with `strictness: ast`, which compares named AST nodes and ignores the separator. An object pattern also requires `context` and `selector`: @@ -139,11 +139,11 @@ whole rule. strictness: ast ``` This moves the boundary from two arguments to one, **not to - zero** — `$A` still has to bind something, so `foo()` is still + zero**, `$A` still has to bind something, so `foo()` is still unmatched. And `strictness` is valid only inside the pattern object: at rule level ast-grep rejects it as an unknown field and fails the whole scan. - - **Leading `$$$`** — `strictness: ast` does not rescue it. Use + - **Leading `$$$`**: `strictness: ast` does not rescue it. Use `any` with one branch per arity you mean to cover. This is upstream's intended behaviour (ast-grep/ast-grep#1365, @@ -152,7 +152,7 @@ whole rule. 6. **Write the tests.** Write `.taskless/rules/sg//.tests/-YYYYMMDD-test.yml` with the matching - `id` field plus `valid` and `invalid` arrays — at least two of each, + `id` field plus `valid` and `invalid` arrays, at least two of each, drawn from real code where you can. The `id` must match the rule's `id` so ast-grep test filtering pairs them. These paths and this shape are the same ones the service writes; do not invent a different @@ -165,7 +165,7 @@ whole rule. exactly like a rule that works. The `invalid:` bucket is the only thing that demonstrates the rule can fire. - Where the rule has an arity boundary — anything from step 5 — put a + Where the rule has an arity boundary (anything from step 5), put a case on each side of it. A pattern that starts at two arguments when it was meant to start at one passes a test suite whose fixtures all have two. @@ -179,7 +179,7 @@ whole rule. `verify` asks whether the rule is well-formed: the YAML matches the ast-grep schema and every Taskless-required field is present. It needs no test file, so run it the moment the rule exists. `test` runs - the cases, after running `verify` and stopping if that fails — so a + the cases, after running `verify` and stopping if that fails, so a malformed rule reports the malformation rather than a test complaint. Both answer in the same shape: @@ -196,7 +196,7 @@ whole rule. | a Taskless-required field is missing | add `id`/`language`/`severity`/`message`/`rule` | | a case didn't behave as expected | fix the rule pattern OR the test case | - A `regex` without an accompanying `kind` fails verification — the two + A `regex` without an accompanying `kind` fails verification, the two always travel together. Pass a directory above a rule and every rule beneath it is checked, @@ -207,7 +207,7 @@ whole rule. plus a one-line summary of what the rule detects. Suggest `%(TASKLESS_CLI)s agent check` to validate against the broader codebase. -9. **On failure, escalate — with confirmation.** If after the feedback +9. **On failure, escalate, with confirmation.** If after the feedback loop the rule still cannot capture the user's cases: - Delete the candidate `.taskless/rules/sg//` directory so the repo is not left with a broken rule. One `rm -rf` removes the rule @@ -221,15 +221,15 @@ whole rule. ## Important Notes - Do NOT make any HTTP requests to taskless.io on this path. -- Do NOT write to `.taskless/rule-metadata/` — locally authored rules +- Do NOT write to `.taskless/rule-metadata/`: locally authored rules have no metadata sidecar; they iterate via file edits. - The verify loop is the quality gate. A clean failure is a legitimate reason to escalate, but only with the user's confirmation (step 9). ## See Also -- `%(TASKLESS_CLI)s agent route` — re-decide the destination -- `%(TASKLESS_CLI)s agent verify-rule` — the `verify` and `test` commands step 7 calls -- `%(TASKLESS_CLI)s agent improve-rule` — iterate on a rule that already exists -- `%(TASKLESS_CLI)s agent create-remote-rule` — generate via the service (login) -- `%(TASKLESS_CLI)s agent check` — validate the new rule against the codebase +- `%(TASKLESS_CLI)s agent route`: re-decide the destination +- `%(TASKLESS_CLI)s agent verify-rule`: the `verify` and `test` commands step 7 calls +- `%(TASKLESS_CLI)s agent improve-rule`: iterate on a rule that already exists +- `%(TASKLESS_CLI)s agent create-remote-rule`: generate via the service (login) +- `%(TASKLESS_CLI)s agent check`: validate the new rule against the codebase diff --git a/packages/cli/src/agent/create-vale-rule.txt b/packages/cli/src/agent/create-vale-rule.txt index 1e9208a0..0a6eddbe 100644 --- a/packages/cli/src/agent/create-vale-rule.txt +++ b/packages/cli/src/agent/create-vale-rule.txt @@ -2,7 +2,7 @@ ## You are here This is `create-vale-rule`. It helps you write a Vale rule: a check over -the words of a document — prose, markup, and the prose parts of code. +the words of a document, prose, markup, and the prose parts of code. If that is not the kind of check you need, re-run `%(TASKLESS_CLI)s agent route` and follow its decision rather than adapting this recipe. @@ -52,7 +52,7 @@ which Vale accepts without complaint. You will not find a project-wide `.vale.ini` to edit. The config Vale actually reads is assembled from every rule's own file at check time -and is gitignored. Editing it is pointless — the next check regenerates +and is gitignored. Editing it is pointless, the next check regenerates it. ## Steps @@ -62,7 +62,7 @@ it. built by extending one of its twelve checks, and the sentence tells you which. Twelve is measured, not counted off the docs: give Vale v%(VALE_VERSION)s an `extends` it does not know and it names the whole - set back at you — + set back at you: ``` 'extends' key must be one of [capitalization conditional consistency @@ -76,7 +76,7 @@ it. | If the rule is about… | extends | |-----------------------------------------------------------------------------------------|------------------| | words or phrases that should not appear | `existence` | -| preferring one term over another — **including the correct spelling of a product name** | `substitution` | +| preferring one term over another. **including the correct spelling of a product name** | `substitution` | | the case of a whole heading or sentence | `capitalization` | | how many times something may appear | `occurrence` | | a word repeated back to back | `repetition` | @@ -91,14 +91,14 @@ it. **`capitalization` is about a whole scope, not a word.** It asks whether an entire heading or sentence matches a case pattern. It cannot express "the word GitHub, wherever it appears, is spelled - thus" — that is a `substitution`, because you are swapping a wrong + thus". That is a `substitution`, because you are swapping a wrong spelling for a right one. Reaching for `capitalization` on a product name produces a rule that flags whole sentences: measured, a rule with `match: GitHub` reports `We host on Github and it is fine. should be GitHub`. - For the five this recipe has no worked example of — `metric`, - `readability`, `spelling`, `sequence`, `script` — read + For the five this recipe has no worked example of, `metric`, + `readability`, `spelling`, `sequence`, `script`, read https://docs.vale.sh/styles before inventing something. Vale has no facility for a rule that does not extend one of these twelve, and an `extends` outside the set is not a rule that misbehaves: Vale exits 2 @@ -107,7 +107,7 @@ it. **The other seven have a worked rule at the end of this recipe**, nine rules between them, each with the near-miss that fails and why. - Read the one closest to your intent before writing anything — the + Read the one closest to your intent before writing anything, the mistakes documented there are observed, and most of them fail silently. @@ -128,7 +128,7 @@ it. | `extends` | yes | one of the twelve above | | `message` | yes | shown to the user; see the `%%s` table below | | `level` | no | `suggestion` (default), `warning`, or `error` | -| `scope` | no | narrow to part of a document — see below | +| `scope` | no | narrow to part of a document; see below | | `link` | no | a URL the reader can follow for the reasoning | | `limit` | no | cap findings from this rule per scope | @@ -141,7 +141,7 @@ it. fields below. **The file extension must be `.yml`.** Measured: rename a working - style file to `.yaml` and Vale loads nothing — no error, no warning, + style file to `.yaml` and Vale loads nothing, no error, no warning, zero findings, and `. = YES` still parses. It is indistinguishable from a rule whose pattern never matched. @@ -150,7 +150,7 @@ it. rejected by Vale: `scope: fenced` loads, runs, and matches nothing. It is the worst of the three failures on this page, because unlike a bad `extends` or a foreign field it does not even take the run down to - tell you — the rule is simply inert, forever. `verify` rejects a scope + tell you. The rule is inert, forever. `verify` rejects a scope outside the table below, which is the only layer that ever will. Every value below was measured against Vale v%(VALE_VERSION)s by @@ -161,7 +161,7 @@ it. | `scope` | reaches | |------------------------|-----------------------------------------------------------| | *(omitted)* | everything the format exposes as prose | -| `text` | prose only — not inline code, not fenced blocks | +| `text` | prose only, not inline code, not fenced blocks | | `code` | inline code spans only | | `raw` | the unparsed document: prose, inline code, fenced blocks | | `heading` | every heading | @@ -178,7 +178,7 @@ it. | `table.header` | header cells | | `table.cell` | body cells | | `table.caption` | a table's caption | -| `figure.caption` | a figure's caption — but see below | +| `figure.caption` | a figure's caption, but see below | | `frontmatter` | every YAML front-matter value | | `frontmatter.` | one front-matter key's value | | `text.class.` | HTML elements carrying that class | @@ -190,7 +190,7 @@ it. the token in prose, in an inline span, and in a fenced block: `text` found one, `code` found one, `[code, text]` found two, `raw` found all three. If you want prose and inline code but not fenced blocks, write - the list — `raw` is not "a bit wider", it is everything. + the list, `raw` is not "a bit wider", it is everything. **Vale drops everything inside a `
` element.** Measured: a `
` nested in `
` is invisible to *every* scope, @@ -207,9 +207,9 @@ it. **A negation over a scope Vale does not know is a silent no-op.** Measured: `~banana` and `text & ~banana` both fire on everything, because there is no such scope to subtract. A typo inside a `~` does - not narrow the rule and does not widen it visibly — it removes the + not narrow the rule and does not widen it visibly. It removes the exclusion you wrote the rule for. `verify` checks the operands inside - `~` and `&` as strictly as a bare one, for exactly this reason — the + `~` and `&` as strictly as a bare one, for exactly this reason, the one place it is deliberately stricter than Vale itself. **`scope` is per-rule, and rules do not interact.** Taskless assembles @@ -225,11 +225,11 @@ it. document, and `raw` reads the unparsed one. Measured: a `text`-scoped rule is silenced by the directive; the same rule at `raw` fires through it. So a rule about a shell command, a flag, or a package name - needs `raw` — commands live in fenced blocks, which nothing else - reaches — and takes that trade: it can no longer be exempted case by + needs `raw`, commands live in fenced blocks, which nothing else + reaches, and takes that trade: it can no longer be exempted case by case, only removed. - **Then the fields the extension point adds** — this is where the rule + **Then the fields the extension point adds**. This is where the rule actually lives, and each check reads only its own: | extends | its fields | @@ -249,7 +249,7 @@ it. The list above is measured, not transcribed: every entry was added to a minimal rule of that check and the run watched for `E201`. Three - corrections fall out of it, all against the published docs — + corrections fall out of it, all against the published docs: `capitalization` takes `prefix` (singular) and rejects both `prefixes` and `suffixes`, it rejects `ignorecase`, and `occurrence` rejects `exceptions` and `vocab`. @@ -258,13 +258,13 @@ it. and `level` are not.** Measured: `Tokens:` and `ignoreCase:` are read exactly as their lowercase spellings, while `EXTENDS:` fails with "Missing the required 'extends' key". Their *values* are case-sensitive - too — `level: WARNING` and `extends: Existence` are both rejected. + too, `level: WARNING` and `extends: Existence` are both rejected. Write everything lowercase and none of this can bite you. **A field from the wrong check is the loudest failure Vale has.** `tokens` on an `occurrence` check gives - `E201 … has invalid keys: 'tokens'`, exit 2, and — because Vale reads - one assembled config per run — **no** Vale rule in the project reports + `E201 … has invalid keys: 'tokens'`, exit 2, and, because Vale reads + one assembled config per run. **no** Vale rule in the project reports anything. `verify` rejects the rule before Vale is invoked, so this cannot reach `check`. @@ -277,7 +277,7 @@ it. **What `%%s` fills with depends on the extension point.** Getting this wrong is the one mistake in this recipe that passes every check below - — the rule fires, the fixtures are green, and only a human reading the +. The rule fires, the fixtures are green, and only a human reading the message sees that it is nonsense. | extends | `%%s` count | fills with, left to right | @@ -291,13 +291,13 @@ it. `Github` renders `Use GitHub not GitHub`. For the other nine, do not guess. Write the message, run step 6, and - read it back off the finding — no test you can write catches a wrong + read it back off the finding, no test you can write catches a wrong `%%s`, so your own eyes on the rendered message are the check. ```yaml - # existence — flag these tokens wherever they appear + # existence, flag these tokens wherever they appear extends: existence - message: "Avoid '%%s' — it hides the work from the reader" + message: "Avoid '%%s', it hides the work from the reader" level: warning ignorecase: true tokens: @@ -306,7 +306,7 @@ it. ``` ```yaml - # substitution — first %%s is the replacement, second is what was found + # substitution, first %%s is the replacement, second is what was found extends: substitution message: "Use '%%s' instead of '%%s'" level: warning @@ -317,7 +317,7 @@ it. ``` ```yaml - # capitalization — a whole heading must be in sentence case + # capitalization, a whole heading must be in sentence case extends: capitalization message: "'%%s' should be in sentence case" level: warning @@ -330,19 +330,19 @@ it. `match` takes `$sentence`, `$title`, `$lower`, or `$upper`. A literal string is legal but means "this whole scope must read exactly that", - which is almost never what anyone wants — see step 1. + which is almost never what anyone wants. See step 1. **`$sentence` means first word capitalized, everything else lowercase - — proper nouns included.** It is not "sentence case allowing proper +, proper nouns included.** It is not "sentence case allowing proper nouns". Measured with `exceptions: [Taskless, API]` on headings: | Heading | Result | |-----------------------------------|------------------------------------------------| | `Getting started with the API` | quiet | -| `Getting started with APIs` | quiet — an exception covers its plural | -| `Taskless and the API` | quiet — an exception may lead the scope | -| `Getting started with Kubernetes` | **fires** — a proper noun you did not list | -| `getting started lowercase` | **fires** — the first word must be capitalized | +| `Getting started with APIs` | quiet, an exception covers its plural | +| `Taskless and the API` | quiet, an exception may lead the scope | +| `Getting started with Kubernetes` | **fires**: a proper noun you did not list | +| `getting started lowercase` | **fires**: the first word must be capitalized | | `Getting Started With Title Case` | **fires** | So `exceptions` is not decoration: every proper noun, product name and @@ -354,7 +354,7 @@ it. not literals.** They compile as **Go RE2** regular expressions. *This step is about `tokens` and `swap` only. A `capitalization`, - `occurrence` or `metric` rule has neither — skip to step 4.* + `occurrence` or `metric` rule has neither, skip to step 4.* - `(?:…)`, `[…]`, `|`, `+`, `?` all work. - **Lookahead and lookbehind do not exist in RE2.** A rule that needs @@ -372,7 +372,7 @@ it. pattern meaning "mayb" followed by an optional "e". Escape it. - **Overlapping alternatives resolve first-wins**, one finding per match. If `can login` and `login with` both match a sentence, you - get whichever is written first, once — not both. + get whichever is written first, once, not both. - `ignorecase: true` matches any casing **and still skips text that already equals the replacement.** Measured with `Github: GitHub`: `github` and `Github` are flagged, `GitHub` is not. You do not need @@ -382,14 +382,14 @@ it. - **A token made only of punctuation can never match without `nonword: true`.** The boundaries above are `\b`, which needs a word character on the inside. An em dash has none, on either side. - Measured against `This is a sentence — with an em dash.`: + Measured against `This is a sentence, with an em dash.`: ```yaml - # fires on nothing, ever — and reports no error + # fires on nothing, ever, and reports no error extends: existence message: "Use a comma, not an em dash" tokens: - - '—' + - ', ' ``` ```yaml @@ -398,12 +398,12 @@ it. message: "Use a comma, not an em dash" nonword: true tokens: - - '—' + - ', ' ``` The first rule verifies, tests green if its `fail/` fixture is missing the dash, and reports nothing forever. Any token whose - pattern contains no `\w` — punctuation, an emoji, a bare symbol — + pattern contains no `\w` (punctuation, an emoji, a bare symbol) needs `nonword: true`. - **A bare word finds senses you did not mean.** `landed on` in a rule @@ -423,7 +423,7 @@ it. `Plain Github here` and not on `https://Github.com/x` or `` `Github/docs` ``. - **Link text *is* prose.** In `[click here](https://example.com)`, - `click here` is matched — the URL is not. With no `scope`, a rule + `click here` is matched. The URL is not. With no `scope`, a rule fires on both link text and ordinary prose; `scope: link` narrows it to link text alone. Measured: without a scope the token hit both the link and the sentence; with `scope: link`, only the link. @@ -449,7 +449,7 @@ it. this rule sees. Match it to the files the rule is actually about, such as `[*.{md,markdown}]` or `[docs/**/*.md]`. A rule can declare several matchers if it needs to. Before you widen a glob, check the - reach table below — what Vale does to a file it cannot parse is not + reach table below, what Vale does to a file it cannot parse is not "nothing". - `tskl) rule = ` is a breadcrumb Taskless reads to attribute the matcher back to this rule after assembly interleaves every rule's @@ -467,7 +467,7 @@ it. **Scope a rule *out* with a second matcher, not a cleverer glob.** A glob says which files a rule sees; it has no way to say "these but not those". The exclusion is a second matcher that assigns `NO`, and - because precedence here is positional — a later matcher wins — the + because precedence here is positional (a later matcher wins), the exclusion goes **after** the inclusion: ```ini @@ -500,22 +500,22 @@ it. rendered from the pinned Vale version, not written out here, so they track the shipped binary. - - **markup** — the document is prose and the format's own non-prose + - **markup**: the document is prose and the format's own non-prose constructs are skipped. This is the tier every `scope:` value assumes; `scope: heading` has nothing to find outside it: %(VALE_MARKUP_FORMATS)s - - **comment text only** — the comments are linted and the code body + - **comment text only**: the comments are linted and the code body is invisible, which is exactly right for "comments must not say 'obviously'": %(VALE_COMMENT_FORMATS)s - - **plaintext fallback** — everything else, `.yml` `.toml` `.sh` + - **plaintext fallback**: everything else, `.yml` `.toml` `.sh` `.sql` and every extension not named above included. There is no parser, so the whole file is linted as prose: a rule matched to YAML flags key names and values, not just the comments. If that is not what the rule means, narrow the glob rather than accepting it. These land here despite reading like markup, so a `scope:` value has nothing to act on in them: %(VALE_PLAINTEXT_FORMATS)s - - **not supported** — Vale parses these only by shelling out to an + - **not supported**: Vale parses these only by shelling out to an external program, and this build does not support any format that needs one: %(VALE_CONVERTER_FORMATS)s @@ -526,8 +526,8 @@ it. since an XSLT stylesheet is specific to the document. **A single unreadable file fails the whole Vale pass.** Vale exits 2 - with an `E100` runtime error and abandons the run — `--no-exit` does - not suppress it — so every other Vale rule over every other file goes + with an `E100` runtime error and abandons the run, `--no-exit` does + not suppress it, so every other Vale rule over every other file goes unreported. `[*.{md,typ}]` is not a slightly wider `[*.md]`; it is a matcher that takes `check` down the first time the repo grows a `.typ` file. Never put one of those extensions in a glob. @@ -539,7 +539,7 @@ it. **`.mdx` is supported** as of Vale v%(VALE_VERSION)s, which parses it natively rather than shelling out. `[*.{md,mdx}]` is a legitimate - matcher again — the example this recipe used to warn about is no + matcher again, the example this recipe used to warn about is no longer the broken one. Check the lists above rather than reaching for that memory: `.typ` moved the opposite way in the same release, so a matcher covering Typst is now the one that takes the run down. @@ -565,26 +565,26 @@ it. document silently passes. **The `pass/` bucket is not "correct prose".** Correct prose proves - nothing — the rule was never going to fire on it. Fill it with the + nothing. The rule was never going to fire on it. Fill it with the near-misses that would catch an over-broad pattern. What counts as a near-miss depends on the rule's shape: - - **`tokens`/`swap` rules** — the noun form you are not flagging, the + - **`tokens`/`swap` rules**: the noun form you are not flagging, the word inside a longer word, the term in a URL or a code span, the correct spelling itself. - - **`scope`d rules** — the same phrase *outside* the scope. A rule + - **`scope`d rules**: the same phrase *outside* the scope. A rule with `scope: link` needs the phrase in ordinary prose; a rule with `scope: heading` needs it in body text. Without that, nothing proves the scope is doing anything. - - **`capitalization` rules** — a scope that is entirely exceptions, a + - **`capitalization` rules**: a scope that is entirely exceptions, a scope whose exception word comes first, and the plural of an exception. That is the half of the fixture set that has to work for you. **When the rule's subject normally appears in code, the `fail/` - fixture must carry it three ways** — inline in a code span, inside a - fenced block, and in ordinary prose — in that one document. A rule + fixture must carry it three ways**, inline in a code span, inside a + fenced block, and in ordinary prose, in that one document. A rule about a command, a flag, a package name or an env var has a subject that lives in fenced blocks in every real README, and the default scope cannot see fenced blocks at all. A `fail/` fixture written only @@ -592,7 +592,7 @@ it. of the real violations. Measured on one document holding the token in all three places: the default scope found one of three, `raw` found three. If the fixture fires on the prose line and not on the other - two, the answer is `scope: raw` — see step 2 for what that costs. + two, the answer is `scope: raw`. See step 2 for what that costs. **Fixtures run under a config that isolates this rule, so a green `test` is not evidence the rule reaches any real file.** `test` @@ -690,7 +690,7 @@ one was run against the bundled Vale; the "what goes wrong" lines are observed behavior, not warnings in principle. Find the entry closest to your intent and start there. -### 1. Ban a word or phrase — `existence` +### 1. Ban a word or phrase, `existence` > "Our docs shouldn't hedge." @@ -705,12 +705,12 @@ tokens: - sort of ``` -**Goes wrong:** dropping `ignorecase: true` when you meant any casing — +**Goes wrong:** dropping `ignorecase: true` when you meant any casing. `We think` at the start of a sentence then sails through. And a phrase with punctuation is a *pattern*: `maybe?` means "mayb" plus an optional "e", so it matches `mayb`. Escape it: `maybe\?`. -### 2. Prefer one term over another — `substitution` +### 2. Prefer one term over another, `substitution` > "Say 'sign in', not 'login', when it's a verb." @@ -725,11 +725,11 @@ swap: ``` **Goes wrong:** one `%%s` instead of two. Measured, `"Use sign in not -%%s"` against `login to` renders **"Use sign in not sign in to"** — the +%%s"` against `login to` renders **"Use sign in not sign in to"**, the replacement, twice. The rule fires, both fixtures pass, and only a human reading the message sees it. Two `%%s`, always, in that order. -### 3. Enforce a product's spelling — `substitution`, not `capitalization` +### 3. Enforce a product's spelling, `substitution`, not `capitalization` > "It's 'GitHub', never 'Github' or 'github'." @@ -743,13 +743,13 @@ swap: ``` **Goes wrong:** reaching for `capitalization` because the complaint is -about capitals. Measured, `match: GitHub` flags whole sentences — -`'We host on Github and it is fine. should be GitHub'` — because that +about capitals. Measured, `match: GitHub` flags whole sentences: +`'We host on Github and it is fine. should be GitHub'`, because that check tests a *scope*, not a word. Note also that `ignorecase: true` is safe here: Vale skips text already equal to the replacement, so the correct `GitHub` is not flagged. -### 4. Sentence-case headings — `capitalization` +### 4. Sentence-case headings, `capitalization` > "Headings are sentence case; our product names keep their capitals." @@ -770,13 +770,13 @@ everything after the first word, proper nouns included, so every product name and acronym in the docs must be listed or correct headings get flagged. Collect them from the docs first; expect to add more. -### 5. Restrict a rule to link text — any check, plus `scope` +### 5. Restrict a rule to link text, any check, plus `scope` > "'click here' is useless link text." ```yaml extends: existence -message: "Link text '%%s' says nothing — name the destination" +message: "Link text '%%s' says nothing, name the destination" level: warning scope: link ignorecase: true @@ -789,10 +789,10 @@ tokens: on `[click here](…)` **and** on "click here to focus the search box" in ordinary prose, which is a false positive on a sentence that is fine. Whenever a rule is about a *place* in the document, the `pass/` fixture -must contain the same phrase outside that place — otherwise nothing +must contain the same phrase outside that place, otherwise nothing proves the scope works. -### 6. Cap how often something appears — `occurrence` +### 6. Cap how often something appears, `occurrence` > "At most one exclamation mark per paragraph." @@ -807,9 +807,9 @@ max: 1 **Goes wrong:** forgetting `scope`. The count is per scope, so with no scope you are capping the whole document rather than the paragraph. -Note `token` here is singular — this check takes one, not a `tokens` list. +Note `token` here is singular, this check takes one, not a `tokens` list. -### 7. Catch a doubled word — `repetition` +### 7. Catch a doubled word, `repetition` > "'the the' keeps slipping through review." @@ -823,12 +823,12 @@ tokens: ``` **Goes wrong:** leaving the pattern unquoted. Measured, an unquoted -`[^\s]+` in YAML silently matches nothing — zero findings, no error, no +`[^\s]+` in YAML silently matches nothing, zero findings, no error, no diagnostic. Quote any pattern containing a backslash. This is the failure mode this recipe warns about most, arriving through YAML rather than through Vale. -### 8. One spelling or the other, consistently — `consistency` +### 8. One spelling or the other, consistently, `consistency` > "Pick -ize or -ise and stick to it." @@ -850,11 +850,11 @@ Vale reads one config for the whole run. Name this one `izeise` or `spelling_variants`. Kebab-case is right everywhere else. **Goes wrong:** expecting it to pick a winner. It flags the *second* -form once both appear in a document — it enforces internal consistency, +form once both appear in a document, it enforces internal consistency, not house style. If you want one specific spelling, that is a `substitution`. -### 9. Require a definition — `conditional` +### 9. Require a definition, `conditional` > "An acronym must be spelled out before it's used." @@ -889,6 +889,6 @@ definition when the acronym is missing". ## See Also -- `%(TASKLESS_CLI)s agent route` — re-decide the destination -- `%(TASKLESS_CLI)s agent check` — run every engine over the repo -- `%(TASKLESS_CLI)s agent create-sg-rule` — author a rule over code structure +- `%(TASKLESS_CLI)s agent route`: re-decide the destination +- `%(TASKLESS_CLI)s agent check`: run every engine over the repo +- `%(TASKLESS_CLI)s agent create-sg-rule`: author a rule over code structure diff --git a/packages/cli/src/agent/delete-rule.txt b/packages/cli/src/agent/delete-rule.txt index fcd13300..6d805d82 100644 --- a/packages/cli/src/agent/delete-rule.txt +++ b/packages/cli/src/agent/delete-rule.txt @@ -18,7 +18,7 @@ rule is deleted by removing its directory by hand. 1. **Identify the rule.** If the user named one, use it. Otherwise, list `.taskless/rules/sg/` and ask which one. Confirm the user's - intent — deletion is destructive. + intent, deletion is destructive. 2. **Invoke the CLI.** Run: ``` @@ -26,7 +26,7 @@ rule is deleted by removing its directory by hand. ``` The CLI removes: - - `.taskless/rules/sg//` — the whole directory, which holds the + - `.taskless/rules/sg//`: the whole directory, which holds the rule, its `.tests/`, and any per-rule config - `.taskless/rule-metadata/.yml` (if present) @@ -49,5 +49,5 @@ emitted from this command. ## See Also -- `%(TASKLESS_CLI)s agent route` — make a new rule -- `%(TASKLESS_CLI)s agent check` — run remaining rules to confirm nothing broke +- `%(TASKLESS_CLI)s agent route`: make a new rule +- `%(TASKLESS_CLI)s agent check`: run remaining rules to confirm nothing broke diff --git a/packages/cli/src/agent/detect.txt b/packages/cli/src/agent/detect.txt index cce03a13..faf3e7f8 100644 --- a/packages/cli/src/agent/detect.txt +++ b/packages/cli/src/agent/detect.txt @@ -3,7 +3,7 @@ ## Goal Scan the working directory for the linters it configures, the languages it uses, and the styles of any rules the repo already -authors. Offline and deterministic — no network, no auth, no state +authors. Offline and deterministic, no network, no auth, no state change. This is the discovery step that feeds rule-authoring: the routing flow reads `detect` to decide where a new rule should live. @@ -36,11 +36,11 @@ routing flow reads `detect` to decide where a new rule should live. ] } ``` - - `linters` — each has a `name` and `evidence` (config-file paths, + - `linters`: each has a `name` and `evidence` (config-file paths, a pyproject table marker, or a dependency marker from the language's package file; not every entry is a path). - - `languages` — inferred from manifests and the detected linters. - - `ruleStyles` — how the repo authors its own rules, surfaced for + - `languages`: inferred from manifests and the detected linters. + - `ruleStyles`: how the repo authors its own rules, surfaced for downstream reuse. 3. **Use the signals to route.** Feed the output into rule authoring: @@ -59,5 +59,5 @@ When `--json` is set, failures emit `{ ok: false, code, message }`: ## See Also -- `%(TASKLESS_CLI)s agent route` — decide where to author a rule from these signals -- `%(TASKLESS_CLI)s agent check` — run rules against the codebase +- `%(TASKLESS_CLI)s agent route`: decide where to author a rule from these signals +- `%(TASKLESS_CLI)s agent check`: run rules against the codebase diff --git a/packages/cli/src/agent/improve-rule.anonymous.txt b/packages/cli/src/agent/improve-rule.anonymous.txt index 5499cc81..8088b7a9 100644 --- a/packages/cli/src/agent/improve-rule.anonymous.txt +++ b/packages/cli/src/agent/improve-rule.anonymous.txt @@ -65,7 +65,7 @@ validate with `verify` and `test` in a feedback loop. ## Important Notes -- Rules created in anonymous mode have no metadata sidecar — that's +- Rules created in anonymous mode have no metadata sidecar: that's fine. This recipe doesn't need or write metadata. - If the rule was originally created via the API path (has metadata), you can still use this anonymous recipe to iterate locally. The @@ -84,6 +84,6 @@ The verify primitive returns structured errors per layer: ## See Also -- `%(TASKLESS_CLI)s agent improve-rule` — API-backed flow (auth required) -- `%(TASKLESS_CLI)s agent create-sg-rule` — make a new rule locally -- `%(TASKLESS_CLI)s agent check` — validate the updated rule +- `%(TASKLESS_CLI)s agent improve-rule`: API-backed flow (auth required) +- `%(TASKLESS_CLI)s agent create-sg-rule`: make a new rule locally +- `%(TASKLESS_CLI)s agent check`: validate the updated rule diff --git a/packages/cli/src/agent/improve-rule.txt b/packages/cli/src/agent/improve-rule.txt index 4b4cfa14..398ec102 100644 --- a/packages/cli/src/agent/improve-rule.txt +++ b/packages/cli/src/agent/improve-rule.txt @@ -16,13 +16,13 @@ If the user wants the local-only flow (no API call), fetch service path as generation, so it carries the same constraint. Check `ghOwner` from `%(TASKLESS_CLI)s info --json`: if it is the literal `[unknown]`, stop and say the tier is unavailable rather than - submitting. `auth login` does not fix it — no GitHub owner is a + submitting. `auth login` does not fix it, no GitHub owner is a property of the project, not the session. - The target rule exists at `.taskless/rules/sg//.yml`. - The rule has metadata at `.taskless/rule-metadata/.yml` (the `ticketId` is required by the iterate endpoint). If metadata is missing, the rule was created in anonymous mode and cannot be - iterated via the API path — fetch the anonymous variant instead. + iterated via the API path. Fetch the anonymous variant instead. ## Steps @@ -38,7 +38,7 @@ If the user wants the local-only flow (no API call), fetch %(TASKLESS_CLI)s rule meta --json ``` This returns the `ticketId` needed for the iterate request. If the - metadata is missing, the rule cannot be iterated via API — fetch + metadata is missing, the rule cannot be iterated via API. Fetch `%(TASKLESS_CLI)s agent improve-rule --anonymous` instead. 4. **Gather improvement guidance.** Ask the user what should change: @@ -51,7 +51,7 @@ If the user wants the local-only flow (no API call), fetch that is merely too narrow.** Read the rule's pattern for a `$$$` sitting next to a comma before you write that guidance. The comma is itself an AST node that has to match, so `f($A, $$$)` never sees a - one-argument call and `f($$$, $A)` sees only one-argument calls — the + one-argument call and `f($$$, $A)` sees only one-argument calls, the pattern reads as variadic and is not. `%(TASKLESS_CLI)s agent create-sg-rule` carries the arity table and both remedies. Guidance that asks to widen the rule when the fix is `strictness: ast` sends the iterate @@ -59,9 +59,9 @@ If the user wants the local-only flow (no API call), fetch 5. **Collect supporting references.** Ask the user for any code examples that should be: - - **Not flagged** (currently flagged but shouldn't be) — false + - **Not flagged** (currently flagged but shouldn't be): false positive references. - - **Flagged** (currently passing but should be caught) — false + - **Flagged** (currently passing but should be caught): false negative references. Each reference is `{ filename: string, content: string }`. Multiple @@ -77,7 +77,7 @@ If the user wants the local-only flow (no API call), fetch ``` %(TASKLESS_CLI)s rule improve --from .taskless/.tmp-improve-request.json --json ``` - This may take 30–60 seconds while the API generates the update. + This may take 30-60 seconds while the API generates the update. 9. **Clean up.** Delete `.taskless/.tmp-improve-request.json` regardless of success or failure. @@ -127,6 +127,6 @@ When `--json` is set, failures emit `{ ok: false, code, message }`: ## See Also -- `%(TASKLESS_CLI)s agent improve-rule --anonymous` — local-only flow -- `%(TASKLESS_CLI)s agent route` — make a new rule from scratch -- `%(TASKLESS_CLI)s agent check` — validate the updated rule +- `%(TASKLESS_CLI)s agent improve-rule --anonymous`: local-only flow +- `%(TASKLESS_CLI)s agent route`: make a new rule from scratch +- `%(TASKLESS_CLI)s agent check`: validate the updated rule diff --git a/packages/cli/src/agent/info.txt b/packages/cli/src/agent/info.txt index a4716b18..127b223a 100644 --- a/packages/cli/src/agent/info.txt +++ b/packages/cli/src/agent/info.txt @@ -57,11 +57,11 @@ When `--json` is set, failures emit `{ ok: false, code, message }`: |------------------|----------------------------|--------------------------| | `INTERNAL_ERROR` | Internal schema validation | Report; likely a CLI bug | -(Network errors during the auth probe are silently swallowed — +(Network errors during the auth probe are silently swallowed: `info` falls back to reporting `loggedIn: false` rather than failing.) ## See Also -- `%(TASKLESS_CLI)s agent auth` — log in / log out / status detail -- `%(TASKLESS_CLI)s agent check` — run rules against the codebase +- `%(TASKLESS_CLI)s agent auth`: log in / log out / status detail +- `%(TASKLESS_CLI)s agent check`: run rules against the codebase diff --git a/packages/cli/src/agent/init.txt b/packages/cli/src/agent/init.txt index c1cb2ccc..7853dac1 100644 --- a/packages/cli/src/agent/init.txt +++ b/packages/cli/src/agent/init.txt @@ -3,7 +3,7 @@ ## Goal Install or update the Taskless skill into the user's coding-agent tools (Claude Code, OpenCode, Cursor, etc.). The user runs this -themselves — the agent's role is mostly to point the user at the +themselves, the agent's role is mostly to point the user at the right command when they need to install or upgrade. ## Preconditions @@ -35,7 +35,7 @@ The wizard will: directory (`.claude/`, `.cursor/`, `.opencode/`, `.agents/`). 5. Update `.taskless/taskless.json` with the install manifest. -The skill content lives in exactly one place — `.taskless/skills/` — +The skill content lives in exactly one place, `.taskless/skills/`, and each tool directory holds only a short stub that points at it. Stale layouts from older installs (full per-tool copies, symlinks) are converged into stubs automatically. If the user is on v0.6 or @@ -50,5 +50,5 @@ what was removed. ## See Also -- `%(TASKLESS_CLI)s agent info` — verify what's installed and check staleness -- `%(TASKLESS_CLI)s agent auth` — authenticate after installing +- `%(TASKLESS_CLI)s agent info`: verify what's installed and check staleness +- `%(TASKLESS_CLI)s agent auth`: authenticate after installing diff --git a/packages/cli/src/agent/onboard.txt b/packages/cli/src/agent/onboard.txt index 7c176d2f..71534018 100644 --- a/packages/cli/src/agent/onboard.txt +++ b/packages/cli/src/agent/onboard.txt @@ -4,7 +4,7 @@ Help a user who has just installed Taskless go from zero rules to a useful starter set by mining their codebase, agent-memory files, PR review history, and issue tracker for high-signal rule candidates. -This is a conversational discovery flow, not a script — the agent +This is a conversational discovery flow, not a script, the agent collaborates with the user on what to scan and surfaces hypothetical rules as a bullet list the user can choose to materialize via the `rule create` flow. @@ -15,7 +15,7 @@ rules as a bullet list the user can choose to materialize via the is automatically satisfied. - A working repository the agent can read. - No auth required to surface candidates. (Materializing a rule via - `rule create` may require auth — fetch `%(TASKLESS_CLI)s agent auth` if + `rule create` may require auth. Fetch `%(TASKLESS_CLI)s agent auth` if needed at that point.) - The criterion for where a rule belongs lives in `%(TASKLESS_CLI)s agent route` and is not restated here. This recipe @@ -39,7 +39,7 @@ rules as a bullet list the user can choose to materialize via the %(TASKLESS_CLI)s detect --json ``` - Read `route` for the destination criterion — which rules are + Read `route` for the destination criterion, which rules are decided by one file's syntax tree, by a document's words, by a tool the repo already runs, or by evidence outside the files. Read `detect --json` for what this repository actually has: @@ -48,34 +48,34 @@ rules as a bullet list the user can choose to materialize via the Do this before you synthesize the bullet list, not after. Proposing first and routing afterwards is how a bullet list ends up carrying candidates that nothing can enforce. You are not - choosing a destination command here — that happens per accepted + choosing a destination command here. That happens per accepted bullet when you offer materialization. 3. **Open the conversation about sources.** Tell the user you can mine several places for rule candidates and ask which ones they want to include. Default sources you should always offer: - - **Codebase TODOs / FIXMEs** — search for `TODO`, `FIXME`, `XXX`, + - **Codebase TODOs / FIXMEs**: search for `TODO`, `FIXME`, `XXX`, and `HACK` comments. Many of these are latent rules ("don't do this", "remove when X"). - - **Agent-memory files** — read `CLAUDE.md`, `AGENTS.md`, + - **Agent-memory files**: read `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `.opencode/AGENTS.md`, and similar agent-context files for explicit rules and conventions stated in prose. - - **Recent PR review comments** — only if the `gh` CLI is + - **Recent PR review comments**: only if the `gh` CLI is available. Probe with `command -v gh`. Suggest scanning the last 30 days of merged PRs for repeated reviewer feedback patterns. - - **Issue tracker tickets** — only if a relevant MCP is wired in + - **Issue tracker tickets**: only if a relevant MCP is wired in (Linear, Jira, GitHub issues via `gh issue list`, etc.). Use whatever issue-tracker tools you have available. - Then explicitly ask: "Are there other places I should look — a + Then explicitly ask: "Are there other places I should look, a team wiki, an internal docs site, a specific design doc, a Slack channel export?" The user often knows about sources you don't. 4. **Probe tool availability before promising a scan.** For each source the user picks, verify the tool exists before committing to it. Don't tell the user "I'll scan PR comments" if `gh` isn't - installed — say "PR comments need the GitHub CLI, or equivalent; want me to skip + installed, say "PR comments need the GitHub CLI, or equivalent; want me to skip this or wait while you install these tools?" 5. **Scan with high-signal filtering.** For each chosen source: @@ -101,7 +101,7 @@ rules as a bullet list the user can choose to materialize via the ``` `` is the routing call you reached when you learned - the routing surface — one of + the routing surface, one of `legacy`, `sg`, `vale`, or `runtime`. Example: ``` @@ -111,20 +111,20 @@ rules as a bullet list the user can choose to materialize via the - exports-must-be-used [runtime]: Flag exported symbols nothing in the repo imports. ``` - The annotation is provisional — it tells the user what kind of rule + The annotation is provisional. It tells the user what kind of rule this would be, and it is why an unroutable candidate never reaches the list. `route` adjudicates for real at materialization time, and may land somewhere else once it has the rule's full description. Order the bullets by your judgment of impact (frequency in the source × severity of the issue × ease of enforcement). Don't - inflate the list — three high-quality candidates beat ten + inflate the list, three high-quality candidates beat ten speculative ones. 7. **Offer materialization per bullet.** For each bullet, ask whether the user wants to turn it into a real Taskless rule. On yes, follow the `%(TASKLESS_CLI)s agent route` recipe you already - fetched when you learned the routing surface — the user's accepted + fetched when you learned the routing surface, the user's accepted bullet becomes the rule description input. Re-fetch it only if it has fallen out of context. @@ -132,8 +132,8 @@ rules as a bullet list the user can choose to materialize via the they're done (they've materialized everything they want, or they've said "that's enough for now"), explicitly ask: "Do you want me to mark Taskless as onboarded? You won't be re-asked to - onboard until you pass `--force`." On explicit yes — and only - on explicit yes — run: + onboard until you pass `--force`." On explicit yes, and only + on explicit yes, run: ``` %(TASKLESS_CLI)s onboard --mark-complete @@ -141,7 +141,7 @@ rules as a bullet list the user can choose to materialize via the Do NOT mark onboarding complete on your own initiative. Do NOT run `--mark-complete` if the user is ambiguous, says "maybe - later", or simply stops responding. The flag is consent-gated. + later", or stops responding. The flag is consent-gated. ## Errors @@ -151,9 +151,9 @@ rules as a bullet list the user can choose to materialize via the ## See Also -- `%(TASKLESS_CLI)s agent route` — the destination criterion; read it +- `%(TASKLESS_CLI)s agent route`: the destination criterion; read it before proposing candidates, and follow it to materialize an accepted one -- `%(TASKLESS_CLI)s agent detect` — what this repository already lints +- `%(TASKLESS_CLI)s agent detect`: what this repository already lints and authors, which bounds what a candidate can be -- `%(TASKLESS_CLI)s agent check` — validate newly created rules against the codebase -- `%(TASKLESS_CLI)s agent info` — inspect the current `.taskless/taskless.json` state +- `%(TASKLESS_CLI)s agent check`: validate newly created rules against the codebase +- `%(TASKLESS_CLI)s agent info`: inspect the current `.taskless/taskless.json` state diff --git a/packages/cli/src/agent/route.txt b/packages/cli/src/agent/route.txt index 373aed4a..d76fdfd0 100644 --- a/packages/cli/src/agent/route.txt +++ b/packages/cli/src/agent/route.txt @@ -2,8 +2,8 @@ ## Goal Turn "write me a rule that…" into one command to run. This is the front -door for every rule-authoring request, and it makes one decision — which -of five recipes authors this rule — from one reading of the evidence. +door for every rule-authoring request, and it makes one decision (which +of five recipes authors this rule) from one reading of the evidence. That single decision covers both halves that used to be asked separately: whether the rule can be built here, and which engine can @@ -22,7 +22,7 @@ answered together. %(TASKLESS_CLI)s detect --json ``` This returns the configured linters, languages, and the repo's own - rule styles. It is deterministic and offline — use it as ground truth + rule styles. It is deterministic and offline. Use it as ground truth instead of guessing the repo's tooling. The scan is monorepo-aware, so evidence may carry a sub-package path. The output shape: ```json @@ -51,7 +51,7 @@ answered together. destinations you offer match what the CLI will actually allow. Reading it is not the same as asking about it. Do **not** open by - offering service generation — at this point neither you nor the user + offering service generation. At this point neither you nor the user knows whether this is a two-line pattern or something local authoring cannot express, so the question costs a turn and cannot be answered well. @@ -66,15 +66,15 @@ answered together. questions end up in `vale`. 4. **Match the evidence to a destination.** The comparison is made here - and only here — the destination recipes describe their own scope and + and only here. The destination recipes describe their own scope and deliberately do not restate this table. | The rule is decided by… | Destination | Login | |------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|------------| | a tool the repo already runs, in that tool's dialect | `create-legacy-rule` | no | -| **one file's syntax tree** — a call, an import, a JSX attribute, a type annotation | `create-sg-rule` | no | -| **a document's words** — docs, README, comments, commit bodies | `create-vale-rule` | no | -| **more than one file, or something outside the files** — the repo graph, git metadata, build output, a resolved config chain | `create-runtime-rule` / `create-remote-rule` | see step 5 | +| **one file's syntax tree**: a call, an import, a JSX attribute, a type annotation | `create-sg-rule` | no | +| **a document's words**: docs, README, comments, commit bodies | `create-vale-rule` | no | +| **more than one file, or something outside the files**: the repo graph, git metadata, build output, a resolved config chain | `create-runtime-rule` / `create-remote-rule` | see step 5 | Sharpening the three engine rows, because most wrong answers are one of these: @@ -83,13 +83,13 @@ answered together. `useEffect` whose dependency array omits a value used in its body" is one file's tree. If exactly one file settles it, it is `sg`. - **Prose about code is still prose.** "Comments must not say - 'obviously'" is `vale` — the evidence is the words. "Every exported - function has a doc comment" is `sg` — the evidence is whether a node + 'obviously'" is `vale`. The evidence is the words. "Every exported + function has a doc comment" is `sg`. The evidence is whether a node exists above a declaration. Ask what you would have to *read* to decide, not what the subject matter is. - **Vale sees one document at a time.** "This term is spelled consistently ACROSS the docs directory" is a graph question, so it - is runtime — even though it is entirely about prose. + is runtime, even though it is entirely about prose. - **Trust tier is not a destination.** `sg` and `vale` are both static-tier: inert data, always run, no login, no reconcile, no signing. Only runtime executes code. "Static vs runtime" is a @@ -104,8 +104,8 @@ answered together. - **ast-grep (v%(AST_GREP_VERSION)s) parses:** %(AST_GREP_LANGUAGES)s. Those spellings are ast-grep's own and go into a rule's `language:` - field verbatim. Nothing local validates that field — the vendored - schema types it as a bare string with no enum — so the first thing + field verbatim. Nothing local validates that field, the vendored + schema types it as a bare string with no enum, so the first thing with an opinion is the binary. It accepts some off-list aliases (`C++` and `cpp` both reach the Cpp parser), but a name it does not know at all, like `C#` for `CSharp`, aborts config parsing and @@ -150,19 +150,19 @@ answered together. - **Vale (v%(VALE_VERSION)s) reads three tiers, and hard-fails on a fourth.** The tier is decided by the file's extension: - - *markup* — the whole document is prose, and the format's own + - *markup*: the whole document is prose, and the format's own non-prose constructs are skipped: %(VALE_MARKUP_FORMATS)s - - *comments only* — the comment text is linted and the code body is + - *comments only*: the comment text is linted and the code body is invisible: %(VALE_COMMENT_FORMATS)s - - *plaintext fallback* — everything else, `.yml` `.toml` `.sh` + - *plaintext fallback*: everything else, `.yml` `.toml` `.sh` `.sql` and every unnamed extension included. There is no parser, so the file is linted as one block of prose, and a Vale rule scoped to YAML flags the code as readily as the comments. That is - rarely what was asked for — say so before writing it. These read + rarely what was asked for. Say so before writing it. These read like markup and are not: %(VALE_PLAINTEXT_FORMATS)s - - *not supported* — Vale parses these only by shelling out to an + - *not supported*: Vale parses these only by shelling out to an external program, and this build does not support any format that needs one: %(VALE_CONVERTER_FORMATS)s @@ -176,10 +176,10 @@ answered together. Vale exits 2 with an `E100` runtime error, `--no-exit` does not suppress it, and every other Vale rule over every other file goes unreported. A matcher written as `[*.{md,typ}]` is not a wider - `[*.md]` — it is a broken one. + `[*.md]`. It is a broken one. **`.mdx` is supported** as of Vale v%(VALE_VERSION)s, which - parses it natively — it needs no external program and belongs + parses it natively. It needs no external program and belongs with the other markup formats above. `.typ` moved the other way in the same release: Typst now parses through `typst2vast`, so a Typst file is excluded rather than read as prose the way it was @@ -242,7 +242,7 @@ answered together. Say which you would choose and why. Not logged in, no GitHub owner, or not locally expressible are not - choices — do not pose them as one. + choices. Do not pose them as one. 8. **Name the command.** Finish by telling the user, or running, the exact fetch for the destination you chose: @@ -302,14 +302,14 @@ will work. server-side the constraint is different again. A named default is wrong in whichever situation it failed to anticipate. - **Stay local when you reasonably can.** Reasonable confidence is - enough to commit to a local rule — the failure fallback backstops a + enough to commit to a local rule, the failure fallback backstops a wrong-but-reasonable bet. ## See Also -- `%(TASKLESS_CLI)s agent create-legacy-rule` — author in a linter the repo already uses -- `%(TASKLESS_CLI)s agent create-sg-rule` — author a local ast-grep rule (no login) -- `%(TASKLESS_CLI)s agent create-vale-rule` — author a local Vale rule (no login) -- `%(TASKLESS_CLI)s agent create-runtime-rule` — the runtime tier, logged out -- `%(TASKLESS_CLI)s agent create-remote-rule` — generate via the service (login) -- `%(TASKLESS_CLI)s agent check` — run every engine over the repo +- `%(TASKLESS_CLI)s agent create-legacy-rule`: author in a linter the repo already uses +- `%(TASKLESS_CLI)s agent create-sg-rule`: author a local ast-grep rule (no login) +- `%(TASKLESS_CLI)s agent create-vale-rule`: author a local Vale rule (no login) +- `%(TASKLESS_CLI)s agent create-runtime-rule`: the runtime tier, logged out +- `%(TASKLESS_CLI)s agent create-remote-rule`: generate via the service (login) +- `%(TASKLESS_CLI)s agent check`: run every engine over the repo diff --git a/packages/cli/src/agent/rule-meta.txt b/packages/cli/src/agent/rule-meta.txt index 1789c98f..72519625 100644 --- a/packages/cli/src/agent/rule-meta.txt +++ b/packages/cli/src/agent/rule-meta.txt @@ -27,4 +27,4 @@ version, etc. ## See Also -- `%(TASKLESS_CLI)s agent improve-rule` — the primary consumer of this command +- `%(TASKLESS_CLI)s agent improve-rule`: the primary consumer of this command diff --git a/packages/cli/src/agent/rule.txt b/packages/cli/src/agent/rule.txt index aea8fe42..fb5be5a8 100644 --- a/packages/cli/src/agent/rule.txt +++ b/packages/cli/src/agent/rule.txt @@ -21,4 +21,4 @@ For the local-only flow on improve, append `--anonymous`. ## See Also -- `%(TASKLESS_CLI)s agent check` — run all configured rules +- `%(TASKLESS_CLI)s agent check`: run all configured rules diff --git a/packages/cli/src/agent/update.txt b/packages/cli/src/agent/update.txt index 90f6186c..c0b3b3a0 100644 --- a/packages/cli/src/agent/update.txt +++ b/packages/cli/src/agent/update.txt @@ -2,7 +2,7 @@ ## Goal Update Taskless skills in the user's coding-agent tools to the -latest bundled version. Non-interactive — no wizard, no prompts. +latest bundled version. Non-interactive, no wizard, no prompts. Installs to all detected tool locations using the same logic as `%(TASKLESS_CLI)s init --no-interactive`, but exposed as its own subcommand so the agent can run it directly without explaining flags. @@ -44,5 +44,5 @@ non-zero with the error message on stderr. ## See Also -- `%(TASKLESS_CLI)s agent init` — interactive variant (wizard with prompts) -- `%(TASKLESS_CLI)s agent info` — verify what's installed and check staleness +- `%(TASKLESS_CLI)s agent init`: interactive variant (wizard with prompts) +- `%(TASKLESS_CLI)s agent info`: verify what's installed and check staleness diff --git a/packages/cli/src/agent/verify-rule.txt b/packages/cli/src/agent/verify-rule.txt index 06418f19..4cc03b4b 100644 --- a/packages/cli/src/agent/verify-rule.txt +++ b/packages/cli/src/agent/verify-rule.txt @@ -77,10 +77,10 @@ with its errors indented beneath. ## Exit codes -- `0` — every rule in scope passed. An empty rules tree also exits 0: a +- `0`: every rule in scope passed. An empty rules tree also exits 0: a project that has not written a rule yet is an ordinary state, and failing there would make `verify` unusable in CI on a fresh install. -- `1` — any rule failed, or the path could not be resolved. +- `1`: any rule failed, or the path could not be resolved. ## In a feedback loop @@ -90,6 +90,6 @@ it still fails. ## See Also -- `%(TASKLESS_CLI)s agent create-sg-rule` — author an ast-grep rule -- `%(TASKLESS_CLI)s agent create-vale-rule` — author a Vale rule -- `%(TASKLESS_CLI)s agent improve-rule` — iterate on a rule that already exists +- `%(TASKLESS_CLI)s agent create-sg-rule`: author an ast-grep rule +- `%(TASKLESS_CLI)s agent create-vale-rule`: author a Vale rule +- `%(TASKLESS_CLI)s agent improve-rule`: iterate on a rule that already exists diff --git a/packages/vale-darwin-arm64/README.md b/packages/vale-darwin-arm64/README.md index d8ce870f..13b3e1a0 100644 --- a/packages/vale-darwin-arm64/README.md +++ b/packages/vale-darwin-arm64/README.md @@ -10,7 +10,7 @@ install time. Nothing else: no `bin` entry, no JavaScript, and no lifecycle script. A consumer locates the executable by resolving this package and running the file by path, so the binary is usable even where the consuming package manager refuses to run -dependency build scripts — which pnpm 10 does by default. +dependency build scripts, which pnpm 10 does by default. `os` and `cpu` are declared, so this package installs only on a matching host and is skipped everywhere else without failing the install. diff --git a/packages/vale-darwin-x64/README.md b/packages/vale-darwin-x64/README.md index b9f796f1..31f9f9fb 100644 --- a/packages/vale-darwin-x64/README.md +++ b/packages/vale-darwin-x64/README.md @@ -10,7 +10,7 @@ install time. Nothing else: no `bin` entry, no JavaScript, and no lifecycle script. A consumer locates the executable by resolving this package and running the file by path, so the binary is usable even where the consuming package manager refuses to run -dependency build scripts — which pnpm 10 does by default. +dependency build scripts, which pnpm 10 does by default. `os` and `cpu` are declared, so this package installs only on a matching host and is skipped everywhere else without failing the install. diff --git a/packages/vale-linux-arm64/README.md b/packages/vale-linux-arm64/README.md index e31a3973..999a3687 100644 --- a/packages/vale-linux-arm64/README.md +++ b/packages/vale-linux-arm64/README.md @@ -10,7 +10,7 @@ install time. Nothing else: no `bin` entry, no JavaScript, and no lifecycle script. A consumer locates the executable by resolving this package and running the file by path, so the binary is usable even where the consuming package manager refuses to run -dependency build scripts — which pnpm 10 does by default. +dependency build scripts, which pnpm 10 does by default. `os` and `cpu` are declared, so this package installs only on a matching host and is skipped everywhere else without failing the install. @@ -43,9 +43,9 @@ the same digest upstream publishes in `vale__checksums.txt`. ## glibc, and why there is no musl package -Vale's Linux build is dynamically linked against glibc — `ELF 64-bit LSB +Vale's Linux build is dynamically linked against glibc (`ELF 64-bit LSB executable, ARM aarch64, dynamically linked, interpreter -/lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0` — so it is not a static Go +/lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0`), so it is not a static Go binary and it does not run on musl-based distributions such as Alpine. Upstream publishes no musl asset, so there is nothing to package for those hosts; they fall back to a `vale` found on `PATH`. diff --git a/packages/vale-linux-x64/README.md b/packages/vale-linux-x64/README.md index 9e57f38f..20f59007 100644 --- a/packages/vale-linux-x64/README.md +++ b/packages/vale-linux-x64/README.md @@ -10,7 +10,7 @@ install time. Nothing else: no `bin` entry, no JavaScript, and no lifecycle script. A consumer locates the executable by resolving this package and running the file by path, so the binary is usable even where the consuming package manager refuses to run -dependency build scripts — which pnpm 10 does by default. +dependency build scripts, which pnpm 10 does by default. `os` and `cpu` are declared, so this package installs only on a matching host and is skipped everywhere else without failing the install. @@ -43,9 +43,9 @@ the same digest upstream publishes in `vale__checksums.txt`. ## glibc, and why there is no musl package -Vale's Linux build is dynamically linked against glibc — `ELF 64-bit LSB +Vale's Linux build is dynamically linked against glibc (`ELF 64-bit LSB executable, dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for -GNU/Linux 3.2.0` — so it is not a static Go binary and it does not run on +GNU/Linux 3.2.0`), so it is not a static Go binary and it does not run on musl-based distributions such as Alpine. Upstream publishes no musl asset, so there is nothing to package for those hosts; they fall back to a `vale` found on `PATH`. diff --git a/packages/vale-win32-arm64/README.md b/packages/vale-win32-arm64/README.md index d45c5bfa..7737b72e 100644 --- a/packages/vale-win32-arm64/README.md +++ b/packages/vale-win32-arm64/README.md @@ -10,7 +10,7 @@ install time. Nothing else: no `bin` entry, no JavaScript, and no lifecycle script. A consumer locates the executable by resolving this package and running the file by path, so the binary is usable even where the consuming package manager refuses to run -dependency build scripts — which pnpm 10 does by default. +dependency build scripts, which pnpm 10 does by default. `os` and `cpu` are declared, so this package installs only on a matching host and is skipped everywhere else without failing the install. diff --git a/packages/vale-win32-x64/README.md b/packages/vale-win32-x64/README.md index 63d1e2a8..b9106068 100644 --- a/packages/vale-win32-x64/README.md +++ b/packages/vale-win32-x64/README.md @@ -10,7 +10,7 @@ install time. Nothing else: no `bin` entry, no JavaScript, and no lifecycle script. A consumer locates the executable by resolving this package and running the file by path, so the binary is usable even where the consuming package manager refuses to run -dependency build scripts — which pnpm 10 does by default. +dependency build scripts, which pnpm 10 does by default. `os` and `cpu` are declared, so this package installs only on a matching host and is skipped everywhere else without failing the install.