Skip to content

feat(cli): run ast-grep against the committed sg config - #73

Merged
thecodedrift merged 7 commits into
mainfrom
openspec/partition-rules-by-engine
Aug 6, 2026
Merged

feat(cli): run ast-grep against the committed sg config#73
thecodedrift merged 7 commits into
mainfrom
openspec/partition-rules-by-engine

Conversation

@thecodedrift

@thecodedrift thecodedrift commented Jul 28, 2026

Copy link
Copy Markdown
Member

Stack (root → tip):

check and rule verify now run ast-grep against the committed .taskless/sg/sgconfig.yml instead of writing an ephemeral config on every run. resolveSgConfigPath is the single place that decision is made.

generateSgConfig is narrowed rather than deleted. Two rule sets still have no committed config of their own and need one generated: the pre-0004 .taskless/rules/ layout, and the materialized .run/ run set. Both pass their directories explicitly, so those options are load-bearing for exactly those callers — a test asserts the split in both directions, that no config is written when only sg/ rules exist and one is written for the legacy layout.

Also updates the twelve help/*.txt recipes, the CLI README, and the .taskless/README.md written by migration 0001, which now describes the partition rather than listing the old flat rules/ and rule-tests/.

Two smaller things ride along. COMMITTED_SG_CONFIG moves to engines.ts beside the layout it derives from, so filesystem/sgconfig.ts no longer imports the spawn machinery in scan.ts to read one string; typing ENGINE_LAYOUTS with satisfies narrows sg.configFile to its literal and retires a fallback that duplicated the same path. And the cli-check spec delta filed two rewrites of existing requirements under ADDED — the archive refused it, so they move to MODIFIED.

Tip of a merge-down stack#79#80#81 → this. The units are only correct together, so none reaches main alone: this merges down into #81, that into #80, into #79, and #79 makes the single protected merge. The OpenSpec change is archived here, on the tip, exactly once.

Verification worth noting, since the interesting one is a substitute: the full suite is 421 passing, and the old and new check paths were run against an identical seed of this repo's own .taskless/ plus real sources, producing the same two findings — same rule, file, line, column, severity, message — and the same exit code. That baseline is a reproduction of what the old path ran, not the published binary, because pnpm dlx is blocked in this environment.

Fixes OSS-24

Built on top of #79

Migration 0004 moves .taskless/ to an engine-partitioned layout: ast-grep rules go to sg/rules/ and sg/rule-tests/, the runtime tree to runtime/rules/ and runtime/rule-tests/, and vale/ is scaffolded but inert until the Vale engine lands. Files move without their bytes being touched, so runtime rule signatures survive the migration.

Also anchors the sgconfig.yml gitignore pattern to .taskless/. The pattern was unanchored, so it would match a same-named file anywhere in the tree.

This PR carries the OpenSpec change docs for the whole effort, including the spec deltas the later PRs implement.

This is the bottom of a merge-down stack — the units are only correct together, since this migration relocates rules that nothing yet knows how to find. Nothing here reaches main on its own: the PRs above merge down into this branch, and the accumulated result is what merges once. The change directory stays unarchived here by design; the archive gate skips non-tip PRs and the tip archives it.

Refs OSS-24

Built on top of #80

The directory a rule sits in under .taskless/ is its engine. Dispatch reads the path and never parses a rule file to decide who owns it, so adding an engine is a directory, not a discriminator.

planEngineDispatch resolves which engine directories are present; an unrecognized directory is ignored rather than guessed at, so a .taskless/ written by a newer CLI degrades to running the engines this one understands instead of handing a rule to the wrong parser.

Two compatibility behaviors are deliberate and worth reviewing as such:

  • Pre-0004 .taskless/rules/ still runs as ast-grep. An unmigrated checkout, or a producer that keeps naming the old path, executes rather than being silently ignored. Both sources are scanned and findings de-duplicated on the match itself, since a rule id can legitimately exist in only one of them.
  • A delivered rule that names no engine is ast-grep. The delivery API carries no engine discriminator — rules[].content is documented as an ast-grep definition — so absence means ast-grep, permanently, not for a migration window. An unrecognized engine is the opposite case: it means the payload is newer than the CLI, so it throws and writes nothing rather than filing the rule where the wrong parser will read it.

Part of a merge-down stack (see the base branch). Not independently shippable: rules have moved but runtime discovery still reads the old path until the next PR.

Refs OSS-24

Built on top of #81

Runtime discovery now reads .taskless/runtime/rules/, deriving the path from ENGINE_LAYOUTS.runtime rather than repeating the string. Only the search path moves — runtime-check.test.ts and runtime-harness.test.ts seed the engine layout and otherwise pass unmodified, which is the evidence that rule semantics are untouched.

This is what closes the loop on directory-based dispatch: a rule under runtime/rules/ can now actually reach the harness, so both engines are asserted end to end. A new test files a runtime-shaped capture under sg/rules/ and confirms it stays static — the directory decides, not the rule's shape.

Runtime deliberately gets no legacy tolerance. The pre-0004 fallback covers .taskless/rules/ only. Since 0004 moves the runtime tree and check migrates before discovering, a leftover runtime-rules/ is never a second source. A test asserts this, so the asymmetry with ast-grep is visible rather than accidental.

Part of a merge-down stack (see the base branch).

Refs OSS-24

@thecodedrift thecodedrift added the skip-changeset PR intentionally ships no release note (bypasses the changeset requirement) label Jul 28, 2026
@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch 2 times, most recently from 9cf8674 to 655179e Compare July 31, 2026 03:49
@thecodedrift
thecodedrift changed the base branch from main to openspec/partition-engine-3-runtime August 2, 2026 17:35
@thecodedrift thecodedrift removed the skip-changeset PR intentionally ships no release note (bypasses the changeset requirement) label Aug 2, 2026
@thecodedrift thecodedrift changed the title docs(openspec): partition rules by engine directory feat(cli): run ast-grep against the committed sg config Aug 2, 2026
@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch from 3f6ea8f to e773da4 Compare August 2, 2026 18:31
@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch 2 times, most recently from eeed898 to 796423c Compare August 2, 2026 21:47
thecodedrift added a commit that referenced this pull request Aug 2, 2026
On a stack, one changeset describes the whole change and lives on the bottom PR
— the one targeting `main`, which is where the check has to pass. Branches above
inherit that file, so it is never *added* in a child's own diff and every
mid-stack PR failed. Labelling them `skip-changeset` records a deliberate "ships
no release note," which is false, so the workflow bypasses on the base ref
instead.

The `on: branches: [main]` filter does not hold for stacked PRs in practice —
the workflow still runs on a PR based on a feature branch — so the base is
re-checked inside the step rather than trusted. Observed on #80, #81, and #73,
all of which ran and failed the check with a non-main base.
thecodedrift added a commit that referenced this pull request Aug 2, 2026
On a stack, one changeset describes the whole change and lives on the bottom PR
— the one targeting `main`, which is where the check has to pass. Branches above
inherit that file, so it is never *added* in a child's own diff and every
mid-stack PR failed. Labelling them `skip-changeset` records a deliberate "ships
no release note," which is false, so the workflow bypasses on the base ref
instead.

The `on: branches: [main]` filter does not hold for stacked PRs in practice —
the workflow still runs on a PR based on a feature branch — so the base is
re-checked inside the step rather than trusted. Observed on #80, #81, and #73,
all of which ran and failed the check with a non-main base.
@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch from 1993325 to cb835f1 Compare August 3, 2026 07:17
thecodedrift added a commit that referenced this pull request Aug 3, 2026
…laim

Review pushed back on three statements, and two of them were wrong.

`propagate_stack.py` does return you to the branch you started on — normal
completion, rebase conflict, balloon guard, and push failure all check it out
before returning. Only a failure to check out a *child* (exit 6, typically that
branch being held by a worktree) returns early without it. That is the path that
actually bit, so the warning now names it instead of describing the script's
normal behavior.

The `branches: [main]` claim is reframed rather than dropped. It is documented to
filter on the base branch, and the guard should be unreachable — but #73, #80,
and #81 each produced a failing `Require a changeset` check run with an
`openspec/partition-engine-*` base, and a workflow that never triggers produces
no check run at all. The comment now cites that evidence and presents the check
as a defensive guard, without teaching that Actions filters are unreliable in
general.

Also replaces the "structural failures are acceptable" guidance with the opposite
rule: a mid-stack PR that is red is incomplete, and the fix belongs in it. That
advice let this stack sit on 20 failures that turned out to be two real defects.
thecodedrift added a commit that referenced this pull request Aug 3, 2026
…l target

`branches: [main]` reads as "only PRs whose base is main," and filtered that way
when a PR had one base. Under GitHub's stacked-PR support a stacked PR targets
`main` eventually, so the filter matches the eventual target and these workflows
run on mid-stack PRs too — which is why #73, #80, and #81 each produced a failing
`Require a changeset` run with an `openspec/partition-engine-*` base.

That makes the base-ref guard required rather than defensive, and generalizes: a
workflow whose correctness depends on "is this the PR that merges to main" has to
establish that itself. Corrects the OpenSpec archive-check section too, which
told readers a stacked PR would not run that check at all.
thecodedrift added a commit that referenced this pull request Aug 3, 2026
On a stack, one changeset describes the whole change and lives on the bottom PR
— the one targeting `main`, which is where the check has to pass. Branches above
inherit that file, so it is never *added* in a child's own diff and every
mid-stack PR failed. Labelling them `skip-changeset` records a deliberate "ships
no release note," which is false, so the workflow bypasses on the base ref
instead.

The `on: branches: [main]` filter does not hold for stacked PRs in practice —
the workflow still runs on a PR based on a feature branch — so the base is
re-checked inside the step rather than trusted. Observed on #80, #81, and #73,
all of which ran and failed the check with a non-main base.
thecodedrift added a commit that referenced this pull request Aug 3, 2026
…laim

Review pushed back on three statements, and two of them were wrong.

`propagate_stack.py` does return you to the branch you started on — normal
completion, rebase conflict, balloon guard, and push failure all check it out
before returning. Only a failure to check out a *child* (exit 6, typically that
branch being held by a worktree) returns early without it. That is the path that
actually bit, so the warning now names it instead of describing the script's
normal behavior.

The `branches: [main]` claim is reframed rather than dropped. It is documented to
filter on the base branch, and the guard should be unreachable — but #73, #80,
and #81 each produced a failing `Require a changeset` check run with an
`openspec/partition-engine-*` base, and a workflow that never triggers produces
no check run at all. The comment now cites that evidence and presents the check
as a defensive guard, without teaching that Actions filters are unreliable in
general.

Also replaces the "structural failures are acceptable" guidance with the opposite
rule: a mid-stack PR that is red is incomplete, and the fix belongs in it. That
advice let this stack sit on 20 failures that turned out to be two real defects.
thecodedrift added a commit that referenced this pull request Aug 3, 2026
…l target

`branches: [main]` reads as "only PRs whose base is main," and filtered that way
when a PR had one base. Under GitHub's stacked-PR support a stacked PR targets
`main` eventually, so the filter matches the eventual target and these workflows
run on mid-stack PRs too — which is why #73, #80, and #81 each produced a failing
`Require a changeset` run with an `openspec/partition-engine-*` base.

That makes the base-ref guard required rather than defensive, and generalizes: a
workflow whose correctness depends on "is this the PR that merges to main" has to
establish that itself. Corrects the OpenSpec archive-check section too, which
told readers a stacked PR would not run that check at all.
@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch from cb835f1 to a8e2c30 Compare August 3, 2026 18:58
@thecodedrift
thecodedrift marked this pull request as ready for review August 3, 2026 19:20
@claude

claude Bot commented Aug 3, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 4m 2s —— View job


Review: PR #73 — run ast-grep against the committed sg config

  • Gather context (diff, changed files, existing Copilot review comments)
  • Review core logic: sgconfig.ts, scan.ts, check.ts, verify.ts
  • Review test coverage in sg-committed-config.test.ts
  • Review docs/help text and migration changes
  • Review OpenSpec archive/spec deltas
  • Post inline comments for concrete issues
  • Post final top-level assessment

Summary

The core change is well-scoped: resolveSgConfigPath is a clean single point of decision between the committed sg/sgconfig.yml and the ephemeral generated config for the legacy layout, and both check.ts and verify.ts route through it consistently. Migration 0004 already writes sg/sgconfig.yml unconditionally (writeIfAbsent), so the "committed config always exists after ensureTasklessDirectory" invariant this PR relies on holds. Test coverage in sg-committed-config.test.ts is solid — it specifically proves the committed config is read as written (custom ruleDirs entry) rather than reconstructed, and that the legacy path still generates its own config. The .taskless/.gitignore anchoring fix (/sgconfig.yml) is a real, well-explained bug fix — the old unanchored pattern would have silently gitignored the new committed config forever.

Three issues found, none blocking correctness of the shipped behavior:

1. openspec/specs/cli-check/spec.md is internally inconsistent (confirmed, same as the automated review comment). Lines 9–44 ("Check subcommand works without taskless.json" and "Check subcommand warns when no rules exist") still say the CLI reads .taskless/rules/ and "SHALL proceed to generate sgconfig.yml", directly contradicting the updated requirement a few lines down (line 47) that says the committed config is read and "No sgconfig.yml is generated at check time." Since this spec file is the source of truth being archived against, the earlier requirements/scenarios should be updated to the engine-partitioned layout and no-generation behavior, not just the section that was clearly touched for this change.

2. Stale/inaccurate doc comment newly added in packages/cli/src/filesystem/sgconfig.ts:35-38. The comment says generateSgConfig is still needed for "the materialized run set under .run/" (this claim is repeated in the PR description and in the archived tasks.md). I couldn't find any live caller that matches this: .run/runtime-rules (rules/runtime/run-set.ts) is consumed only by the runtime harness's rule discovery, not ast-grep, and the actual ast-grep "narrow" scan for runtime rules (rules/runtime/narrow.ts:89-91) writes its own sgconfig.yml directly via writeFile/stringify, bypassing generateSgConfig entirely. As far as I can tell, generateSgConfig's only real caller today is resolveSgConfigPath's legacy branch. Worth double-checking whether that run-set caller was dropped somewhere along this stack, or whether the comment (and the parallel SgConfigOptions.rulesDirectory doc a few lines above, which makes the same claim and predates this PR) should just be corrected to avoid misleading future readers about why the generic options need to stay.

3. Test flakiness risk in packages/cli/test/sg-committed-config.test.ts:47-61 (confirmed, same as the automated review comment). The runCli helper spawns via the bare string "node" (PATH-dependent) rather than process.execPath, unlike the second describe block's test lower in the same file, which was specifically hardened in the last commit (a0d12ea) to avoid exactly this kind of host-environment dependency. It also types the caught error's code as number, but child_process sets .code to a string (e.g. 'ENOENT') when the process fails to spawn rather than exiting non-zero, so a spawn failure would produce a non-number exitCode and an unhelpful assertion failure rather than a clear one.

Nothing else stood out — the check.ts/verify.ts/engines.ts wiring, the dedupe-by-finding logic, and the twelve help/*.txt + README path updates all look correct and consistent with the new .taskless/<engine>/ layout.

Branch: openspec/partition-engine-3-runtime

@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch from a0d12ea to ab597f1 Compare August 4, 2026 15:05
@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch from ab597f1 to c53f866 Compare August 4, 2026 16:05
@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch from c53f866 to ac4b725 Compare August 6, 2026 01:39
@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch from ac4b725 to 2024781 Compare August 6, 2026 02:23
@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch from 2024781 to bd6d473 Compare August 6, 2026 02:35
@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch from bd6d473 to 2024781 Compare August 6, 2026 04:57
@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch from 2024781 to 14b8fd5 Compare August 6, 2026 05:22
Base automatically changed from openspec/partition-engine-3-runtime to main August 6, 2026 05:49
thecodedrift and others added 5 commits August 5, 2026 22:49
`check` and `rule verify` now point ast-grep at `.taskless/sg/sgconfig.yml` —
committed, authored once, read as-is. Nothing is generated on the check path,
which is what makes the config the source of truth rather than a reconstruction
of whatever happened to be on disk that run: a `ruleDirs` entry a human or the
generator added is honoured, and a test proves it.

`generateSgConfig` is not gone, only narrowed. Rule sets with no committed
config of their own still need one written for them — the pre-migration
`.taskless/rules/` layout and the materialized `.run/` set — so its
`rulesDirectory` / `testDirectory` options remain load-bearing for those.
`resolveSgConfigPath` is the one place that decides which of the two a rule set
gets.

Also covers the missing-binary path end to end: the bundled CLI, run from
outside the workspace with an empty PATH, reports `ast-grep binary not found`
and exits 1 rather than failing obscurely at spawn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ding

Twelve help recipes, the package README, and the `.taskless/README.md` that
migration 0001 writes still told agents and humans to look in
`.taskless/rules/` and `.taskless/runtime-rules/`. They now name the engine
directories, and the scaffolded README explains the partition itself — that the
directory picks the engine and each holds that tool's own native config.

Closes out the change's quality gates: typecheck, lint, and test are clean
(421 passed), and `check` output was confirmed unchanged across the relayout on
this repo's own rule set — same findings, same positions, same exit code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The constant lived in `scan.ts`, which made `filesystem/sgconfig.ts` import the
spawn machinery to read one string. `engines.ts` imports nothing of ours but the
error type, so both layers can reach it there without pulling in the other's
dependencies.

Typing ENGINE_LAYOUTS with `satisfies` narrows `sg.configFile` to its literal,
which retires the `?? "sg/sgconfig.yml"` fallback that duplicated the same path.
Also corrects the cli-check delta, which filed two rewrites of existing
requirements under ADDED. The archive refused it — both headers already exist
in the spec — so they move to MODIFIED, which is what they always were.

The changeset lives at the base of the stack, not here: Require Changeset runs
only on PRs based on `main` and reads the PR's own diff, so the bottom PR is
where it has to be. This branch inherits it.
The test asserted that no ast-grep exists anywhere reachable, but handed the
child a copy of `process.env`. `NODE_PATH` and `HOME` both feed module
resolution — node consults `$HOME/.node_modules` — so on CI the resolver found
a binary, the scan ran, and the assertion failed against the *findings* path:
exit 1 with empty stderr, which is indistinguishable from the error path on the
exit code alone.

The child now gets a minimal environment, and the failure message reports what
went to stdout so the two paths can be told apart.
@thecodedrift
thecodedrift force-pushed the openspec/partition-rules-by-engine branch from 14b8fd5 to 92f513e Compare August 6, 2026 05:49
…erateSgConfig note

Both reviewers flagged the same contradiction: the requirement updated by this
change says the committed `sg/sgconfig.yml` is read and no config is generated
at check time, while requirements a few lines above still described
`.taskless/rules/` and "SHALL proceed to generate sgconfig.yml". The earlier
requirements now describe the engine directory, and keep the pre-migration
layout as the compatibility case it actually is — including a scenario for the
unmigrated rule set, which was previously implied by nothing.

The `generateSgConfig` note was also wrong, in the doc comment and in the
archived tasks.md. It claimed two callers needed it: the legacy layout and the
materialized `.run/` run set. Only the first is real — the runtime narrow writes
its own sgconfig.yml in `rules/runtime/narrow.ts` and never calls
generateSgConfig. That claim came from an answer I accepted on the strength of a
test that only ever exercised the legacy split.

Also hardens `runCli` in the committed-config tests: it shelled out to a
PATH-resolved `node` while a test in the same file already used
`process.execPath`, and it read `code` as a number when a signal death gives
null, which would have reported a killed process as exit 0.
#73 became the bottom of the stack when #81 merged, so the release note has to
be visible in its own diff. Extends the existing note rather than adding a
second entry for a change that ships once.
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @claude[bot] — "Claude finished @thecodedrift's task — Review: PR #73 — run ast-grep against the committed sg config … Three issues found, none blocking correctness"
#73

Both of the issues I can act on are fixed, and the second one was a genuine catch.

The spec inconsistency is corrected — the earlier requirements now describe the sg engine directory and treat the pre-migration layout as the compatibility case, with a scenario for the unmigrated rule set that the spec previously implied nowhere.

The generateSgConfig claim was wrong, and you were right to distrust it. The doc comment, the PR description, and the archived tasks.md all said two callers still needed it: the legacy layout and the materialized .run/ run set. Only the first is real. rules/runtime/narrow.ts writes its own sgconfig.yml via writeFile/stringify and never routes through generateSgConfig, exactly as you found — its only production caller is resolveSgConfigPath.

That claim originated in an answer I accepted because a test asserted the generated-vs-committed split. That test only ever exercised the legacy path, so it could not have caught the .run/ half being false. Both the comment and the archived note now say one caller, and say explicitly that the narrow is not a second one.

— AI Coding Agent

@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @copilot-pull-request-reviewer — "## Pull request overview — Updates the CLI's ast-grep execution to use the committed .taskless/sg/sgconfig.yml as the source of truth…"

Both findings addressed and resolved inline: the cli-check spec is internally consistent again, and runCli now uses process.execPath with a coerced exit code.

— AI Coding Agent

@thecodedrift
thecodedrift merged commit d7abf6a into main Aug 6, 2026
7 checks passed
@thecodedrift
thecodedrift deleted the openspec/partition-rules-by-engine branch August 6, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants