A .taskless/ tree that is already in the current rule-directory layout but has no taskless.json reads as schema version 0. The migrations then "upgrade" it: the rules are moved to .taskless/sg/rules/sg/, which is not where the scanner looks. check finds nothing and reports success.
The failure mode is the dangerous one — not an error, a clean pass on a project whose rules were silently moved out from under it.
Reproduction
Build the CLI, then:
T=$(mktemp -d)
mkdir -p "$T/.taskless/rules/sg/no-eval"
printf 'id: no-eval\nlanguage: javascript\nseverity: error\nmessage: Avoid eval\nrule:\n pattern: eval($$$ARGS)\n' \
> "$T/.taskless/rules/sg/no-eval/no-eval.yml"
echo 'eval("x")' > "$T/a.js"
find "$T/.taskless" -type f # .taskless/rules/sg/no-eval/no-eval.yml
node packages/cli/dist/index.js check -d "$T" --json; echo "exit=$?"
find "$T/.taskless" -type f
Observed:
Migrating to latest .taskless/ schema...
{"success":true,"results":[]}
exit=0
and the tree afterwards:
.taskless/rules/sg/.gitkeep <- emptied
.taskless/sg/rules/sg/no-eval/no-eval.yml <- rule moved here
.taskless/taskless.json
a.js contains eval("x") and the rule has severity: error, so a correct run exits 1 with one finding. Adding {"version":5} to .taskless/taskless.json before the run makes it behave correctly, which confirms the version read is the trigger.
Why it happens
Migration 0004 moves .taskless/rules/ to .taskless/sg/rules/. Applied to a tree that already holds .taskless/rules/sg/..., that produces the doubled .taskless/sg/rules/sg/.... 0005 then creates the current directories but does not reclaim the rules stranded by 0004.
How real is it
A project created by taskless init always has a version file, so this is not the ordinary upgrade path. It is reachable by:
- hand-authored or scripted
.taskless/ trees (this is how it was found — a test fixture built in the current layout)
- a
taskless.json lost to a bad merge or a partial checkout
- anything that copies rule directories into a fresh project without the version file
Worth deciding
The relocation itself is arguably correct behavior for a genuinely pre-versioned tree. The part that is not defensible is reporting success afterwards. Options, roughly in order of cost:
- Refuse to migrate when the tree already matches the current layout but declares no version — ask rather than guess.
- Have 0005 reclaim
.taskless/sg/rules/<engine>/ into .taskless/rules/<engine>/, making the 0004+0005 pair idempotent on an already-current tree.
- Make an empty scan across every engine, on a project that had rule files before migrating, a loud condition rather than a pass.
(3) is the one that generalizes: an empty scan reporting success is the shape that hid this.
Found while restacking the #71→#106 Vale engine stack; the fixture in packages/cli/test/vale-orchestration.test.ts now declares its version, which papers over the symptom in the tests but not the behavior.
A
.taskless/tree that is already in the current rule-directory layout but has notaskless.jsonreads as schema version 0. The migrations then "upgrade" it: the rules are moved to.taskless/sg/rules/sg/, which is not where the scanner looks.checkfinds nothing and reports success.The failure mode is the dangerous one — not an error, a clean pass on a project whose rules were silently moved out from under it.
Reproduction
Build the CLI, then:
Observed:
and the tree afterwards:
a.jscontainseval("x")and the rule hasseverity: error, so a correct run exits 1 with one finding. Adding{"version":5}to.taskless/taskless.jsonbefore the run makes it behave correctly, which confirms the version read is the trigger.Why it happens
Migration 0004 moves
.taskless/rules/to.taskless/sg/rules/. Applied to a tree that already holds.taskless/rules/sg/..., that produces the doubled.taskless/sg/rules/sg/.... 0005 then creates the current directories but does not reclaim the rules stranded by 0004.How real is it
A project created by
taskless initalways has a version file, so this is not the ordinary upgrade path. It is reachable by:.taskless/trees (this is how it was found — a test fixture built in the current layout)taskless.jsonlost to a bad merge or a partial checkoutWorth deciding
The relocation itself is arguably correct behavior for a genuinely pre-versioned tree. The part that is not defensible is reporting success afterwards. Options, roughly in order of cost:
.taskless/sg/rules/<engine>/into.taskless/rules/<engine>/, making the 0004+0005 pair idempotent on an already-current tree.(3) is the one that generalizes: an empty scan reporting success is the shape that hid this.
Found while restacking the #71→#106 Vale engine stack; the fixture in
packages/cli/test/vale-orchestration.test.tsnow declares its version, which papers over the symptom in the tests but not the behavior.