Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .agents/skills/taskless/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
name: taskless
description: |
Use for any Taskless task. Trigger when the user mentions Taskless by name,
or when their request involves the .taskless/ directory or files in it
(rules, rule-tests, rule-metadata).

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

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

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

Read `.taskless/skills/taskless/SKILL.md` and follow its instructions.
6 changes: 5 additions & 1 deletion .github/workflows/stack-breadcrumb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ name: Stack Breadcrumb
on:
pull_request:
# Tree SHAPE only — no `synchronize` (a head push never changes membership).
types: [opened, reopened, edited, closed]
# `ready_for_review` is not in the default set and is named deliberately: a
# draft becoming ready is the moment the PR joins the reviewable stack, and
# without it the breadcrumb keeps describing the PR as a draft until some
# other event happens to fire.
types: [opened, reopened, edited, ready_for_review, closed]
repository_dispatch:
types: [stack-reconcile]
workflow_dispatch:
Expand Down
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ worktrees/

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

# Taskless rule fixtures: deliberately-wrong prose and source.
.taskless/
4 changes: 3 additions & 1 deletion .taskless/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.env.local.json
sgconfig.yml
/sgconfig.yml
/.vale.ini
/.sgconfig.yml
Empty file.
9 changes: 9 additions & 0 deletions .taskless/rules/sg/no-eval/.tests/no-eval-20260824-test.yml
Original file line number Diff line number Diff line change
@@ -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");
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
20 changes: 20 additions & 0 deletions .taskless/rules/sg/no-index-imports/no-index-imports.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 },
});
22 changes: 22 additions & 0 deletions .taskless/rules/sg/no-pii-in-telemetry/no-pii-in-telemetry.yml
Original file line number Diff line number Diff line change
@@ -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($$$)
Original file line number Diff line number Diff line change
@@ -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();
});
Original file line number Diff line number Diff line change
@@ -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/'
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
@@ -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$'
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
@@ -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$'
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
id: prefer-do-rpc
valid:
- |
const orgDO = env.GITHUB_ORGANIZATION_DO.get(
orgDoId,
) as DurableObjectRPC<GitHubOrganizationDO>;
- |
const stub = env.USER_DO.get(id);
invalid:
- |
const orgDO = env.GITHUB_ORGANIZATION_DO.get(
orgDoId,
) as DurableObjectStub<GitHubOrganizationDO>;
- |
const userDO = env.USER_DO.get(userDoId) as DurableObjectStub<UserDO>;
14 changes: 14 additions & 0 deletions .taskless/rules/sg/prefer-do-rpc/prefer-do-rpc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
id: prefer-do-rpc
language: TypeScript
severity: warning
message: Use DurableObjectRPC<T> for cross-worker Durable Object access.
note: |
`DurableObjectStub<T>` does not Promise-wrap method returns, so a
cross-worker RPC call type-checks against a synchronous signature it will
never have. `DurableObjectRPC<T>` 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<T>` is correct — silence this rule there.
rule:
pattern: $EXPR as DurableObjectStub<$T>
Empty file added .taskless/rules/vale/.gitkeep
Empty file.
Loading
Loading