Skip to content
Merged
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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CHANGELOG.md
plugins/corbits-skills/skills/opsh/SKILL.md
plugins/corbits-skills/skills/refactor/SKILL.md
plugins/corbits-skills/skills/scribe/SKILL.md
plugins/corbits-skills/skills/ast-grep/SKILL.md

tmp/
.claude/
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename

- Drop unused `@opentui/keymap`, `@opentui/solid`, and `solid-js`. The interactive TUI is imperative `@opentui/core` only.
- Restore the scribe skill 1:1 with GaaS. ask_operator mapping stays on native-integration. Slash /scribe remains.
- Restore the ast-grep skill 1:1 with GaaS. run_shell mapping stays on native-integration. Slash /ast-grep remains.
- Restore the opsh skill 1:1 with GaaS. Tool/shell mapping stays on native-integration. user-invocable: false stays so it remains use_skill-only.
- Restore the pull-request-review skill 1:1 with GaaS. ask_operator, /review mapping, and GitHub posting stay on native-integration. Slash /pull-request-review remains.
- Restore the refactor skill 1:1 with GaaS. ask_operator mapping stays on native-integration. Slash /refactor remains.
- Ignore GaaS opsh, refactor, and scribe SKILL.md in prettier so table/list alignment stays 1:1.
- Ignore GaaS opsh, refactor, scribe, and ast-grep SKILL.md in prettier so table/list alignment stays 1:1.
- Restore the git-rebase skill body 1:1 with GaaS. Intern execution recipe stays on native-integration. user-invocable: false stays so it remains use_skill-only.
- Restore the linear-issue-workflow skill body 1:1 with GaaS. Claim-first, In Review, and git-worktrees extras stay on native-integration. user-invocable: false stays so it remains use_skill-only.
- Restore the interview skill body 1:1 with GaaS (AskUserQuestion). Operator-ask mapping stays on native-integration. Slash /interview remains.
Expand Down
95 changes: 43 additions & 52 deletions plugins/corbits-skills/skills/ast-grep/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Bulk code refactoring using AST patterns instead of manual read-edi

# ast-grep

Use `ast-grep` (CLI: `sg`) for structural code search and rewriting. Invoke `sg` via `run_shell`. It matches and transforms code using Abstract Syntax Tree patterns rather than text, so it understands code structure and handles formatting, whitespace, and nesting correctly.
Use `ast-grep` (CLI: `sg`) for structural code search and rewriting. It matches and transforms code using Abstract Syntax Tree patterns rather than text, so it understands code structure and handles formatting, whitespace, and nesting correctly.

If you can describe the change as "rename X to Y" or "change all A-shaped code to B-shaped code," use ast-grep — even if you already know some of the locations. Knowing where the definitions are does not mean you know where all the access sites are.

Expand Down Expand Up @@ -36,12 +36,12 @@ Patterns are code snippets in the target language with metavariable placeholders

### Metavariables

| Syntax | Meaning |
| --------- | ------------------------------------------------------ |
| `$NAME` | Matches exactly one AST node, captured as `NAME` |
| `$_` | Matches one node, not captured |
| Syntax | Meaning |
|---|---|
| `$NAME` | Matches exactly one AST node, captured as `NAME` |
| `$_` | Matches one node, not captured |
| `$$$NAME` | Matches zero or more sibling nodes, captured as `NAME` |
| `$$$` | Matches zero or more siblings, not captured |
| `$$$` | Matches zero or more siblings, not captured |

**Same-name constraint:** Two occurrences of the same metavariable in one pattern must match identical text. `foo($X, $X)` matches `foo(a, a)` but not `foo(a, b)`.

Expand Down Expand Up @@ -79,24 +79,22 @@ The `-U` (`--update-all`) flag applies changes to files without prompting. Witho

### Key flags

| Flag | Purpose |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `-p, --pattern` | AST pattern to match |
| `-r, --rewrite` | Replacement template using captured metavariables |
| `-l, --lang` | Target language |
| `-U, --update-all` | Apply rewrites in place |
| `--globs` | Filter files by glob (prefix `!` to exclude) |
| `--json` | Structured JSON output |
| Flag | Purpose |
|---|---|
| `-p, --pattern` | AST pattern to match |
| `-r, --rewrite` | Replacement template using captured metavariables |
| `-l, --lang` | Target language |
| `-U, --update-all` | Apply rewrites in place |
| `--globs` | Filter files by glob (prefix `!` to exclude) |
| `--json` | Structured JSON output |
| `--debug-query=<mode>` | Show AST structure; modes: `pattern` (pattern parse tree), `ast` (named nodes), `cst` (full tree), `sexp` (S-expression). Requires `--lang`. |

### Common inline recipes

**Rename a function call site:**

```bash
sg run -p 'oldName($$$ARGS)' -r 'newName($$$ARGS)' -l js -U src/
```

This pattern only matches `identifier` nodes in call-expression position. It will not catch the name where it appears as a type annotation (`type_identifier`), an interface or object field (`property_identifier`), a destructured binding (`shorthand_property_identifier_pattern`), or an object literal shorthand (`shorthand_property_identifier`). For a name that appears in more than one syntactic position, use the multi-kind rename recipe below.

**Rename an identifier across all syntactic positions (TypeScript):**
Expand Down Expand Up @@ -126,35 +124,29 @@ fix: NewName
This is the default approach for renaming a type, class, interface, or any identifier that may surface in more than just call-site position. The inline `sg run -p` form is the shortcut for call-site-only renames.

**Change an import source:**

```bash
sg run -p 'import $$$ITEMS from "old-package"' -r 'import $$$ITEMS from "new-package"' -l ts -U src/
```

Use `$$$ITEMS` (not `$ITEMS`) because `import type` inserts an extra `type` node as a sibling before the import clause. `$ITEMS` expects exactly one node in that position and fails when two are present.

The symmetric export form does not work inline. `sg run -p 'export $$$ITEMS from "old-package"' -r '...'` fails with "Multiple AST nodes are detected" — the re-export does not parse as a single AST node. For re-export source rewrites, use a YAML rule keyed on `kind: export_statement` with a `has` constraint on the source string, or fall back to manual edits when the file count is small.

**Add an argument to a call:**

```bash
sg run -p 'client.get($URL)' -r 'client.get($URL, { timeout: 5000 })' -l ts -U src/
```

**Wrap a call with an additional outer call:**

```bash
sg run -p 'fetchData($$$ARGS)' -r 'withRetry(() => fetchData($$$ARGS))' -l ts -U src/
```

**Unwrap a wrapper (Rust):**

```bash
sg run -p '$EXPR.unwrap()' -r '$EXPR?' -l rust -U src/
```

**Remove a function call, keep the argument:**

```bash
sg run -p 'deprecated($VALUE)' -r '$VALUE' -l js -U src/
```
Expand All @@ -174,7 +166,6 @@ fix: logger.info($$$ARGS)
```

Run a single rule file:

```bash
sg scan --rule my-rule.yaml src/
sg scan --rule my-rule.yaml -U src/
Expand All @@ -183,7 +174,6 @@ sg scan --rule my-rule.yaml -U src/
### Inline YAML rules

For quick one-offs that need rule features but not a file:

```bash
sg scan --inline-rules '
id: example
Expand Down Expand Up @@ -227,12 +217,12 @@ rule:

Available relational rules:

| Rule | Meaning |
| ---------- | ------------------------------------------- |
| `inside` | Node is a descendant of a matching ancestor |
| `has` | Node has a descendant matching this |
| `follows` | Node is preceded by a matching sibling |
| `precedes` | Node is followed by a matching sibling |
| Rule | Meaning |
|---|---|
| `inside` | Node is a descendant of a matching ancestor |
| `has` | Node has a descendant matching this |
| `follows` | Node is preceded by a matching sibling |
| `precedes` | Node is followed by a matching sibling |

All accept `stopBy` with three valid forms: `neighbor` (only check adjacent — the default when omitted), `end` (traverse all the way to the root), or a rule object (e.g., `stopBy: { kind: function_declaration }` to stop at a specific node type).

Expand Down Expand Up @@ -261,11 +251,11 @@ rule:
pattern: logger.$_($$$)
```

| Combinator | Meaning |
| ---------- | ------------------------------ |
| `all` | All sub-rules must match (AND) |
| `any` | Any sub-rule must match (OR) |
| `not` | Sub-rule must not match (NOT) |
| Combinator | Meaning |
|---|---|
| `all` | All sub-rules must match (AND) |
| `any` | Any sub-rule must match (OR) |
| `not` | Sub-rule must not match (NOT) |

### Disambiguating same-named identifiers

Expand Down Expand Up @@ -323,29 +313,26 @@ fix: $CAMEL_NAME($$$ARGS)

Available transforms:

| Transform | Purpose |
| ----------- | ------------------------------------------------------------------------------------------- |
| `convert` | Change case (`upperCase`, `lowerCase`, `camelCase`, `snakeCase`, `pascalCase`, `kebabCase`) |
| `substring` | Extract a substring by char index |
| `replace` | String find-and-replace within a metavar |
| `rewrite` | Apply sub-rewriters to a metavar (for nested transformations) |
| Transform | Purpose |
|---|---|
| `convert` | Change case (`upperCase`, `lowerCase`, `camelCase`, `snakeCase`, `pascalCase`, `kebabCase`) |
| `substring` | Extract a substring by char index |
| `replace` | String find-and-replace within a metavar |
| `rewrite` | Apply sub-rewriters to a metavar (for nested transformations) |

## Debugging Non-Matching Patterns

When a pattern does not match what you expect:

1. **Inspect your pattern's AST.** Use `--debug-query=pattern` to see how ast-grep parses your pattern:

```bash
sg run --pattern 'your_pattern($X)' --lang js --debug-query=pattern
```

2. **Inspect the source code's AST.** Use the target code itself as the pattern to see its tree structure:

```bash
sg run --pattern 'myFunc(arg1, arg2)' --lang js --debug-query=ast
```

This shows you the node kinds in the source, which tells you what your real pattern needs to match against. Compare the AST of your pattern (step 1) with the AST of the source to find the mismatch.

3. **Common causes of non-matches:**
Expand Down Expand Up @@ -387,7 +374,7 @@ These steps are mandatory, not advisory. Skipping them is the single most common
### After applying rewrites

5. **Format.** ast-grep rewrites can collapse multi-line formatting to single lines. Run the project's formatter (prettier, rustfmt, gofmt, etc.) after applying rewrites.
6. **Check for stragglers.** Use `grep` for the old name across all file types — including comments, strings, docs, and test fixtures. ast-grep only matches code structure; occurrences in prose, JSDoc, string literals, and non-code files will be missed.
6. **Check for stragglers.** Grep for the old name across all file types — including comments, strings, docs, and test fixtures. ast-grep only matches code structure; occurrences in prose, JSDoc, string literals, and non-code files will be missed.
7. **Run the type checker.** ast-grep matches on syntax, not semantics — it cannot guarantee that every reference to a name has been caught across every syntactic context, and it cannot see scope. In typed languages, run the type checker before the test suite. It is the safety net that surfaces both kinds of miss: occurrences of the old name that the pattern did not anticipate (e.g., type annotations missed by a call-site-only rename), and scope collisions where the new name shadows an existing binding. Without a type checker, these gaps are silent and only show up at runtime.
8. **Run the full build.** Run the project's build and test suite to catch anything ast-grep's structural matching could not anticipate.

Expand All @@ -404,14 +391,14 @@ ast-grep handles code; prose requires separate attention. Skipping this pass lea

### Choosing inline vs YAML

| Situation | Use |
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| Call-site-only rename or argument change | `sg run -p ... -r ...` |
| Situation | Use |
|---|---|
| Call-site-only rename or argument change | `sg run -p ... -r ...` |
| Renaming an identifier that may appear in type annotations, fields, or destructuring | YAML rule with `any:` over the relevant node kinds (see "Rename an identifier across all syntactic positions" above) |
| Need to exclude certain matches | YAML rule with `not` or `constraints` |
| Need positional context (inside a function, after an import) | YAML rule with `inside`/`follows`/`precedes` |
| Need case conversion or string manipulation in the replacement | YAML rule with `transform` |
| Applying multiple related transformations | Multiple `sg run` commands in sequence, or multiple YAML rules |
| Need to exclude certain matches | YAML rule with `not` or `constraints` |
| Need positional context (inside a function, after an import) | YAML rule with `inside`/`follows`/`precedes` |
| Need case conversion or string manipulation in the replacement | YAML rule with `transform` |
| Applying multiple related transformations | Multiple `sg run` commands in sequence, or multiple YAML rules |

### Combining with manual edits

Expand All @@ -422,3 +409,7 @@ ast-grep handles the bulk structural transformation. Use manual edits for:
- One-off fixups after a bulk rewrite (e.g., adjusting a special case that the pattern caught incorrectly)

The ideal workflow for a large refactor: ast-grep for the mechanical bulk, manual edits for the exceptions, build verification to confirm everything holds together.

## Acknowledgment

After reviewing this skill, state: "I have reviewed the ast-grep skill."
2 changes: 2 additions & 0 deletions plugins/corbits-skills/skills/native-integration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ When a GaaS skill names a Claude/GaaS tool, use the Corbits equivalent. Do not c

`intent="general"` is not a Corbits spawn. Use a closed director id.

GaaS ast-grep invokes `sg` as a CLI. Corbits extras: run `sg` via `run_shell`. Do not fork the GaaS ast-grep body.

Slash names that differ from GaaS skill ids: `/review` is GaaS `code-review`; `/create-issue` is GaaS `linear-create`. Keep those Corbits names.

GaaS refactor says "ask clarifying questions" / "ask the user". Corbits extras: `ask_operator` (tool mapping above). Do not fork the GaaS refactor body.
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/corbits-skills-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,20 @@ test("scribe skill is 1:1 with GaaS scribe", async () => {
expect(skill).not.toContain("## Acknowledgment");
});

test("ast-grep skill is 1:1 with GaaS ast-grep", async () => {
const skill = await Bun.file(join(pluginRoot, "skills/ast-grep/SKILL.md")).text();
expect(skill).toContain("CLI: `sg`");
expect(skill).toContain("|---|---|");
expect(skill).toContain("sg run");
expect(skill).toContain("sg scan --inline-rules");
expect(skill).toContain("## Acknowledgment");
expect(skill).toContain("I have reviewed the ast-grep skill.");
expect(skill).not.toContain("run_shell");
expect(skill).not.toContain("Invoke `sg` via `run_shell`");
expect(skill).not.toContain(USER_INVOCABLE_FALSE);
expect(skill).not.toContain(DISABLE_MODEL_INVOCATION);
});

test("create-issue is Linear-first without restated MCP tool contracts", async () => {
const skill = await Bun.file(join(pluginRoot, "skills/create-issue/SKILL.md")).text();
expect(skill).toContain("mcp__linear__");
Expand Down Expand Up @@ -422,6 +436,8 @@ test("native-integration maps GaaS tool names and parks Corbits extras", async (
expect(skill).toContain("Do not fork the GaaS pull-request-review body");
expect(skill).toContain("Do not fork the GaaS refactor body");
expect(skill).toContain("Do not fork the GaaS scribe body");
expect(skill).toContain("Do not fork the GaaS ast-grep body");
expect(skill).toContain("run `sg` via `run_shell`");
expect(skill).toContain("prove");
});

Expand Down
Loading