From ea21e7e70f8227c40b5d7129a80a9ce2e5b25554 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 24 Aug 2026 15:16:06 -0700 Subject: [PATCH 1/8] feat(taskless): an onboarding pass over our own repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven rules mined from CLAUDE.md, the .conventions styleguides, and the posthog conventions — the places this repo already states its rules in prose. Each one is verified and tested locally; `check` reports two real findings, both READMEs telling external consumers to run `pnpm dlx`. sg: no-index-imports relative imports ending in /index no-pii-in-telemetry PII in capture/identify/groupIdentify prefer-do-rpc `as DurableObjectStub` casts no-regex-over-build-output a test function that reads dist/ and regexes it no-pull-request-branches-filter a branches: filter under pull_request: vale: docs-npx-cli pnpm invocations in READMEs no-hedging simply / obviously / of course Also backfills tests for the pre-existing no-eval rule, which was failing `verify` for a missing test file, and puts every rule's `language:` on ast-grep's own spelling. The install churn — the rules/ partition, taskless.json, the embedded SKILL.md — was already in the tree and is carried along rather than authored here. --- .agents/skills/taskless/SKILL.md | 27 +++++++++++++ .taskless/.gitignore | 4 +- .taskless/rules/runtime/.gitkeep | 0 .../no-eval/.tests/no-eval-20260824-test.yml | 9 +++++ .taskless/rules/{ => sg/no-eval}/no-eval.yml | 2 +- .../.tests/no-index-imports-20260824-test.yml | 10 +++++ .../sg/no-index-imports/no-index-imports.yml | 20 ++++++++++ .../no-pii-in-telemetry-20260824-test.yml | 25 ++++++++++++ .../no-pii-in-telemetry.yml | 22 ++++++++++ ...-request-branches-filter-20260824-test.yml | 31 ++++++++++++++ .../no-pull-request-branches-filter.yml | 40 +++++++++++++++++++ ...-regex-over-build-output-20260824-test.yml | 31 ++++++++++++++ .../no-regex-over-build-output.yml | 36 +++++++++++++++++ .../.tests/prefer-do-rpc-20260824-test.yml | 15 +++++++ .../rules/sg/prefer-do-rpc/prefer-do-rpc.yml | 14 +++++++ .taskless/rules/vale/.gitkeep | 0 .../vale/docs-npx-cli/.tests/fail/README.md | 13 ++++++ .../vale/docs-npx-cli/.tests/pass/README.md | 12 ++++++ .taskless/rules/vale/docs-npx-cli/.vale.ini | 12 ++++++ .../rules/vale/docs-npx-cli/docs-npx-cli.yml | 8 ++++ .../vale/no-hedging/.tests/fail/README.md | 7 ++++ .../vale/no-hedging/.tests/pass/README.md | 8 ++++ .taskless/rules/vale/no-hedging/.vale.ini | 17 ++++++++ .../rules/vale/no-hedging/no-hedging.yml | 10 +++++ .taskless/sgconfig.yml | 2 - .taskless/taskless.json | 13 ++++-- 26 files changed, 380 insertions(+), 8 deletions(-) create mode 100644 .agents/skills/taskless/SKILL.md create mode 100644 .taskless/rules/runtime/.gitkeep create mode 100644 .taskless/rules/sg/no-eval/.tests/no-eval-20260824-test.yml rename .taskless/rules/{ => sg/no-eval}/no-eval.yml (94%) create mode 100644 .taskless/rules/sg/no-index-imports/.tests/no-index-imports-20260824-test.yml create mode 100644 .taskless/rules/sg/no-index-imports/no-index-imports.yml create mode 100644 .taskless/rules/sg/no-pii-in-telemetry/.tests/no-pii-in-telemetry-20260824-test.yml create mode 100644 .taskless/rules/sg/no-pii-in-telemetry/no-pii-in-telemetry.yml create mode 100644 .taskless/rules/sg/no-pull-request-branches-filter/.tests/no-pull-request-branches-filter-20260824-test.yml create mode 100644 .taskless/rules/sg/no-pull-request-branches-filter/no-pull-request-branches-filter.yml create mode 100644 .taskless/rules/sg/no-regex-over-build-output/.tests/no-regex-over-build-output-20260824-test.yml create mode 100644 .taskless/rules/sg/no-regex-over-build-output/no-regex-over-build-output.yml create mode 100644 .taskless/rules/sg/prefer-do-rpc/.tests/prefer-do-rpc-20260824-test.yml create mode 100644 .taskless/rules/sg/prefer-do-rpc/prefer-do-rpc.yml create mode 100644 .taskless/rules/vale/.gitkeep create mode 100644 .taskless/rules/vale/docs-npx-cli/.tests/fail/README.md create mode 100644 .taskless/rules/vale/docs-npx-cli/.tests/pass/README.md create mode 100644 .taskless/rules/vale/docs-npx-cli/.vale.ini create mode 100644 .taskless/rules/vale/docs-npx-cli/docs-npx-cli.yml create mode 100644 .taskless/rules/vale/no-hedging/.tests/fail/README.md create mode 100644 .taskless/rules/vale/no-hedging/.tests/pass/README.md create mode 100644 .taskless/rules/vale/no-hedging/.vale.ini create mode 100644 .taskless/rules/vale/no-hedging/no-hedging.yml delete mode 100644 .taskless/sgconfig.yml 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/.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/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-pull-request-branches-filter/.tests/no-pull-request-branches-filter-20260824-test.yml b/.taskless/rules/sg/no-pull-request-branches-filter/.tests/no-pull-request-branches-filter-20260824-test.yml new file mode 100644 index 00000000..e45c90a1 --- /dev/null +++ b/.taskless/rules/sg/no-pull-request-branches-filter/.tests/no-pull-request-branches-filter-20260824-test.yml @@ -0,0 +1,31 @@ +id: no-pull-request-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/no-pull-request-branches-filter/no-pull-request-branches-filter.yml b/.taskless/rules/sg/no-pull-request-branches-filter/no-pull-request-branches-filter.yml new file mode 100644 index 00000000..2b89ba35 --- /dev/null +++ b/.taskless/rules/sg/no-pull-request-branches-filter/no-pull-request-branches-filter.yml @@ -0,0 +1,40 @@ +id: no-pull-request-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/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/prefer-do-rpc/.tests/prefer-do-rpc-20260824-test.yml b/.taskless/rules/sg/prefer-do-rpc/.tests/prefer-do-rpc-20260824-test.yml new file mode 100644 index 00000000..bb39dfd9 --- /dev/null +++ b/.taskless/rules/sg/prefer-do-rpc/.tests/prefer-do-rpc-20260824-test.yml @@ -0,0 +1,15 @@ +id: prefer-do-rpc +valid: + - | + const orgDO = env.GITHUB_ORGANIZATION_DO.get( + orgDoId, + ) as DurableObjectRPC; + - | + const stub = env.USER_DO.get(id); +invalid: + - | + const orgDO = env.GITHUB_ORGANIZATION_DO.get( + orgDoId, + ) as DurableObjectStub; + - | + const userDO = env.USER_DO.get(userDoId) as DurableObjectStub; diff --git a/.taskless/rules/sg/prefer-do-rpc/prefer-do-rpc.yml b/.taskless/rules/sg/prefer-do-rpc/prefer-do-rpc.yml new file mode 100644 index 00000000..b3b6ab65 --- /dev/null +++ b/.taskless/rules/sg/prefer-do-rpc/prefer-do-rpc.yml @@ -0,0 +1,14 @@ +id: prefer-do-rpc +language: TypeScript +severity: warning +message: Use DurableObjectRPC for cross-worker Durable Object access. +note: | + `DurableObjectStub` does not Promise-wrap method returns, so a + cross-worker RPC call type-checks against a synchronous signature it will + never have. `DurableObjectRPC` from `@taskless/shared/rpc` wraps every + method return in `Promise<>`. + + A `get()` used from inside the worker that owns the DO is the one case + where `DurableObjectStub` is correct — silence this rule there. +rule: + pattern: $EXPR as DurableObjectStub<$T> 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/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..7924450f --- /dev/null +++ b/.taskless/rules/vale/docs-npx-cli/.vale.ini @@ -0,0 +1,12 @@ +# 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. +[**/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-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..af02a2a0 --- /dev/null +++ b/.taskless/rules/vale/no-hedging/.vale.ini @@ -0,0 +1,17 @@ +# 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 + +[.conventions/*.md] +tskl) rule = no-hedging +BasedOnStyles = +no-hedging.no-hedging = YES + +# Test fixtures are inputs to the CLI's own suite, not documentation. +[**/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..cbaef7dc 100644 --- a/.taskless/taskless.json +++ b/.taskless/taskless.json @@ -1,13 +1,18 @@ { - "version": 2, + "version": 5, "install": { "targets": { + ".taskless": { + "skills": ["taskless"], + "commands": ["tskl.md"], + "mode": "canonical" + }, ".claude": { "skills": ["taskless"], - "commands": ["tskl.md"] + "commands": ["tskl.md"], + "mode": "reference" } }, - "installedAt": "2026-05-11T16:38:34.273Z", - "cliVersion": "0.6.0" + "cliVersion": "0.11.0-20260824213902xf26a7b0" } } From d6abfe667494e3f69d565eeb6579d61214d7bac7 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 24 Aug 2026 17:04:28 -0700 Subject: [PATCH 2/8] feat(taskless): add the second workflow rule the sweep asked for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A sweep of merged-PR review threads and the OSS Linear backlog turned up OSS-49, which names two planned GitHub Actions rules and records that a prior onboarding pass stalled on whether ast-grep handles YAML. It does, so both are buildable locally rather than runtime-tier. Adds pr-workflow-ready-for-review, and renames the branches-filter rule to the pr-workflow-* name OSS-49 already uses for the pair. One finding on the real workflows: stack-breadcrumb.yml names types: [opened, reopened, edited, closed] with no ready_for_review. That is a question rather than a defect — it reacts to PR metadata, not to readiness — and is left for review to settle. --- ...flow-no-branches-filter-20260824-test.yml} | 2 +- .../pr-workflow-no-branches-filter.yml} | 2 +- ...orkflow-ready-for-review-20260824-test.yml | 27 +++++++++++++ .../pr-workflow-ready-for-review.yml | 40 +++++++++++++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) rename .taskless/rules/sg/{no-pull-request-branches-filter/.tests/no-pull-request-branches-filter-20260824-test.yml => pr-workflow-no-branches-filter/.tests/pr-workflow-no-branches-filter-20260824-test.yml} (93%) rename .taskless/rules/sg/{no-pull-request-branches-filter/no-pull-request-branches-filter.yml => pr-workflow-no-branches-filter/pr-workflow-no-branches-filter.yml} (97%) create mode 100644 .taskless/rules/sg/pr-workflow-ready-for-review/.tests/pr-workflow-ready-for-review-20260824-test.yml create mode 100644 .taskless/rules/sg/pr-workflow-ready-for-review/pr-workflow-ready-for-review.yml diff --git a/.taskless/rules/sg/no-pull-request-branches-filter/.tests/no-pull-request-branches-filter-20260824-test.yml b/.taskless/rules/sg/pr-workflow-no-branches-filter/.tests/pr-workflow-no-branches-filter-20260824-test.yml similarity index 93% rename from .taskless/rules/sg/no-pull-request-branches-filter/.tests/no-pull-request-branches-filter-20260824-test.yml rename to .taskless/rules/sg/pr-workflow-no-branches-filter/.tests/pr-workflow-no-branches-filter-20260824-test.yml index e45c90a1..5edc5906 100644 --- a/.taskless/rules/sg/no-pull-request-branches-filter/.tests/no-pull-request-branches-filter-20260824-test.yml +++ b/.taskless/rules/sg/pr-workflow-no-branches-filter/.tests/pr-workflow-no-branches-filter-20260824-test.yml @@ -1,4 +1,4 @@ -id: no-pull-request-branches-filter +id: pr-workflow-no-branches-filter valid: - | on: diff --git a/.taskless/rules/sg/no-pull-request-branches-filter/no-pull-request-branches-filter.yml b/.taskless/rules/sg/pr-workflow-no-branches-filter/pr-workflow-no-branches-filter.yml similarity index 97% rename from .taskless/rules/sg/no-pull-request-branches-filter/no-pull-request-branches-filter.yml rename to .taskless/rules/sg/pr-workflow-no-branches-filter/pr-workflow-no-branches-filter.yml index 2b89ba35..972f68f9 100644 --- a/.taskless/rules/sg/no-pull-request-branches-filter/no-pull-request-branches-filter.yml +++ b/.taskless/rules/sg/pr-workflow-no-branches-filter/pr-workflow-no-branches-filter.yml @@ -1,4 +1,4 @@ -id: no-pull-request-branches-filter +id: pr-workflow-no-branches-filter language: Yaml severity: error message: A pull_request trigger must not carry a branches filter. 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..decb6ed4 --- /dev/null +++ b/.taskless/rules/sg/pr-workflow-ready-for-review/.tests/pr-workflow-ready-for-review-20260824-test.yml @@ -0,0 +1,27 @@ +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] 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..f64e11d2 --- /dev/null +++ b/.taskless/rules/sg/pr-workflow-ready-for-review/pr-workflow-ready-for-review.yml @@ -0,0 +1,40 @@ +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: + 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$' From d53858b641a4309267dc007dd57b87e09eb22b49 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 24 Aug 2026 17:42:08 -0700 Subject: [PATCH 3/8] feat(taskless): add the OSS-28 house-style rules, scoped to READMEs OSS-28 specifies the house writing style as Vale rules. This adds the two that can be expressed cleanly, scoped to READMEs rather than the whole repository: broadening is a separate decision with roughly 2200 findings attached. no-em-dashes needs `nonword: true`, which the snippet in OSS-28 does not carry. Measured: without it the rule parses, passes verify, and does not fire, because Vale wraps every token in word boundaries and an em dash is non-word on both sides. no-blocklist-phrases drops the `landed on` token OSS-28 proposed. Measured firing on "the plane landed on time", which is the literal sense the issue says to protect; `we landed` already covers "we landed on the second option", so the broader token earned nothing. Not included: the "not X, Y" antithesis rule. OSS-28 is undecided on how to express it and calls it a heuristic, so it wants a human judgement this pass cannot make. 24 findings in READMEs, all em dashes, left unfixed pending a call on whether to rewrite the prose or relax the level. --- .../.tests/fail/README.md | 9 +++++ .../.tests/pass/README.md | 9 +++++ .../rules/vale/no-blocklist-phrases/.vale.ini | 12 +++++++ .../no-blocklist-phrases.yml | 33 +++++++++++++++++++ .../vale/no-em-dashes/.tests/fail/README.md | 5 +++ .../vale/no-em-dashes/.tests/pass/README.md | 6 ++++ .taskless/rules/vale/no-em-dashes/.vale.ini | 12 +++++++ .../rules/vale/no-em-dashes/no-em-dashes.yml | 7 ++++ 8 files changed, 93 insertions(+) create mode 100644 .taskless/rules/vale/no-blocklist-phrases/.tests/fail/README.md create mode 100644 .taskless/rules/vale/no-blocklist-phrases/.tests/pass/README.md create mode 100644 .taskless/rules/vale/no-blocklist-phrases/.vale.ini create mode 100644 .taskless/rules/vale/no-blocklist-phrases/no-blocklist-phrases.yml create mode 100644 .taskless/rules/vale/no-em-dashes/.tests/fail/README.md create mode 100644 .taskless/rules/vale/no-em-dashes/.tests/pass/README.md create mode 100644 .taskless/rules/vale/no-em-dashes/.vale.ini create mode 100644 .taskless/rules/vale/no-em-dashes/no-em-dashes.yml 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..4e38dbcd --- /dev/null +++ b/.taskless/rules/vale/no-blocklist-phrases/.vale.ini @@ -0,0 +1,12 @@ +# 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 + +# Test fixtures are inputs to the CLI's own suite, not documentation. +[**/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..a837d6cb --- /dev/null +++ b/.taskless/rules/vale/no-em-dashes/.vale.ini @@ -0,0 +1,12 @@ +# 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 + +# Test fixtures are inputs to the CLI's own suite, not documentation. +[**/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: + - '—' + - '–' From 8123ac463377a0d91e478a13dc2f71bf8aec63d3 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 24 Aug 2026 17:47:53 -0700 Subject: [PATCH 4/8] docs: remove em dashes from every README Brings the nine READMEs in line with no-em-dashes, which this branch scopes to `**/README.md`. Twenty-four instances, rewritten as a period, comma, colon, or parentheses per the rule's own message rather than mechanically swapped for one substitute. --- README.md | 18 +++++++++--------- packages/cli/README.md | 20 ++++++++++---------- packages/vale-darwin-arm64/README.md | 2 +- packages/vale-darwin-x64/README.md | 2 +- packages/vale-linux-arm64/README.md | 6 +++--- packages/vale-linux-x64/README.md | 6 +++--- packages/vale-win32-arm64/README.md | 2 +- packages/vale-win32-x64/README.md | 2 +- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 11e1df90..0862ee11 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 @@ -46,7 +46,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,12 +56,12 @@ 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. ## Releasing taskless/cli @@ -79,7 +79,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 +109,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 +129,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/packages/cli/README.md b/packages/cli/README.md index 357e95e6..18c441b4 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -26,7 +26,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 +40,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 +60,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 +70,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/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. From 0a3762a68bec3316bf91f9ac043e515381961225 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 24 Aug 2026 18:14:40 -0700 Subject: [PATCH 5/8] docs: install with npx, and drop the pnpm dlx variant docs-npx-cli says docs use `npx @taskless/cli`, the normalized invocation for external consumers. Two README blocks offered `pnpm dlx` alongside it, which is the same claim the rule exists to prevent, so the alternative is removed rather than annotated. Annotating was measured and is not available here: a rule scoped to `raw` reads the unparsed document, so Vale's `` directive is inert for it. That scope is what lets the rule see fenced blocks at all, and every command in a README lives in one. --- README.md | 1 - packages/cli/README.md | 4 ---- 2 files changed, 5 deletions(-) diff --git a/README.md b/README.md index 0862ee11..e8a3e959 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/packages/cli/README.md b/packages/cli/README.md index 18c441b4..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 From 0be74f678ebfcc248215266dea6fd5e98670be10 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 24 Aug 2026 18:53:05 -0700 Subject: [PATCH 6/8] feat(taskless): record measurements in comments, do not forecast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The most repeated finding in the merged-PR review threads: a comment that cites a version or a claim the code moved out from under it. On #161 alone, five threads — "we pin 3.17.1" left behind after VALE_VERSION went to 3.18.0, a comment naming .rmd as plaintext after the diff moved it to markup, a block still framing 3.18.0 as "the known incoming bump" after it shipped. Same shape on #155 and #157. The rule flags the construction rather than the stale number, which is the half a static check can see. Vale reads .ts in its comments-only tier, so it lints the comment text and never the code. Zero findings on packages/cli/src today, because #161 fixed them all. Reach verified separately by planting a forecast comment in a real source file: fixtures run under an isolating config, so a passing test does not prove the matcher glob reaches anything. Also ignores .taskless/ in eslint and prettier. A rule about source comments needs .ts fixtures, Vale decides its comments-only tier by extension, and those files belong to no tsconfig — so lint-staged failed the commit on "not found by the project service". Rule fixtures are inputs written to be flagged; verify and test are what keep them honest. --- .prettierignore | 3 +++ .../.tests/fail/probe.ts | 6 ++++++ .../.tests/pass/probe.ts | 7 +++++++ .../comments-record-not-forecast/.vale.ini | 6 ++++++ .../comments-record-not-forecast.yml | 15 +++++++++++++++ .taskless/taskless.json | 19 ++++++++++++++----- eslint.config.js | 6 ++++++ 7 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 .taskless/rules/vale/comments-record-not-forecast/.tests/fail/probe.ts create mode 100644 .taskless/rules/vale/comments-record-not-forecast/.tests/pass/probe.ts create mode 100644 .taskless/rules/vale/comments-record-not-forecast/.vale.ini create mode 100644 .taskless/rules/vale/comments-record-not-forecast/comments-record-not-forecast.yml 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/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/taskless.json b/.taskless/taskless.json index cbaef7dc..d0d82140 100644 --- a/.taskless/taskless.json +++ b/.taskless/taskless.json @@ -3,16 +3,25 @@ "install": { "targets": { ".taskless": { - "skills": ["taskless"], - "commands": ["tskl.md"], + "skills": [ + "taskless" + ], + "commands": [ + "tskl.md" + ], "mode": "canonical" }, ".claude": { - "skills": ["taskless"], - "commands": ["tskl.md"], + "skills": [ + "taskless" + ], + "commands": [ + "tskl.md" + ], "mode": "reference" } }, - "cliVersion": "0.11.0-20260824213902xf26a7b0" + "cliVersion": "0.11.0-20260824213902xf26a7b0", + "onboarded": true } } 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 From c514efebd8d6478a0f519c359eac2a079ec3dfa8 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 24 Aug 2026 20:03:56 -0700 Subject: [PATCH 7/8] ci: run the breadcrumb when a draft becomes ready MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pr-workflow-ready-for-review flagged this as the only finding on the repo, and it is a real gap rather than the intended exception. `ready_for_review` is not in the default event set, so a draft marked ready fired nothing here: the breadcrumb went on describing the PR as a draft until some later event happened to run. The existing comment says tree SHAPE only, which is still true — `synchronize` stays out because a head push never changes membership. A draft becoming ready does change the stack, so it is named explicitly and the comment now says why. `taskless check` is clean at 12 rules. --- .github/workflows/stack-breadcrumb.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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: From e8551988499f83355cb8500527344be843a5c7dc Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Tue, 25 Aug 2026 21:17:50 -0700 Subject: [PATCH 8/8] feat(taskless): drop prefer-do-rpc, document the local cli script prefer-do-rpc targets `$EXPR as DurableObjectStub<$T>`, a Cloudflare Durable Objects pattern. The Taskless services need it; this repository has no Cloudflare code for it to reach, so the rule and its synthetic fixtures are removed rather than carried pre-emptively. Nothing else in the tree referenced it. docs-npx-cli stays as written, and the root README now records that `package.json` carries a `cli` script pointing at `./packages/cli/dist/index.js` for running the local build. The note names the script instead of reproducing its shell invocation, because docs-npx-cli has no per-case escape hatch: its `raw` scope is what reaches a fenced code block, and Vale evaluates a raw scope against the unparsed markup, so in-file directives are ignored. Measured on Vale 3.18.0, both the rule-specific comment and a blanket `vale off` fail to suppress it, and dropping `raw` makes them work at the cost of every fenced-block finding. The rule keeps its full reach; the finding is recorded in its .vale.ini. --- .../.tests/prefer-do-rpc-20260824-test.yml | 15 --------------- .../rules/sg/prefer-do-rpc/prefer-do-rpc.yml | 14 -------------- .taskless/rules/vale/docs-npx-cli/.vale.ini | 10 ++++++++++ README.md | 12 ++++++++++++ 4 files changed, 22 insertions(+), 29 deletions(-) delete mode 100644 .taskless/rules/sg/prefer-do-rpc/.tests/prefer-do-rpc-20260824-test.yml delete mode 100644 .taskless/rules/sg/prefer-do-rpc/prefer-do-rpc.yml diff --git a/.taskless/rules/sg/prefer-do-rpc/.tests/prefer-do-rpc-20260824-test.yml b/.taskless/rules/sg/prefer-do-rpc/.tests/prefer-do-rpc-20260824-test.yml deleted file mode 100644 index bb39dfd9..00000000 --- a/.taskless/rules/sg/prefer-do-rpc/.tests/prefer-do-rpc-20260824-test.yml +++ /dev/null @@ -1,15 +0,0 @@ -id: prefer-do-rpc -valid: - - | - const orgDO = env.GITHUB_ORGANIZATION_DO.get( - orgDoId, - ) as DurableObjectRPC; - - | - const stub = env.USER_DO.get(id); -invalid: - - | - const orgDO = env.GITHUB_ORGANIZATION_DO.get( - orgDoId, - ) as DurableObjectStub; - - | - const userDO = env.USER_DO.get(userDoId) as DurableObjectStub; diff --git a/.taskless/rules/sg/prefer-do-rpc/prefer-do-rpc.yml b/.taskless/rules/sg/prefer-do-rpc/prefer-do-rpc.yml deleted file mode 100644 index b3b6ab65..00000000 --- a/.taskless/rules/sg/prefer-do-rpc/prefer-do-rpc.yml +++ /dev/null @@ -1,14 +0,0 @@ -id: prefer-do-rpc -language: TypeScript -severity: warning -message: Use DurableObjectRPC for cross-worker Durable Object access. -note: | - `DurableObjectStub` does not Promise-wrap method returns, so a - cross-worker RPC call type-checks against a synchronous signature it will - never have. `DurableObjectRPC` from `@taskless/shared/rpc` wraps every - method return in `Promise<>`. - - A `get()` used from inside the worker that owns the DO is the one case - where `DurableObjectStub` is correct — silence this rule there. -rule: - pattern: $EXPR as DurableObjectStub<$T> diff --git a/.taskless/rules/vale/docs-npx-cli/.vale.ini b/.taskless/rules/vale/docs-npx-cli/.vale.ini index 7924450f..d1acb470 100644 --- a/.taskless/rules/vale/docs-npx-cli/.vale.ini +++ b/.taskless/rules/vale/docs-npx-cli/.vale.ini @@ -1,5 +1,15 @@ # 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 = diff --git a/README.md b/README.md index e8a3e959..a09782c1 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,18 @@ step, so local dogfooding always matches a true install.) > The `dev`/`self` invocations are local paths and must never be published: > only `pnpm build` (or `pnpm package`) produces a release artifact. +### 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.