diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ce3ddd96d..447d9cae6 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -468,7 +468,7 @@ Corbits Code **ships a bundled catalog** as the first-party data-only plugin `pl `discoverRepoPlugins` locates `plugins/` next to the source root, at `dist/plugins`, or at `dirname(execPath)/plugins`. It never scans the session cwd for the bundled catalog. -Primary is Skywalker. Bundled skill bodies are **how-to playbooks** (steps, done-definition) — not director personas and not fleet routers. Identity and who-does-what live on director system prompts. Default slashes: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`, `/lexicon`. `/review` classifies the target first, then dispatches a selected fleet; `/pull-request-review` is worktree checkout plus a surface pass, loading `/review` for quality rules only; `/scribe` is how to maintain PRODUCT / ARCHITECTURE / IMPLEMENTATION; `/implement` is the per-commit greybeard → implement → gate → critic loop and does not steal planning from `/plan`; substantial Builder work consumes a counsel / `/plan` plan first; `/plan` authors an eng change plan and does not implement or file tickets; `/create-issue` remains the tracker command — Linear MCP when available, otherwise `ask_operator` for the platform and persists `Preferred issue tracker` in `.corbits/MEMORY.md`; `/lexicon` owns director-prompt drift and size against the agents repo at a pinned commit. The less-is-more quality bar (`idiot-proof`) is baked into builder and critic; it is not a slash. There is no first-party dispatch skill — Skywalker orchestrates natively. Draper and emil are closed directors via `spawn_agent(agent=…)`, not slashes. There is no catch-all worker. The operator types the slash; the primary follows the playbook. +Primary is Skywalker. Bundled skill bodies are **how-to playbooks** (steps, done-definition) — not director personas and not fleet routers. Identity and who-does-what live on director system prompts. Default slashes: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`. `/review` classifies the target first, then dispatches a selected fleet; `/pull-request-review` is worktree checkout plus a surface pass, loading `/review` for quality rules only; `/scribe` is how to maintain PRODUCT / ARCHITECTURE / IMPLEMENTATION; `/implement` is the per-commit greybeard → implement → gate → critic loop and does not steal planning from `/plan`; substantial Builder work consumes a counsel / `/plan` plan first; `/plan` authors an eng change plan and does not implement or file tickets; `/create-issue` remains the tracker command — Linear MCP when available, otherwise `ask_operator` for the platform and persists `Preferred issue tracker` in `.corbits/MEMORY.md` There is no first-party dispatch skill — Skywalker orchestrates natively. Draper and emil are closed directors via `spawn_agent(agent=…)`, not slashes. There is no catch-all worker. The operator types the slash; the primary follows the playbook. #### Discovery and precedence diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md index bbee2f459..a91e0c3f6 100644 --- a/docs/PLUGINS.md +++ b/docs/PLUGINS.md @@ -331,12 +331,10 @@ shape. `resolveSkillBody`), so the model does not auto-suggest background libraries. First-party recipes that are not operator slashes remain listed for `skill_search` / `use_skill` when they only set `user-invocable: false` - (`git-rebase`, `linear-issue-workflow`, `style`, `philosophy`, - `native-integration`, `typescript`, `ponytail`, `opsh`). Background libs - such as `git-worktrees` set both flags. Bodies that stay out of discovery - such as `idiot-proof` and `native-runtime` also set both flags (baked into - workers; not a slash and not listed by `skill_search`/`discoverSkills`, but - still loadable by explicit `use_skill`/`resolveSkillBody` name). The slash + (`linear-issue-workflow`, `style`, `philosophy`, `native-integration`, + `typescript`). Background libs such as `git-worktrees` set both flags: not a + slash and not listed by `skill_search`/`discoverSkills`, but still loadable + by explicit `use_skill`/`resolveSkillBody` name. The slash command is a direct user entry point on top. - **First-party catalog.** `plugins/corbits-skills/` (id `corbits-skills`, kind `command`, `defaultEnabled: true`) is the bundled skill catalog. Origin diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 6ae4b42af..06302e2ce 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -107,7 +107,7 @@ the file path and parse details. The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (models-only picker for connected accounts; **Alt+A** or `/connect` adds a provider), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/compact` (fold conversation context now, optional trailing instructions to the summarizer; does not wait for the 60% occupancy governor; idle success shows the fold and does not start a new turn), `/mcp` (enable, disable, or remove servers), `/handoff [optional instructions]` (folds context through the shared operator pipeline, then immediately starts the next turn with the instructions as the inbound content — default copy when omitted; unlike `/compact`, which stops after the fold, handoff always re-infers, so the operator can pivot goals without `/clear`; a handoff issued mid-tool-batch queues behind the in-flight batch and whichever boundary fires first runs the single fold), and `/yolo` (`/yolo [on|off|toggle]`, bare `/yolo` toggles), plus a `/` command per available workflow. `/yolo` persists skip-permissions to the active settings file. That file is the user-global `~/.corbits/settings.json` by default, making the setting machine-wide; explicit `--config ` selects a different active file, and `/yolo` writes that file. An ordinary TUI launch without the same `--config` returns to the user-global source and does not modify the custom file. `--dangerously-skip-permissions` and `--yolo` are process-only aliases. Secret-guard and authz still apply. When a session starts with its active persisted setting already on, the TUI and `corbits exec` warn that permission prompts are disabled by saved settings at the active settings path and direct the operator to edit that file to re-enable them. Plugins can register additional commands. -**Default skills** exist out of the gate as first-party slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`, `/lexicon`. Each one is a how-to playbook — the slash sends the skill body to the primary, which follows the steps. Skills do not assign identity or route the fleet; that stays on director system prompts. `/review` classifies the target first, then dispatches a selected fleet; `/pull-request-review` is worktree checkout plus a surface pass, loading `/review` for quality rules only; `/scribe` is how to maintain PRODUCT / ARCHITECTURE / IMPLEMENTATION; `/implement` is the per-commit review/build/critique loop — it does not steal planning from `/plan`. Substantial Builder work consumes a counsel / `/plan` plan first; tiny parent-DIY stays plan-optional. `/plan` authors an eng change plan (files, AC, non-goals, risks, ordered steps) and does not implement. `/create-issue` remains the tracker command: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). `/lexicon` owns director-prompt drift and size against the agents repo at a pinned commit. There is no first-party dispatch skill — Skywalker orchestrates natively. `git-rebase`, `linear-issue-workflow`, `style`, `philosophy`, `native-integration`, `typescript`, `ponytail`, and `opsh` stay `use_skill` only (`user-invocable: false`). Bake-only bars such as `idiot-proof` and `native-runtime` are not slashes and are not listed for `use_skill`. Draper and emil are not slashes; they remain closed directors via `spawn_agent(agent=…)`. There is no catch-all worker. Slash names are also available to the model via `skill_search` (descriptions) then `use_skill` (body). Disable the catalog in `/plugins` (`corbits-skills`) if you want them gone. +**Default skills** exist out of the gate as first-party slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`. Each one is a how-to playbook — the slash sends the skill body to the primary, which follows the steps. Skills do not assign identity or route the fleet; that stays on director system prompts. `/review` classifies the target first, then dispatches a selected fleet; `/pull-request-review` is worktree checkout plus a surface pass, loading `/review` for quality rules only; `/scribe` is how to maintain PRODUCT / ARCHITECTURE / IMPLEMENTATION; `/implement` is the per-commit review/build/critique loop — it does not steal planning from `/plan`. Substantial Builder work consumes a counsel / `/plan` plan first; tiny parent-DIY stays plan-optional. `/plan` authors an eng change plan (files, AC, non-goals, risks, ordered steps) and does not implement. `/create-issue` remains the tracker command: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). There is no first-party dispatch skill — Skywalker orchestrates natively. `linear-issue-workflow`, `style`, `philosophy`, `native-integration`, and `typescript` stay `use_skill` only (`user-invocable: false`); `git-worktrees` is a background library that is not listed for `use_skill`. Draper and emil are not slashes; they remain closed directors via `spawn_agent(agent=…)`. There is no catch-all worker. Slash names are also available to the model via `skill_search` (descriptions) then `use_skill` (body). Disable the catalog in `/plugins` (`corbits-skills`) if you want them gone. Providers are **models-first**: there is no standalone `/login` command. `/model` opens a **models-only list** (Recent, Favorites, then connected provider/model rows) — type-to-filter owns printable keys, so Connect is never a bare letter. **Alt+A** or `/connect` opens a dedicated add-provider selector over every first-class kind (OpenAI dual-path ChatGPT OAuth or API key, xAI, OpenCode Zen, Anthropic, Google, OpenCode Go, Z.AI Coding Plan, Ollama, Custom), each annotated with its live account count and never filtered out for “already connected.” **Alt+F** toggles favorite on the highlighted model. **Alt+D** persists the highlighted pair as the default without switching the live session. Advanced provider drill-down (edit/delete/tiers) stays on the advanced surface, not a bare printable key while the model list is filtering. OAuth providers open their existing browser login with a named account step so multiple accounts per kind coexist (`codex/work`, …). API-key providers use the same named-instance step before the key (auth-only form: instance name + key + fixed catalog base URL), so personal and team keys land as distinct catalog rows (`openai/default`, `anthropic/work`, …); reusing a name re-keys that instance after confirm. Custom remains a free-form single endpoint (full manual form). Successful connect refreshes the catalog and reopens the model list focused on the new account’s default model. OpenCode Go lists models from the live `/zen/go/v1/models` catalog (packaged seed on fetch failure), routes each by its protocol metadata (chat completions, OpenAI responses, or Anthropic messages) and can show subscription usage in the status bar when active (rolling 5h / weekly / monthly windows when the usage API responds; omitted on auth or network failure). When Go returns a quota or rate-limit error — including some HTTP 400 responses that carry limit payloads — Corbits classifies them so quota aborts cleanly and short provider rate limits remain retryable. On a free-tier or subscription quota hit, wait for the window to reset or use OpenCode Zen free models. diff --git a/docs/TELEMETRY.md b/docs/TELEMETRY.md index fae9806d1..1a21cf8bb 100644 --- a/docs/TELEMETRY.md +++ b/docs/TELEMETRY.md @@ -80,21 +80,12 @@ with it. `agent_name` on `subagent_*` is the same pattern: first-party director ids from `DIRECTOR_IDS` (and the legacy `worker` alias) are reported by id; project-defined or marketplace profile ids become `custom`. `skill_used` carries `skill_name`: a first-party skill name reportable by -name from the closed `corbits-skills` allowlist (`ast-grep`, `create-issue`, -`git-rebase`, `git-worktrees`, `implement`, `interview`, `lexicon`, -`linear-issue-workflow`, `opsh`, `philosophy`, `plan`, -`pull-request-review`, `refactor`, `review`, `scribe`, `style`, -`typescript`), or `custom` for anything else. `user-invocable: false` opts a -skill out of slash synthesis, not out of name reporting: eleven bundled -skills carry the flag — eight stay listed and loadable (`git-rebase`, -`linear-issue-workflow`, `opsh`, `philosophy`, `style`, `typescript`, -`native-integration`, `ponytail`), `git-worktrees` resolves by explicit name -only, and two stay hidden from discovery (`idiot-proof`, `native-runtime`): -still loadable by explicit name, but every load reports `custom`. Of -the eleven, seven are reported by name and four stay `custom` (pinned; -conservative under-reporting, never a leak). Unknown, -project-local, and plugin-authored skill names are never transmitted — -`skill_name` is the only identifying-adjacent property the event can carry. +name from the closed `corbits-skills` allowlist in `src/telemetry/classify.ts` +(the slash workflows plus `git-worktrees`, `philosophy`, `style`, +`typescript`), or `custom` for anything else, including bundled background +skills outside the list. Unknown, project-local, and plugin-authored skill +names are never transmitted; `skill_name` is the only identifying-adjacent +property the event can carry. `plugin_loaded` goes further: there is no first-party list of plugins to match against, so it carries only `origin`, the discovery tier (`repo`, `user`, `project`, `path`). Enabled telemetry reports the same plugin diff --git a/plugins/corbits-skills/manifest.json b/plugins/corbits-skills/manifest.json index 7063ce6d3..6c4376a22 100644 --- a/plugins/corbits-skills/manifest.json +++ b/plugins/corbits-skills/manifest.json @@ -3,5 +3,5 @@ "name": "Corbits Skills", "kind": "command", "defaultEnabled": true, - "description": "Default operator skills and slash commands (implement, refactor, review, pull-request-review, create-issue, scribe, interview, ast-grep, plan, lexicon)." + "description": "Default operator skills and slash commands (implement, refactor, review, pull-request-review, create-issue, scribe, interview, plan)." } diff --git a/plugins/corbits-skills/skills/ast-grep/SKILL.md b/plugins/corbits-skills/skills/ast-grep/SKILL.md deleted file mode 100644 index 8dbe8d7b9..000000000 --- a/plugins/corbits-skills/skills/ast-grep/SKILL.md +++ /dev/null @@ -1,415 +0,0 @@ ---- -name: ast-grep -description: Bulk refactors with AST patterns. Load for renames and API migrations across many files. ---- - -# ast-grep - -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. - -Prefer ast-grep over manual read-edit-write cycles when: - -- Renaming functions, methods, types, or variables across files -- Changing function signatures (adding/removing/reordering arguments) -- Migrating API calls from one shape to another -- Updating import paths or restructuring imports -- Applying the same structural transformation to many call sites -- Any change where the pattern is "find all X shaped like this, replace with Y" - -Do not use ast-grep when: - -- The change is truly isolated — a single site with no callers, no consumers, no matching pattern elsewhere in the codebase (e.g., fixing a typo in one string literal) -- The transformation depends on runtime semantics ast-grep cannot see (e.g., type resolution across modules) -- The target language is not supported - -## Supported Languages - -JavaScript, TypeScript, TSX, JSX, Python, Rust, Go, Java, C, C++, C#, Kotlin, Swift, Scala, Ruby, PHP, Lua, Bash, Dart, Elixir, Haskell, HTML, CSS, JSON, YAML. - -Not supported: SCSS, Vue, Svelte, OCaml, SQL, Dockerfile, XML, TOML. - -## Pattern Syntax - -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 | -| `$$$NAME` | Matches zero or more sibling nodes, captured as `NAME` | -| `$$$` | 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)`. - -### Pattern Rules - -1. A pattern must parse as a single AST node (or sibling sequence with `$$$`). -2. Multi-statement patterns are not supported. `x = 1; y = 2` will error. Use relational rules (`inside`, `has`, `follows`) for multi-node relationships. -3. Optional syntax (like `extends` on a class) must appear in the pattern to match nodes that have it. Use `$$$` to absorb optional parts: `class $NAME $$$REST { $$$BODY }` matches both `class Foo {}` and `class Foo extends Bar {}`. -4. Code inside comments is not matched by patterns (a commented-out `console.log(...)` will not match pattern `console.log($$$)`). However, comment nodes themselves can be targeted using `kind: comment` in YAML rules. -5. Whitespace and formatting differences are ignored — the match is structural, not textual. - -## Inline Patterns with `sg run` - -Use `sg run` for quick, one-off search and rewrite operations. This is the default approach — reach for YAML rules only when you need constraints, transforms, or relational matching. - -### Search - -```bash -sg run --pattern 'console.log($$$ARGS)' --lang js src/ -``` - -### Search and rewrite (preview diff) - -```bash -sg run --pattern 'console.log($$$ARGS)' --rewrite 'logger.info($$$ARGS)' --lang js src/ -``` - -### Apply rewrites in place - -```bash -sg run --pattern 'console.log($$$ARGS)' --rewrite 'logger.info($$$ARGS)' --lang js -U src/ -``` - -The `-U` (`--update-all`) flag applies changes to files without prompting. Without it, ast-grep prints a diff preview. - -### 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 | -| `--debug-query=` | 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):** - -In TypeScript the same bare name parses as a different AST node kind depending on where it sits — `identifier` in expressions, `type_identifier` in type annotations, `property_identifier` in interface or object fields, `shorthand_property_identifier_pattern` in destructured bindings, and `shorthand_property_identifier` in object literal shorthand. A bare inline rewrite (`sg run -p 'OldName' -r 'NewName'`) only matches `identifier` and silently misses the rest. Use a YAML rule that enumerates the node kinds: - -```bash -sg scan --inline-rules ' -id: rename-identifier -language: typescript -rule: - any: - - kind: identifier - regex: "^OldName$" - - kind: type_identifier - regex: "^OldName$" - - kind: property_identifier - regex: "^OldName$" - - kind: shorthand_property_identifier_pattern - regex: "^OldName$" - - kind: shorthand_property_identifier - regex: "^OldName$" -fix: NewName -' -U src/ -``` - -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/ -``` - -## YAML Rules - -Use YAML rules when you need constraints, transforms, relational matching, or complex multi-part logic that inline patterns cannot express. - -### Basic rule structure - -```yaml -id: replace-console-log -language: javascript -rule: - pattern: console.log($$$ARGS) -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/ -``` - -### Inline YAML rules - -For quick one-offs that need rule features but not a file: -```bash -sg scan --inline-rules ' -id: example -language: javascript -rule: - pattern: console.log($$$ARGS) -fix: logger.info($$$ARGS) -' src/ -``` - -### Constraints - -Filter metavariable matches by node kind or regex: - -```yaml -id: ban-untyped-empty-objects -language: typescript -rule: - pattern: "const $NAME: {} = $VALUE" -constraints: - NAME: - regex: "^[a-z]" -message: "Avoid empty object type {}; use Record instead" -``` - -Constraints narrow which matches a rule reports or rewrites. Use `regex` to filter by the matched text content, or `kind` to filter by AST node type. - -### Relational rules - -Match based on the structural position of nodes relative to each other: - -```yaml -rule: - pattern: console.log($$$) - inside: - kind: function_declaration - stopBy: end -``` - -**`stopBy: end` is critical.** Without it, `inside` only checks the immediate parent node. With `stopBy: end`, it traverses all ancestors up to the file root. This matters because even simple nesting has multiple intermediate AST nodes between a matched node and its logical container (e.g., `call_expression` → `expression_statement` → `statement_block` → `function_declaration`). Without `stopBy: end`, matching `console.log($$$)` inside a `function_declaration` fails even with trivial one-level nesting. Always add `stopBy: end` unless you specifically want immediate-parent-only matching. - -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 | - -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). - -### Matching by node kind - -Use `kind` to match all AST nodes of a given type regardless of content: - -```yaml -id: find-arrow-functions -language: typescript -rule: - kind: arrow_function -``` - -This matches every arrow function in the codebase. Combine with `has`, `inside`, or `constraints` to narrow further. Use `--debug-query=ast` on a representative code snippet to discover the node kind names for your target language. - -### Combinators - -Compose rules with boolean logic: - -```yaml -rule: - all: - - pattern: $FUNC($$$ARGS) - - not: - 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) | - -### Disambiguating same-named identifiers - -When the same identifier appears in multiple semantic contexts and you only want to match some of them, use these techniques: - -**By sibling content.** Match only when a sibling property has a specific value (e.g., only match `message` inside objects that also contain `type: "inference.done"`): - -```yaml -rule: - kind: pair - has: - field: key - kind: property_identifier - regex: "^message$" - inside: - kind: object - has: - kind: pair - has: - field: value - regex: "inference\\.done" - stopBy: neighbor -``` - -**By descendant access chain.** Exclude matches that are part of a longer property chain (e.g., match `$X.data.message` but not `$X.data.message.headers`): - -```yaml -rule: - pattern: $X.data.message - not: - inside: - pattern: $X.data.message.headers - stopBy: end -fix: $X.data.turn -``` - -**By node kind.** Distinguish type-level vs value-level occurrences. Use `kind: property_signature` for interface/type definitions and `kind: pair` for object literal expressions — they share the same surface syntax but are different AST nodes. - -### Transforms - -Derive new metavariables for use in `fix`: - -```yaml -id: snake-to-camel -language: typescript -rule: - pattern: $FUNC($$$ARGS) -transform: - CAMEL_NAME: - convert: - source: $FUNC - toCase: camelCase -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) | - -## 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:** - - **Ambiguous parse:** The pattern is valid syntax but parsed as something you did not intend. ast-grep picks the first valid AST interpretation, which may not be what you meant. The most common case: `key: value` patterns (e.g., `message: AssistantTurn`) parse as a **labeled statement** (`message:` label + `AssistantTurn` expression), not a property signature or object pair. The pattern silently matches nothing (exit 1, no error). Use `--debug-query=pattern` — if the output shows `labeled_statement`, you have hit this. Fix by wrapping in braces for object context (`{ message: $VAL }`) or using a YAML rule with `kind: property_signature` or `kind: pair` to match the intended node type. - - Optional syntax missing from pattern (add `$$$` to absorb it) - - Multi-statement pattern (not supported — use relational rules) - - Wrong node granularity (pattern matches expression but code is in a statement context, or vice versa) - - Language alias mismatch (`ts` vs `typescript` vs `tsx` — use the right one for the file type) - -## Exit Codes - -`sg run`: exit `0` means matches were found, exit `1` means no matches — but this is not reliable when the pattern contains an ERROR node, which also exits 0 with zero matches and a warning on stderr. Always check stderr for warnings. Other exit codes indicate errors (e.g., 3 when `--stdin` is used without `--lang`, 2 when `--debug-query` is used without `--lang`, 8 for patterns that fail to parse such as multi-statement patterns). Do not treat all non-zero exits as "no matches." - -`sg scan`: exit `0` means no error-severity findings, exit `1` means at least one `severity: error` finding. All other severities (`warning`, `info`, `hint`) exit `0`. - -## Language Detection - -When running on file paths, ast-grep auto-detects the language from file extensions. The `--lang` flag is required when using `--stdin` (no file extension to infer from) or when using `--debug-query` (always requires explicit language). You can omit `--lang` for normal search and rewrite operations targeting directories or specific files. - -## Workflow Guidance - -### Before writing any pattern - -These steps are mandatory, not advisory. Skipping them is the single most common cause of wasted work with ast-grep. - -1. **Check known pitfalls.** Review the "Common causes of non-matches" list in the Debugging section before writing your pattern. The labeled statement trap (`key: value` parsing as a label, not a property) catches people repeatedly even after they know about it. -2. **Inspect your pattern's parse.** Run `--debug-query=pattern` on every new pattern before using it. If the output shows an unexpected node type (e.g., `labeled_statement` when you expected `pair`), fix the pattern before proceeding. -3. **Inspect the source code's AST.** Run `--debug-query=ast` on a representative snippet of the code you want to match. Identifiers parse as different node kinds depending on context — `property_identifier`, `shorthand_property_identifier`, `type_identifier`, etc. — and a pattern written for the wrong kind will silently match nothing. Discover the actual node kinds before writing the pattern. - -### Before applying rewrites - -**Never apply blind.** Do not pass `-U` on the first run. A blind rewrite overwrites file content in place with no recovery path short of `git checkout` — and if the fix template was wrong, the revert may not restore the original code (e.g., a hardcoded replacement loses the distinct values that were at each site). Follow these steps in order: - -1. **Preview first.** Run without `-U` to see the diff. -2. **Check match count.** Pipe through `--json | jq length` or review the diff output. If the count is higher than expected, inspect the extra matches before applying. If it is lower, you are missing sites. -3. **Scan the whole repo.** The pattern itself provides selectivity — do not manually restrict to specific directories, as this leads to missed rename sites that only surface as build failures. Use `--globs` only to exclude known false positives (e.g., `--globs '!**/vendor/**'` or `--globs '!**/generated/**'`). Always use `**/` in exclusion patterns to match at any depth. -4. **Apply.** Only after previewing and confirming the match set, run with `-U`. - -### 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.** 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. - -### Terminology migrations - -When a rename is a terminology migration (not just a code rename), the code rewrite is only part of the job. After ast-grep handles the structural code changes, do a deliberate manual pass over: - -- Comments and JSDoc that reference the old terminology -- Documentation files (README, guides, API docs) -- String literals (error messages, log messages, descriptions) -- Schema names and persisted keys that should reflect the new terminology - -ast-grep handles code; prose requires separate attention. Skipping this pass leaves the codebase in an inconsistent state where code says one thing and documentation says another. - -### Choosing inline vs YAML - -| 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 | - -### Combining with manual edits - -ast-grep handles the bulk structural transformation. Use manual edits for: - -- New code that has no existing pattern to transform from -- Changes that require understanding type relationships across files -- 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." diff --git a/plugins/corbits-skills/skills/git-rebase/SKILL.md b/plugins/corbits-skills/skills/git-rebase/SKILL.md deleted file mode 100644 index 03126c5da..000000000 --- a/plugins/corbits-skills/skills/git-rebase/SKILL.md +++ /dev/null @@ -1,725 +0,0 @@ ---- -name: git-rebase -description: Reshape branch history non-interactively with rebase, fixup, squash, drop, split, or reword. -user-invocable: false ---- - -# git-rebase - -Use this skill when you need to rewrite a branch's commit history — squashing -fixups, dropping wrong-turn commits, splitting bundled changes, rewording -messages — in an environment that does not support interactive prompts. - -`git rebase -i` is normally driven through an interactive editor. This skill -shows how to drive every editor invocation programmatically so the rebase -runs to completion without a human at the keyboard. The same techniques are -useful when you do have an editor: scripted rebases are reproducible, -re-runnable, and self-documenting in a way that vim-driven ones never are. - -## Assumptions - -- Git ≥ 2.18. Earlier versions handle `--autosquash` interaction with - `GIT_SEQUENCE_EDITOR=true` differently and do not support some of the - edit-todo behaviors used below. Git ≥ 2.38 adds `--update-refs`, - which the workflow uses when stacked branches are present (see - "Stacked branches" below). -- A POSIX shell (`/bin/sh`) is available and used as the editor-script - shebang. Every editor script in this skill starts with `set -eu` so - an intermediate failure (failed `sed`, missing file, undefined - variable) aborts the script with a non-zero exit — letting the rebase - fail loudly rather than silently succeeding with a no-op edit. - Examples write helper scripts to `/tmp`; substitute a repo-local - `tmp/` if your project bans `/tmp`. -- `sed -i.bak ` is the portable in-place form across BSD and GNU - sed; it creates `.bak`, which the script then removes with - `rm -f .bak`. Bare `sed -i` is GNU-only; bare `sed -i ''` is - BSD-only. Don't mix them. -- If the repository enforces signed commits (`commit.gpgsign=true`, - `gpg.format=ssh`, or similar), rebase strips signatures from every - replayed commit unless you pass `-S` / `--gpg-sign` (or set - `rebase.gpgSign=true`). Re-sign explicitly when the project's policy - requires it; an unsigned commit that sneaks through a rebase is - invisible until the next push fails. - -## When to use this skill - -Reach for it when development pace produced messy history that needs to be -made coherent before the branch is pushed for review: - -- A flip-flop: commit X added behavior, commit Y reverted it, commit Z added - it back. The net change is what Z does, but the history reads as confusion. -- A commit with a process-talk message ("Address review findings", "Fix bug - from last commit") that should describe the behavior change instead. -- A commit that bundles unrelated changes that belong in different earlier - commits. -- A drive-by lint fix smuggled into an unrelated feature commit. -- A bug fix discovered during integration testing that belongs in the commit - that introduced the bug. - -Do **not** reach for it when: - -- The commit you want to fix is HEAD itself. Use `git commit --amend` - (with `-F /path/to/message.txt` for a pre-written message, `--no-edit` - to keep the existing one). No rebase needed. -- The branch is already pushed and other people are basing work on it. -- The history is already coherent and you are only chasing aesthetic - perfection. Style and philosophy say "commits should read like a story" — - not "every commit must be perfect." -- The base branch is unstable and your branch will need to rebase repeatedly. - Wait until things settle, or enable `git rerere` (see "Repeated rebases" - below). -- The job is repo-wide history surgery — removing a secret from every - commit, splitting a monorepo, rewriting author identities, mass-rewriting - hundreds of commits. Reach for [`git filter-repo`](https://github.com/newren/git-filter-repo) - instead. Scripting `GIT_SEQUENCE_EDITOR` for that many commits is a path - of suffering. - -## The non-interactive insight - -Git invokes an editor at several points during a rebase. The two that -matter for scripting are: - -| Editor invocation | Env var | What it edits | -| ------------------------- | --------------------- | --------------------------------------------------------------------------------------------------- | -| Rebase plan ("todo list") | `GIT_SEQUENCE_EDITOR` | The list of `pick`/`reword`/`edit`/`fixup`/`drop`/`squash` lines | -| Commit message editing | `GIT_EDITOR` | A single commit message file (used for `reword`, `squash` combined messages, and amend-during-edit) | - -`GIT_EDITOR` also fires for `git rebase --edit-todo` and for conflict-file -editing when configured. The risk of using `GIT_EDITOR="cp ..."` is exactly -that it fires for _every_ editor invocation in the rebase, not just the -one you have in mind. See "Pattern 3" below for safer dispatchers. - -`GIT_SEQUENCE_EDITOR` is the killer feature. Almost everything else flows -from being able to script the rebase plan. - -## Safety first: branch your way back - -Before any history surgery, create a backup branch at the current HEAD so a -hard reset returns you to a known-good state if anything goes wrong: - -```bash -git branch backup-$(git rev-parse --abbrev-ref HEAD)-pre-rebase -``` - -(Avoid `backup/` if your branch names contain `/` — git refs cannot -be both a directory and a file. A flat `backup-` namespace is -safer.) - -If you need to restore and a rebase is in progress, abort it first: - -```bash -git rebase --abort 2>/dev/null # safe if no rebase is in progress -git reset --hard backup--pre-rebase -``` - -**`git rebase --abort` is your deliberate bail-out.** If you find -yourself in a tangled `--continue`/`--skip` loop and have lost the -thread of what each conflict means, abort and start over from the -backup branch. Re-running a planned rebase from a clean state is almost -always faster than rescuing one mid-flight. - -**If you skipped the backup branch, the reflog is your fallback.** Every -update to a branch ref is recorded: - -```bash -git reflog show # find the pre-rebase entry -git reset --hard @{} # reset to that entry -``` - -The reflog entries expire (default 90 days for reachable, 30 for -unreachable), so this is a recovery path of last resort — the backup -branch is the right primary mechanism. - -Validate the rewritten branch against the backup at the end. Use the -two-argument form of `git diff` (not the `A..B` range form): `git diff` -operates on trees, not commit ranges. - -```bash -git diff backup--pre-rebase HEAD --stat -``` - -An empty diff is the "you did not lose any content, only reshaped history" -proof. Run this after every meaningful rebase step. - -**Detecting a zombie rebase.** Some safety configs — notably -`rebase.missingCommitsCheck=error` — pause a rebase rather than -aborting it when the rebase plan is rejected, leaving -`.git/rebase-merge/` in place. The editor script exits 0 and the outer -`git rebase` command exits 0 too, so a naive caller sees apparent -success. After every rebase, check explicitly: - -```bash -if [ -d .git/rebase-merge ] || [ -d .git/rebase-apply ]; then - echo "rebase in progress — investigate before proceeding" >&2 - exit 1 -fi -``` - -Treat a leftover rebase dir as a failure regardless of what git's exit -code said. - -## Patterns - -### Pattern 1: Drop a commit cleanly - -The cleanest non-interactive way to drop a single commit is `git rebase ---onto`. No editor needed. - -```bash -# Drop commit BAD_SHA, replay everything after it onto BAD_SHA's parent. -# Pass the branch name (not HEAD) so the branch ref moves on success. -git rebase --onto BAD_SHA^ BAD_SHA -``` - -If `BAD_SHA` is the root commit it has no parent, and `BAD_SHA^` fails to -resolve. Use `--root` instead and reshape the rebase to start from a known -empty tree, or first create a parent for it (rare; consult `git rebase ---root` documentation if you hit this). - -If the dropped commit had a counterpart in a later commit (e.g. you added -something in X and reverted it in Y), expect a conflict when the later -commit tries to apply. Resolve by editing the conflict markers out of the -file directly with whatever editing tool is available, then: - -```bash -git add -git rebase --continue -``` - -**Detached-HEAD caveats:** - -- During the rebase itself, HEAD is detached. If conflicts arise mid-way, - you are resolving them on a detached HEAD; that's normal and expected. -- Do _not_ `git checkout` away from a mid-rebase detached HEAD as a way - to "escape" an unexpected state. Doing so abandons the in-flight - rebase work — only the reflog can recover what was committed, and - only within its expiry window. If you want out, `git rebase --abort` - first. -- On _successful completion_, passing `` causes git to move - the branch ref forward. Passing `HEAD` does not — you finish on a - detached HEAD and have to re-attach manually with `git checkout -B -my-branch HEAD`. - -### Pattern 2: Script the rebase todo list - -Write a tiny shell script that takes the todo file path as `$1` and rewrites -it in place. Point `GIT_SEQUENCE_EDITOR` at it. - -```bash -cat > /tmp/rebase-editor.sh <<'EOF' -#!/bin/sh -set -eu -todo=$1 -# Substitute the real abbreviated SHA before running — a no-op sed pattern -# produces a successful no-op rebase that looks like it worked. -sed -i.bak 's/^pick abc1234/edit abc1234/' "$todo" -rm -f "$todo.bak" -# Show the rewritten plan so a silent no-op is obvious in the output. -echo "--- rewritten rebase plan ---" >&2 -cat "$todo" >&2 -EOF -chmod +x /tmp/rebase-editor.sh -GIT_SEQUENCE_EDITOR=/tmp/rebase-editor.sh git rebase -i origin/main -``` - -The `echo` + `cat` to stderr is cheap insurance: any time you don't see -the expected change in the printed plan, abort and inspect. - -For more complex rewrites, replace the todo wholesale. Note that -`rebase.missingCommitsCheck=error` (a common safety setting) does _not_ -reject a wholesale-replace plan that omits commits — it pauses the -rebase mid-flight with `No commands done` and leaves -`.git/rebase-merge/` in place. The editor script exits 0 and so does -the outer `git rebase` command, so a naive caller sees apparent -success. Preserve every line you don't want to drop, and explicitly use -`drop` rather than just removing lines, so the check is satisfied and -the rebase actually runs to completion. Combine with the zombie-rebase -detection from "Safety first" above to catch any case where a paused -rebase slips past. - -```bash -cat > /tmp/rebase-editor.sh <<'EOF' -#!/bin/sh -set -eu -todo=$1 -cat > "$todo" <&2 -cat "$todo" >&2 -EOF -chmod +x /tmp/rebase-editor.sh -GIT_SEQUENCE_EDITOR=/tmp/rebase-editor.sh git rebase -i origin/main -``` - -The script runs once when git opens the editor for the plan. Its job is to -leave the todo file in the state you want git to execute. - -### Pattern 3: Provide pre-written commit messages - -For `reword` actions (and for `squash` actions that combine messages), git -invokes `GIT_EDITOR` on a temp file containing the current message, -expecting you to edit it. Replace the editor with a `cp` that overwrites -the file with your pre-written message: - -```bash -cat > /tmp/new-message.txt <<'EOF' -A descriptive subject line under 72 characters - -A body that explains why the change was made, wrapped to 72 columns. -Each paragraph is a complete thought. -EOF -GIT_EDITOR="cp /tmp/new-message.txt" git rebase --continue -``` - -`cp` takes two arguments: source and destination. Git passes the message -file path as the editor's only argument, which becomes `cp`'s -destination. - -`cp` has two surprises worth knowing about: it follows symlinks -(overwriting the target rather than the link), and it changes the -destination's inode and mtime — which can confuse hooks that fingerprint -the file. For paranoid use, prefer a form that truncates in place: - -```bash -GIT_EDITOR='sh -c "cat /tmp/new-message.txt > \"$0\""' git rebase --continue -``` - -or use `install -m644 /tmp/new-message.txt "$0"` inside a small editor -script. Both preserve the destination's inode. Git creates a fresh -regular file each time it opens the message editor, so the symlink -hazard is largely theoretical in the common case, but the safer forms -cost nothing. - -**Scope-of-invocation pitfall.** `GIT_EDITOR="cp ..."` fires for _every_ -editor invocation during the wrapped command, including conflict editors -and any other commits' message editing. Use it only when you know exactly -which one invocation will happen. For any rebase where you don't know, -write a dispatcher (below) or prefer the direct alternatives: - -- `git commit --amend -F /tmp/message.txt` — supplies the message - directly, no editor. -- `git commit -F /tmp/message.txt` — same for fresh commits. - -The `cp` trick is the right tool only when git owns the invocation -(mid-rebase). - -#### Multiple reword targets in one rebase - -When several commits are being reworded in a single rebase pass, git calls -the editor once per `reword` action. Write a dispatcher that recognizes -which commit is being reworded by inspecting the current message — and -fails loudly when an invocation doesn't match anything it knows about: - -```bash -cat > /tmp/msg-dispatch.sh <<'EOF' -#!/bin/sh -set -eu -target=$1 -first_line=$(head -1 "$target") -case "$first_line" in - "Old subject line A") - cat /tmp/msg-A.txt > "$target" ;; - "Old subject line B"*) - cat /tmp/msg-B.txt > "$target" ;; - *) - echo "msg-dispatch: unmatched message: $first_line" >&2 - exit 1 ;; -esac -EOF -chmod +x /tmp/msg-dispatch.sh -GIT_EDITOR=/tmp/msg-dispatch.sh git rebase -i origin/main -``` - -The `*)` catch-all is mandatory. Without it, a `reword` action whose -message doesn't match any known case silently accepts the original -message, and the rebase reports success — violating the "errors must -surface" rule. A failing exit aborts the rebase at the unmatched commit -and tells you which one. - -Disambiguation: if two commits share an identical subject line, the -dispatcher can't tell them apart from `head -1` alone. Either: - -- Match on a longer prefix using more of the message body, or -- Use `git rebase -i` with explicit SHAs in the todo and have the - dispatcher key on the commit currently being reworded by reading - `git rev-parse HEAD` inside the script. During a `reword` action, git - cherry-picks the target commit onto the rebase head _before_ opening - the editor, so HEAD inside the dispatcher resolves to the target's - newly-rewritten SHA. The same is true at the editor invocation for - `edit` (HEAD = the commit you stopped at, before any amend) and at - the message-combine step of `squash` (HEAD = the partially-combined - commit so far). `fixup` does not invoke the editor — the message is - taken from the predecessor unchanged — so no dispatcher fires. - -### Pattern 4: Edit a commit in place - -This is the workhorse for folding a change into an earlier commit. If -the target is HEAD, don't rebase at all — `git commit --amend` (with -`-F` for a pre-written message, `--no-edit` to keep the existing one). -The rest of this pattern is for editing an earlier commit. - -Mark the target `edit`, modify the working tree at the stop, and -`git commit --amend`: - -```bash -# editor script: sed 's/^pick TARGET_SHA/edit TARGET_SHA/' -GIT_SEQUENCE_EDITOR=/tmp/rebase-editor.sh git rebase -i origin/main - -# At the stop: -# ... edit files in working tree ... -git add -git commit --amend --no-edit -git rebase --continue -``` - -If a later commit's diff conflicts with the amendment, git stops again at -the conflicted commit. Resolve and continue. This is the price of editing -mid-history: every commit downstream of the edit gets replayed and may -conflict. - -**Why prefer this over the `fixup! + --autosquash` flow (Pattern 5)?** -With `edit`, you author the fix against the _target commit's actual -tree_ — what was there at that point in history. With `fixup!`, you -author the change at HEAD's tree (after all intervening commits), and -`--autosquash` later tries to apply that diff against the much-earlier -target tree. When the fix touches anything that intervening commits -also modified, that backward apply conflicts — and you end up -resolving a conflict between a hunk written against late state and a -tree from early state, which is easy to get wrong (dragging in -late-state assumptions). Reach for `edit` whenever the fix's content -might depend on intervening commits, or whenever the branch has -non-trivial churn between the target and HEAD. Reach for `fixup!` -(Pattern 5) for small, isolated changes you're confident don't overlap -intervening work. - -Two smaller wins follow from the same property: at the `edit` stop, -the working tree is exactly the target commit's state. First, no -accidentally-bundled drive-by changes from HEAD can sneak into your -amend. Second, you can install dependencies, lint, build, and run -tests against the _historical_ state — verifying the commit actually -works in the world it lived in. With `fixup!`, your validation only -ever sees HEAD's tree; the squashed commit is never tested against -the rewound state where it lands. (Pattern 7's `--exec` mechanizes -this validation across every commit in the rebase.) - -### Pattern 5: Fixup + autosquash - -For small, isolated changes that don't depend on context introduced -after the target commit, the `fixup!`-subject + `--autosquash` flow is -the cleanest path. When the fix overlaps intervening work, prefer -Pattern 4 (Edit in place) instead. - -```bash -# Make the change at HEAD, then: -git commit -m "fixup! " - -# Later, fold all such fixups into their targets: -GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash origin/main -``` - -`--autosquash` reorders the todo so each `fixup! X` commit becomes a -`fixup` action right after commit `X`. With `GIT_SEQUENCE_EDITOR=true`, -the editor (`true`, a successful no-op) accepts the autosquashed plan -unchanged. - -**Generate the fixup subject automatically** with `git commit --fixup=SHA` -when the target SHA is known and stable. It writes `fixup! ` for you. - -**Pitfall: `--no-verify` only for commit-msg hooks on transient fixups.** -A commit-msg hook that enforces a subject-length limit will reject `fixup! -` even though the squashed result inherits the target's -compliant message. `--no-verify` on the transient fixup is acceptable -because the message is discarded at squash time. Note that -`--no-verify` is a single switch — it disables _both_ the commit-msg -and pre-commit hook chains; you can't disable one without the other. - -**`--no-verify` is NOT acceptable for pre-commit hooks** that run linters, -formatters, or tests on the working tree. The squashed commit inherits the -same working tree, so any defect that would have been caught at the fixup -commit will still be there in the squashed result. The hook would catch -it next time anyway, and you've just lost the early warning. - -**Pitfall: every `--fixup` commit runs the full pre-commit hook chain.** -On projects with slow pre-commit hooks (full test suites, codegen, type -generation), creating ten fixups in a row is ten hook runs. There's no -correctness-preserving shortcut — pace your fixups accordingly, or use -`git commit -n` only on hooks you're sure won't matter for the -intermediate state (and accept the same caveat as above). - -**Exception: hooks that mutate the working tree fight the rebase.** A -pre-commit hook that re-formats files, regenerates code, or stages -additional files during the hook itself can desync a rebase — git -replays a commit, the hook rewrites the tree, and the resulting commit -no longer matches what the rebase plan recorded. When you must rebase -under such a hook, temporarily disable the _mutating step specifically_ -(uninstall pre-commit, comment out the relevant hook, set the hook's -documented no-op env var) rather than reaching for blanket -`--no-verify`, which discards every other pre-commit safety check on -every replayed commit. Restore the hook after the rebase. - -### Pattern 6: Split a commit into pieces - -Builds on the `edit` mechanism from Pattern 4: stop the rebase at the -commit you want to split, then reconstruct it as multiple commits before -continuing. - -```bash -cat > /tmp/rebase-editor.sh <<'EOF' -#!/bin/sh -set -eu -sed -i.bak 's/^pick TARGET_SHA/edit TARGET_SHA/' "$1" -rm -f "$1.bak" -echo "--- rewritten rebase plan ---" >&2 -cat "$1" >&2 -EOF -chmod +x /tmp/rebase-editor.sh -GIT_SEQUENCE_EDITOR=/tmp/rebase-editor.sh git rebase -i origin/main - -# Git stops at TARGET_SHA with that commit applied. Verify the working -# tree is clean (the commit-being-split is the only thing in the -# working/staging area): -git status - -# Undo the commit but keep its changes in the working tree (--mixed is -# the default, but spell it out so the intent is unambiguous): -git reset --mixed HEAD~ - -# Stage and commit the pieces. Use exact paths, NOT `git add -A` or -# wildcards — a typo here can re-introduce content from outside the split -# commit if your working tree had unrelated changes. -git add path/to/group-a/specific-file.ts -git commit -m "Subject for group A" -git add path/to/group-b/specific-file.ts -git commit -m "Subject for group B" - -# Resume the rebase: -git rebase --continue -``` - -If the pieces should fold into different _other_ commits, name them with -the `fixup!` prefix and let a follow-up autosquash route them. The -`--no-verify` below disables _both_ pre-commit and commit-msg hook -chains (the flag can't disable one without the other). It's acceptable -on these transient fixups because (a) the commit-msg hook would reject -the `fixup! ` line that the squash discards anyway, and -(b) the working tree at this fixup is the same tree that will be -squashed into the target — the next commit through the pre-commit hook -will see exactly the same state and either accept or reject it on the -same merits: - -```bash -git add path/to/group-a/specific-file.ts -git commit --no-verify -m "fixup! " -git add path/to/group-b/specific-file.ts -git commit --no-verify -m "fixup! " -git rebase --continue - -# Then: -GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash origin/main -``` - -### Pattern 7: Validate every commit during the rebase - -For "every commit on this branch must build / lint / test," let `git -rebase --exec` enforce it during the rebase itself: - -```bash -# Substitute the project's build / type-check / test command. -git rebase --exec '' origin/main -``` - -`--exec` runs the given command after each pick. If it fails, the rebase -stops at the broken commit — ready for you to amend in place. This is -strictly better than the after-the-fact validation loop in the workflow -below, because the broken commit is right there under your fingers with -the failed state still in the working tree. - -For a quick gate, use the project's build or type-check command. For -full validation, use the project's test command — slower, but catches -commits where tests don't yet pass. - -### Stacked branches: `--update-refs` - -If the branch you're rebasing has dependent branches stacked on it — -intermediate refs pointing at commits the rebase will rewrite — git ≥ -2.38 can move them forward automatically: - -```bash -git rebase --update-refs -i origin/main -``` - -Without `--update-refs`, the stacked branches end up pointing at the -_old_, now-orphaned commits, and you have to reset each one manually -against the reflog. Enable globally with `git config rebase.updateRefs -true` if you work with stacked branches routinely. - -### Repeated rebases against an unstable base - -If you must rebase the same branch repeatedly against a moving base, enable -`git rerere` (reuse recorded resolution) so you only resolve each conflict -once: - -```bash -git config rerere.enabled true -``` - -The first time you resolve a conflict, git records the resolution keyed on -the conflict's content. On a subsequent rebase that produces the same -conflict, git applies the recorded resolution automatically. You still -need to `git add` and continue, but you don't re-do the resolution work. - -## Workflow: a rebase session start to finish - -1. **Identify what needs fixing.** Read `git log --oneline origin/main..HEAD` - and the diffs. List the surgery you need: drops, rewords, squashes, - splits, in-place amends. - -2. **Branch your way back.** - - ```bash - git branch backup--pre-rebase - ``` - -3. **Plan the smallest viable set of operations.** Each operation is a - separate rebase. Multiple small rebases with validation between is - easier to debug than one giant rebase. - -4. **For each operation:** - - Write the editor script (if scripting the todo). - - Write any pre-canned commit messages (if rewording). - - Run the rebase. - - Resolve conflicts as they arise. - - When the rebase finishes, run `git diff backup--pre-rebase HEAD`. - If you intended to change content, the diff is meaningful and you read - it. If you only intended to reshape history, the diff is empty. - -5. **Validate.** - - `git diff backup--pre-rebase HEAD --stat` — empty unless - intended. - - Per-commit build, capturing failure output so a red mark is - actionable: - ```bash - branch=$(git rev-parse --abbrev-ref HEAD) - mkdir -p /tmp/per-commit-build - for sha in $(git log --reverse --format=%h origin/main..HEAD); do - git checkout -q $sha - if > "/tmp/per-commit-build/$sha.log" 2>&1; then - echo "pass $sha" - else - echo "FAIL $sha — see /tmp/per-commit-build/$sha.log" - fi - done - git checkout -q "$branch" - ``` - Note: capture the symbolic branch name _before_ the loop (the loop's - checkouts leave you on detached HEAD if you don't). - - Per-commit tests (the same loop with ``). - - Final tree: the project's full build and test command. - - Even better, fold validation into the rebase itself with `git rebase ---exec` (Pattern 7) so the rebase stops at the first broken commit. - -6. **Delete the backup** once you've pushed and confirmed the final state: - ```bash - git branch -D backup--pre-rebase - ``` - -## Common conflict patterns and resolutions - -When a dropped commit had a counterpart in a later commit (added X in A, -removed X in B), dropping A causes B's removal to fail to apply. The right -resolution is "keep neither side" — the file should end up as if neither -A nor B happened. Edit the conflict markers out directly: - -``` -<<<<<<< HEAD -// nothing here, X was never added -======= -// B's removal of X ->>>>>>> B (later commit) -``` - -becomes: - -``` -// nothing here, X was never added -``` - -When you `edit` a commit and modify a hunk that a later commit also -touches, that later commit may conflict on the same hunk. Note that during -a rebase, `--ours` and `--theirs` are _inverted_ from the normal merge -sense: - -- `--ours` = the rebase target (HEAD at the conflict point, which is your - edited result so far) -- `--theirs` = the commit being replayed (your in-flight commit's version) - -So in both common cases — your edit _includes_ the later commit's intent, -or your edit _supersedes_ it — the version you want to keep is in `--ours` -(HEAD). The conflicting commit is either now a no-op (and git drops it -automatically when its tree change becomes empty) or partially still -needed (in which case `git rebase --skip` after deciding deliberately, or -edit the markers manually). - -Resolution decision tree: - -- If your edit makes the later commit redundant (its intent is already in - HEAD): `git checkout --ours ` then `git add`. On `git rebase ---continue`, git's handling of the now-empty commit is configurable - via `--empty=` (the documented default for the interactive merge - backend is `stop`, not `drop`). If git stops on the empty commit: - - Confirm the diff is genuinely empty: `git diff --cached` should be - silent. - - `git rebase --skip` to drop with intent, or - - `git commit --allow-empty` then `git rebase --continue` to preserve - an empty marker commit if that's what you actually want. - Older versions and some configs auto-drop without stopping — be ready - for either path. -- If the later commit's version is what you actually want (your edit - was wrong, or your edit accidentally over-included downstream - content): `git checkout --theirs `. Whether this is "rare" - depends on why you started the rebase — it's common in mid-edit - reconsiderations, rare in pure cleanup rebases. -- If neither side alone is right: edit the conflict markers manually. - -**Beware: `git checkout --ours ` and `--theirs ` are -whole-file operations.** If a file has five hunks and only one -conflicts, `--ours` blows away non-conflicting `--theirs` content -elsewhere in the file (and vice versa). For files with mixed -conflicting and non-conflicting hunks, edit the conflict markers -manually — don't reach for `--ours`/`--theirs` as a shortcut. - -## What not to do - -- **Don't use `--no-verify` to bypass pre-commit hooks** (lint, format, - tests). The squashed commit inherits the same working tree, so the hook - failure will resurface. The commit-msg-hook carve-out for `fixup! -` commits is the only acceptable use; document any other - use explicitly. -- **Don't `git rebase --skip` to dodge a conflict you don't understand.** - Skip discards the currently-applying commit's intent entirely; any - partially-staged resolution is also discarded. If you intended to keep - the commit, you'll lose content. Resolve the conflict instead — or use - `git rebase --abort` if you've lost the thread. -- **Don't reach for `--force-with-lease` to push** until you've validated - the rebased branch against the backup branch. The backup is your last - line of defense. -- **Don't try to clean up history that's already pushed and shared** - unless the rest of the team is on board. Force-pushing rebased history - forces every collaborator to reset their local copies. -- **Don't use `GIT_EDITOR="cp ..."` for an unknown number of editor - invocations** — it will fire for every one, including conflict editors - you didn't plan for. Use a dispatcher script (Pattern 3) or prefer - `git commit -F` / `--amend -F` when you control the call directly. -- **Don't trust a scripted rebase's exit code alone.** Some safety - configs (e.g. `rebase.missingCommitsCheck=error`) pause rather than - abort when the rebase plan is rejected; the editor script exits 0 and - the outer `git rebase` command exits 0 too, but `.git/rebase-merge/` - is left in place. Always check for a leftover rebase dir after the - command returns — see "Detecting a zombie rebase" in "Safety first". diff --git a/plugins/corbits-skills/skills/idiot-proof/SKILL.md b/plugins/corbits-skills/skills/idiot-proof/SKILL.md deleted file mode 100644 index 45ac7d42c..000000000 --- a/plugins/corbits-skills/skills/idiot-proof/SKILL.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: idiot-proof -user-invocable: false -disable-model-invocation: true -description: Less is more. Reuse. Clean only files you already touch. Read first. ---- - -Prefer deletion over addition. Shortest working diff. - -Reuse existing helpers, types, and tests. Do not copy. - -In files you already touch: remove dead code, unused imports, and comments that narrate. Do not clean other files. - -Read the target, its neighbors, and existing tests before editing. - -No commented-out code, no shims for impossible states, no duplicated logic, no adapters for callers you own. - -Comments only for non-obvious constraints. - -Review: flag only what this diff introduced. Cite path. Do not fix. diff --git a/plugins/corbits-skills/skills/lexicon/SKILL.md b/plugins/corbits-skills/skills/lexicon/SKILL.md deleted file mode 100644 index 2b0187025..000000000 --- a/plugins/corbits-skills/skills/lexicon/SKILL.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -name: lexicon -description: Diff director prompts against the agents repo at a pinned commit and file drift issues. -argument-hint: "[--pin ] [--file-issues]" ---- - -# Lexicon - -Own prompt drift and size for the director prompts mirrored from the -agents repo. Diff each same-name director prompt against the agents -checkout at a pinned commit, report assembled prompt sizes, and file -Linear issues for drift. - -## What this is not - -- Not a director. There is no `lexicon` director package, no - `src/agent/directors/lexicon/` module, and no dispatch - classification route. Never call `spawn_agent(agent="lexicon")` — - this skill runs as a `/lexicon` slash playbook on the primary only. -- Not a fleet router. It does not assign identity or dispatch workers. -- Not ponytail. That skill stays untouched. - -## Step 1: Pin the agents commit - -All comparisons in one run use a single pinned commit of the agents -checkout — never float mid-run. - -1. Resolve the checkout — probe `../agents` beside this repo, then - `$AGENTS_CHECKOUT` when set, else ask the operator for the path. - Never assume a machine-specific default. -2. Record the pin: `git -C rev-parse HEAD`. -3. Read every agents-side file with `git show :` so local - working-tree edits cannot skew the diff. - -## Step 2: Match same-name prompts - -- Director side: the closed `DIRECTOR_IDS` set in - `src/agent/directors/types.ts`; each prompt lives in - `src/agent/directors//package.ts` as `systemPrompt`. -- Agents side: `plugins/*/agents/*.md` files in the checkout, matched - by file basename — `.md` matches director `` exactly. -- Near-misses are not diffs: `critique.md` is not `reviewer`, and - `marketing-intern.md` is not `coder`. List them as unmatched, do - not force a comparison. - -## Step 3: Diff same-name pairs - -For each matched pair, compare the director `systemPrompt` against the -agents file body at the pin. Report per director: in sync, or drifted -with the drifted sections quoted on both sides. Note the ported-from -commit recorded in the package comment (e.g. `@ 6e16b6c`) -when it disagrees with the pin — a stale port marker is itself drift. - -## Step 4: Report assembled prompt sizes - -Reuse the canonical helper in `src/agent/prompt-sizes.ts` — do not -hand-roll a new measurement: - -- `directorPromptSizeTable()` for the full per-director x per-family - table (default assembly vs Grok, pinned env). -- `formatPromptSizeTable(rows)` to render it as markdown. - -Include the rendered table in the report. Sizes move only when real -prompt changes land; the env and provider inputs stay pinned. - -## Step 5: File Linear issues for drift - -Follow the `linear-issue-workflow` skill conventions. File one issue -per drifted director (never one mega-issue across directors), using -the mounted Linear MCP tools as they appear in the toolset — do not -invent a Linear REST client. Each issue carries the pinned agents -commit, the quoted drift from Step 3, and the size-table row from -Step 4. Without `--file-issues`, report the drift and stop — do not -file. Without mounted Linear MCP tools, stop and tell the operator -to enable Linear MCP. - -## Report - -Per director: match status (matched / unmatched with reason), sync -status at the pin, size rows, and the filed issue id or why nothing -was filed. diff --git a/plugins/corbits-skills/skills/native-integration/SKILL.md b/plugins/corbits-skills/skills/native-integration/SKILL.md index c7bc9c1af..21e9456ce 100644 --- a/plugins/corbits-skills/skills/native-integration/SKILL.md +++ b/plugins/corbits-skills/skills/native-integration/SKILL.md @@ -10,7 +10,7 @@ Corbits-specific rules that the other skills rely on. Load alongside `style` and ## Test runner -Corbits tests use `bun:test` (`bun test`, `bun run test`). `opsh` scripts are bash and use TAP via `prove`; that harness is separate from `bun:test`. Agent commands run through `bash`. +Corbits tests use `bun:test` (`bun test`, `bun run test`). Agent commands run through `bash`. ## Fleet @@ -28,9 +28,7 @@ Move the issue to In Review when the PR is ready for review. Draft or WIP PRs st ## Non-git folders -A folder without `.git` is a valid working directory (scratch, unpacked tarball, new project). Git-using skills (`implement`, `review`, `git-rebase`, `pull-request-review`) no-op or ask when they need a repo. Do not invent a git repo to satisfy them. - -The primary plans sequenced git and the coder executes it via `bash`, using inline `GIT_SEQUENCE_EDITOR` instead of writing editor scripts. +A folder without `.git` is a valid working directory (scratch, unpacked tarball, new project). Git-using skills (`implement`, `review`, `pull-request-review`) no-op or ask when they need a repo. Do not invent a git repo to satisfy them. ## Tracker-agnostic issues diff --git a/plugins/corbits-skills/skills/native-runtime/SKILL.md b/plugins/corbits-skills/skills/native-runtime/SKILL.md deleted file mode 100644 index ef0d23894..000000000 --- a/plugins/corbits-skills/skills/native-runtime/SKILL.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: native-runtime -user-invocable: false -disable-model-invocation: true -description: Compact Corbits worker runtime invariants for baked prompts. ---- - -Use Corbits tool names: `read`, `write`, `edit`, `delete`, -`grep`, `glob`, `bash`, `web_search`, `web_fetch`, -`manage_tasks`, and `ask_director` for worker questions. - -Use file tools for file reads, edits, writes, and deletions. Never use shell -redirects, heredocs, `echo`, `cat`, stream editors, or remove commands as -substitutes for file tools. Use bounded `grep` and `glob` instead of -unbounded recursive shell searches. Use web tools for URLs; never use curl or -wget. - -Workers ask the spawning director with `ask_director`; they cannot reach the -operator. If permission denies an action, make the best effort that remains and -report the assumption or blocker. - -Before implementation reports, run the repository-defined typecheck, relevant -tests, and full verification gate when present. Report every exact command with -outcome and exit status. - -Finish worker turns with the required `Summary`, `Findings`, `Blockers`, and -`Paths` envelope. Do not leave implied work outside the report. diff --git a/plugins/corbits-skills/skills/opsh/SKILL.md b/plugins/corbits-skills/skills/opsh/SKILL.md deleted file mode 100644 index 8ae1ecc95..000000000 --- a/plugins/corbits-skills/skills/opsh/SKILL.md +++ /dev/null @@ -1,482 +0,0 @@ ---- -name: opsh -description: Write and debug opsh scripts and use its built-in libraries. -user-invocable: false ---- - -# opsh Scripting - -opsh is a scripting environment for operations use. It is a curated bash -environment with sensible defaults and a standard library. Scripts are -bash, but opsh sets strict runtime options and provides a library -ecosystem accessed via `lib::import`. - -## Script Structure - -Every opsh script starts with: - -```bash -#!/usr/bin/env opsh - -lib::import git semver # import libraries you need - -# your script here -``` - -opsh can also be sourced into an existing bash script: - -```bash -#!/usr/bin/env bash -source opsh -lib::import git -``` - -## Runtime Defaults - -opsh sets these options before your script runs. Do not disable them. - -| Setting | Effect | -|--------------------------|-----------------------------------------------------| -| `set -e` (errexit) | Non-zero return terminates unless caught | -| `set -u` (nounset) | Referencing an unset variable is fatal | -| `set -o pipefail` | A pipeline fails if any command in it fails | -| `IFS=''` | Word splitting is disabled by default | -| `shopt -s inherit_errexit` | Command substitutions inherit errexit | -| `set -o errtrace` | ERR traps propagate into functions and subshells | - -**The `IFS=''` default is important.** Unquoted `$var` where -`var="a b c"` stays as a single string, not three words. Use -`array::split` when you actually need to split a string. - -## Global Variables - -These are set by opsh before your script runs: - -| Variable | Description | -|---------------|------------------------------------------------| -| `$SCRIPTFILE` | Absolute path to your script | -| `$SCRIPTDIR` | Directory containing your script | -| `$TMPDIR` | Managed temp directory, cleaned up on exit | -| `$OPSHROOTDIR`| Root of the opsh installation | -| `$DEBUG` | Set this (any value) to enable `log::debug` | - -Color variables `$CRED`, `$CGRN`, `$CYEL`, `$CBLU`, `$CNONE` are -available and are automatically empty when output is not a terminal. - -## Import System - -```bash -lib::import [name2 ...] -``` - -- Imports are idempotent; importing the same library twice is safe. -- Libraries can declare their own dependencies (e.g. `ssh` imports - `path`, `cloud-init` imports `command`). -- If a library is not found, the script dies via `log::fatal`. -- Multiple libraries can be imported in a single call. - -## Naming Convention - -All opsh functions use `module::function` naming with `::` as the -namespace separator. Follow this convention in your own scripts: - -```bash -deploy::prepare() { ... } -deploy::execute() { ... } -deploy::cleanup() { ... } -``` - -## Core Functions (Always Available) - -### Logging - -All log output goes to stderr. Messages are colorized when stderr is a -terminal. - -| Function | Behavior | -|----------------|---------------------------------------------| -| `log::debug` | Blue output, only when `$DEBUG` is set | -| `log::info` | Green output | -| `log::warn` | Yellow output | -| `log::error` | Red output | -| `log::fatal` | Red output, then `exit 1` | - -```bash -log::info "deploying version $VERSION..." -log::fatal "config file not found" -``` - -### Exit Triggers - -Register cleanup functions that run on exit in LIFO (last-in, -first-out) order: - -```bash -exit::trigger [args...] -``` - -```bash -start-service() { ... } -stop-service() { ... } - -start-service -exit::trigger stop-service # guaranteed to run on exit -``` - -Multiple triggers are supported. They run in reverse registration order. - -### Temporary Files - -`$TMPDIR` is a managed directory that is automatically removed on exit. -Create files and directories inside it: - -```bash -temp::file [mktemp_args...] # create a temp file in $TMPDIR -temp::dir [mktemp_args...] # create a temp directory in $TMPDIR -``` - -```bash -scratch=$(temp::file) -echo "data" > "$scratch" -# no cleanup needed; $TMPDIR is removed on exit -``` - -### Array Utilities - -```bash -array::join [element2 ...] -array::split -``` - -```bash -parts=(one two three) -array::join , "${parts[@]}" # stdout: one,two,three - -array::split result ":" "$PATH" # result is now an array -``` - -### Version Management - -```bash -opsh::version # print the opsh version -opsh::version::require # fatal if opsh is too old -``` - -```bash -opsh::version::require 0.9.0 -``` - -## Libraries - -### command - -```bash -lib::import command -``` - -| Function | Description | -|-------------------|------------------------------------| -| `command::exists` | Returns 0 if command is in `$PATH` | - -```bash -command::exists docker || log::fatal "docker is required" -``` - -### path - -```bash -lib::import path -``` - -| Function | Description | -|---------------------|--------------------------------------| -| `path::env::add` | Prepend directories to `$PATH` | -| `path::env::remove` | Remove a directory from `$PATH` | - -```bash -path::env::add /opt/mytools/bin -path::env::remove /usr/local/old/bin -``` - -### git - -```bash -lib::import git -``` - -| Function | Description | -|-------------------------------|----------------------------------------------------------| -| `git::repo::version` | Version from `git describe --tags --dirty` or short SHA | -| `git::repo::current-branch` | Current branch name | -| `git::repo::is-clean` | Returns 0 if working tree is clean | -| `git::tag::exists` | Returns 0 if a local tag exists | -| `git::tag::lookup::remote` | Lookup a tag on a remote; prints commit hash | - -`git::tag::lookup::remote` returns 1 if the tag is not found, 2 if -ambiguous. - -```bash -VERSION=$(git::repo::version) -git::repo::is-clean || log::fatal "working tree is dirty" -``` - -### semver - -```bash -lib::import semver -``` - -| Function | Description | -|-----------------|---------------------------------------------------------| -| `semver::parse` | Parse into `$OPSH_SEMVER` array `[major, minor, patch]` | -| `semver::test` | Compare two versions: `-eq`, `-gt`, `-lt`, `-ge`, `-le` | -| `semver::bump` | Bump `major`, `minor`, or `patch`; prints new version | - -`semver::parse` populates the global `$OPSH_SEMVER` array. If the -version has a suffix (e.g. `-rc1`, `+build`), it appears as a fourth -element. - -```bash -semver::parse v2.1.0 || log::fatal "bad version" -echo "major: ${OPSH_SEMVER[0]}" # 2 - -semver::test "$current" -ge "$minimum" || log::fatal "version too old" - -new=$(semver::bump minor v1.2.3) # v1.3.0 -``` - -### ssh - -```bash -lib::import ssh -``` - -| Function | Description | -|--------------------------|------------------------------------------------| -| `ssh::begin` | Start SSH context: agent, proxied ssh, config | -| `ssh::end` | Tear down SSH context | -| `ssh::config` | Append SSH config from stdin | -| `ssh::key::add` | Add keys from files or stdin to the agent | -| `ssh::background::run` | Launch SSH port forwarding in background | -| `ssh::background::close` | Close background port forwarding | - -`ssh::begin` creates an isolated SSH agent, a proxied `ssh` binary -that uses a managed config file, and registers `ssh::end` as an exit -trigger. Everything between `ssh::begin` and `ssh::end` uses this -isolated context. - -```bash -ssh::begin -ssh::config <<'EOF' -Host bastion - User deploy - HostName bastion.example.com -EOF -ssh::key::add ~/.ssh/deploy_key -ssh bastion "uptime" -ssh::end -``` - -### cloud-init - -```bash -lib::import cloud-init -``` - -| Function | Description | -|--------------------------------|-------------------------------------| -| `cloud-init::is-enabled` | Returns 0 if cloud-init is present | -| `cloud-init::wait-for-finish` | Blocks until cloud-init completes | - -```bash -if cloud-init::is-enabled; then - log::info "waiting for cloud-init..." - cloud-init::wait-for-finish -fi -``` - -### step-runner - -```bash -lib::import step-runner -``` - -| Function | Description | -|---------------|-------------------------------------------------------| -| `steps::run` | Run all `prefix::*` functions in alphabetical order | - -Define functions with a shared prefix, then run them: - -```bash -deploy::01-build() { ... } -deploy::02-test() { ... } -deploy::03-push() { ... } - -steps::run deploy # runs all three in order -steps::run deploy 02-test # starts from 02-test -``` - -`steps::run` logs each step as it executes. Use numbered prefixes to -control ordering. - -### test-harness - -```bash -lib::import test-harness -``` - -| Function | Description | -|----------------------|------------------------------------------| -| `testing::register` | Register a test function with a description | -| `testing::run` | Execute all tests, output TAP v13 | -| `testing::fail` | Fail the current test with a message | - -See the "Writing Tests" section below. - -## Common Idioms - -### Fatal preconditions - -Use `|| log::fatal` for conditions that must be true: - -```bash -[[ -f $CONFIG ]] || log::fatal "config not found: $CONFIG" -command::exists kubectl || log::fatal "kubectl is required" -``` - -### Recoverable errors - -Capture the return code when you need to handle failure: - -```bash -local ret=0 -some-command || ret=$? -if [[ $ret -ne 0 ]]; then - log::warn "command failed with $ret, retrying..." -fi -``` - -### Cleanup with exit triggers - -Register cleanup in the order you acquire resources. They run in -reverse: - -```bash -start-database -exit::trigger stop-database - -start-server -exit::trigger stop-server -# on exit: stop-server runs first, then stop-database -``` - -### Default values - -Use the `${var:=default}` pattern (standard bash parameter expansion). -The `:` builtin discards the result while still triggering assignment: - -```bash -: "${DEPLOY_ENV:=staging}" -: "${RETRIES:=3}" -``` - -## Writing Tests - -Test files use the `test-harness` library and output TAP v13 format. -They are typically run with `prove`: - -```bash -#!/usr/bin/env opsh - -lib::import test-harness - -check-something() { - local result - result=$(my-function) - [[ $result = "expected" ]] || testing::fail "got: $result" -} - -testing::register check-something "verify my-function output" - -check-another-thing() { - my-precondition || testing::fail - my-action || testing::fail "action failed" -} - -testing::register check-another-thing "verify action succeeds" - -testing::run -``` - -**Key patterns:** - -- Use `|| testing::fail` as an assertion. Optionally pass a message. -- Each test function runs in a subshell, so variable changes do not - leak between tests. -- Register all tests before calling `testing::run`. -- Use `$SCRIPTDIR` to locate fixture files and shared utilities - relative to the test file. -- Source shared test helpers with `source "$SCRIPTDIR/utils.opsh"`. - -### Checking command output - -A common pattern uses `diff -u` against a heredoc to verify output: - -```bash -check-output() { - local outfile - outfile=$(temp::file) - - my-command > "$outfile" - - diff -u - "$outfile" < # import a library -log::{debug,info,warn,error} # log to stderr -log::fatal # log and exit 1 -exit::trigger # register cleanup (LIFO) -temp::file / temp::dir # create managed temp files -array::join # join to stdout -array::split # split into named array -command::exists # check PATH for a command -path::env::add # prepend to $PATH -path::env::remove # remove from $PATH -git::repo::version # repo version string -git::repo::current-branch # current branch name -git::repo::is-clean # clean working tree? -git::tag::exists # local tag exists? -semver::parse # parse into $OPSH_SEMVER -semver::test # compare versions -semver::bump # bump major/minor/patch -ssh::begin / ssh::end # SSH context lifecycle -ssh::config < [start] # run prefixed functions -testing::register [desc] # register a test -testing::run # execute all tests (TAP v13) -testing::fail [msg] # fail current test -``` diff --git a/plugins/corbits-skills/skills/ponytail/SKILL.md b/plugins/corbits-skills/skills/ponytail/SKILL.md deleted file mode 100644 index 6457a589f..000000000 --- a/plugins/corbits-skills/skills/ponytail/SKILL.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: ponytail -user-invocable: false -description: Compact coder mode guidance for minimal safe implementation diffs. ---- - -Ponytail is a Coder discipline for keeping implementation diffs small without -weakening the brief. - -Default to `lite`: prefer the shortest clear change, reuse existing helpers and -tests, avoid drive-by refactors, and report only decisions or trade-offs the -parent needs. Reporting only decisions and trade-offs never licenses omitting -verification evidence: the Build Gate command-evidence requirement applies in -full in every mode. `off` means ignore Ponytail and follow the rest of the brief -normally. `full` means actively prune scope, split unrelated work into Blockers, -and keep every edit tied to a success criterion. `ultra` means the same -discipline under stricter pressure: delete dead paths you touch, reject -ornamental structure, and stop as soon as acceptance criteria and required -verification are complete. - -Escalation ladder: start at `lite`; move to `full` when the brief asks for -minimal surface area, context is expensive, or the diff starts spreading; move -to `ultra` only when requested or when the parent explicitly prioritizes the -smallest viable implementation. De-escalate or turn `off` when Ponytail would -hide necessary reasoning. - -Safety precedence is absolute: correctness, validation, security, -accessibility, data integrity, tests, repo conventions, operator requirements, -and explicit success criteria outrank minimal LOC. A mode never weakens those -constraints. - -For review or audit, treat Ponytail as a lens for reviewer or primary -instructions; do not create a Ponytail director or agent. diff --git a/plugins/corbits-skills/skills/refactor/SKILL.md b/plugins/corbits-skills/skills/refactor/SKILL.md index 47dc7b83a..d4e49cea5 100644 --- a/plugins/corbits-skills/skills/refactor/SKILL.md +++ b/plugins/corbits-skills/skills/refactor/SKILL.md @@ -70,7 +70,6 @@ Append an **Improvement Plan** section to the document with: - Suggested order of operations - Any constraints or risks to be aware of - Enough detail that another agent could execute the plan -- For structural transformations (renames, signature changes, API migrations), note that the `ast-grep` skill should be loaded during execution — it enables bulk AST-based rewrites instead of manual read-edit-write cycles ## Output diff --git a/src/telemetry/classify.ts b/src/telemetry/classify.ts index 7f41a9746..f3f2a2dc3 100644 --- a/src/telemetry/classify.ts +++ b/src/telemetry/classify.ts @@ -78,26 +78,16 @@ const BUILT_IN_AGENT_NAMES: ReadonlySet = new Set([ // plugin id and kind — no skill list — so the closed set is spelled out here // and pinned by src/telemetry/product-events.test.ts. // `user-invocable: false` is a slash-surface flag, not a telemetry flag: -// eleven bundled skills carry it — eight stay listed and loadable -// (git-rebase, linear-issue-workflow, opsh, philosophy, style, typescript, -// native-integration, ponytail), `git-worktrees` resolves by explicit name -// only, and two stay hidden from listings (idiot-proof, native-runtime): -// `disable-model-invocation` keeps them out of discovery, but an explicit -// `use_skill` / `resolveSkillBody` load still resolves — and reports `custom`. -// Of the eleven, -// seven are reported by name and four stay `custom` (pinned; conservative -// under-reporting, never a leak). Project- or plugin-authored skills are -// never reported by name. +// background skills stay loadable by explicit name, and a bundled skill that +// is not in the set below reports `custom` (conservative under-reporting, +// never a leak). Project- or plugin-authored skills are never reported by +// name. const FIRST_PARTY_SKILL_NAMES: ReadonlySet = new Set([ - "ast-grep", "create-issue", - "git-rebase", "git-worktrees", "implement", "interview", - "lexicon", "linear-issue-workflow", - "opsh", "philosophy", "plan", "pull-request-review", diff --git a/src/telemetry/product-events.test.ts b/src/telemetry/product-events.test.ts index fa7dae78e..97c721e9c 100644 --- a/src/telemetry/product-events.test.ts +++ b/src/telemetry/product-events.test.ts @@ -227,10 +227,7 @@ test("first-party skill names are reported by name; everything else stays custom // name either — the allowlist is the closed set, not the skills directory. // Hidden-from-discovery background skills (explicit loads still resolve) // stay custom. - expect(classifySkillName("idiot-proof")).toBe("custom"); expect(classifySkillName("native-integration")).toBe("custom"); - expect(classifySkillName("native-runtime")).toBe("custom"); - expect(classifySkillName("ponytail")).toBe("custom"); expect(classifySkillName("Review")).toBe("custom"); expect(classifySkillName("")).toBe("custom"); });