mirror of
https://github.com/obra/superpowers.git
synced 2026-06-14 22:59:06 +08:00
Compare commits
1 Commits
codex/shel
...
update-por
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
505a13e629 |
13
README.md
13
README.md
@@ -4,7 +4,7 @@ Superpowers is a complete software development methodology for your coding agent
|
|||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
Give your agent Superpowers: [Claude Code](#claude-code), [Antigravity](#antigravity), [Codex App](#codex-app), [Codex CLI](#codex-cli), [Cursor](#cursor), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [GitHub Copilot CLI](#github-copilot-cli), [OpenCode](#opencode), [Pi](#pi).
|
Give your agent Superpowers: [Claude Code](#claude-code), [Codex App](#codex-app), [Codex CLI](#codex-cli), [Cursor](#cursor), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [GitHub Copilot CLI](#github-copilot-cli), [OpenCode](#opencode), [Pi](#pi).
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
@@ -60,17 +60,6 @@ The Superpowers marketplace provides Superpowers and some other related plugins
|
|||||||
/plugin install superpowers@superpowers-marketplace
|
/plugin install superpowers@superpowers-marketplace
|
||||||
```
|
```
|
||||||
|
|
||||||
### Antigravity
|
|
||||||
|
|
||||||
Install Superpowers as a plugin from this repository:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
agy plugin install https://github.com/obra/superpowers
|
|
||||||
```
|
|
||||||
|
|
||||||
Antigravity runs the plugin's session-start hook, so Superpowers is active from
|
|
||||||
the first message. Reinstall with the same command to update.
|
|
||||||
|
|
||||||
### Codex App
|
### Codex App
|
||||||
|
|
||||||
Superpowers is available via the [official Codex plugin marketplace](https://github.com/openai/plugins).
|
Superpowers is available via the [official Codex plugin marketplace](https://github.com/openai/plugins).
|
||||||
|
|||||||
@@ -124,10 +124,19 @@ and reuse that fallback wording when it doesn't.
|
|||||||
### You may not need a new directory at all
|
### You may not need a new directory at all
|
||||||
|
|
||||||
Some "new harnesses" are really existing integrations under a different
|
Some "new harnesses" are really existing integrations under a different
|
||||||
installer. Factory's Droid, for example, consumes the Claude Code plugin via its
|
installer. Factory's Droid consumes the Claude Code plugin via its own `plugin
|
||||||
own `plugin install` command and needs no new files here. Before building,
|
install` command and needs no new files here. Antigravity (`agy`) goes further:
|
||||||
check whether the harness can simply load an existing manifest. A port that adds
|
`agy plugin install <repo-url>` imports the Claude Code plugin's skills **and its
|
||||||
nothing to this repo but a paragraph in the README is a perfectly good outcome.
|
`SessionStart` hook, which agy then runs** — so the bootstrap fires the Shape-A
|
||||||
|
way (Part 4) with no new manifest, hook config, or installer. Its entire
|
||||||
|
integration is a tool-mapping reference plus a README section.
|
||||||
|
|
||||||
|
**So before building anything, install the existing Claude Code (or Gemini)
|
||||||
|
plugin into the harness and run the acceptance test (Part 3).** If the harness
|
||||||
|
already loads the skills and runs the hook, you are done — a port that adds
|
||||||
|
nothing to this repo but a tool mapping and a README paragraph is a perfectly
|
||||||
|
good outcome. Don't assume a derived harness needs its own bootstrap mechanism
|
||||||
|
until you've watched the existing one fail.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -204,6 +213,14 @@ had to do every one of these):
|
|||||||
`@`-include syntax and *still not honor them the same way*. Verify with a marker;
|
`@`-include syntax and *still not honor them the same way*. Verify with a marker;
|
||||||
never assume the parent's recipe transfers.
|
never assume the parent's recipe transfers.
|
||||||
|
|
||||||
|
**The inverse also bites: a derived harness may inherit *more* than you expect.**
|
||||||
|
Antigravity runs the Claude Code plugin's `SessionStart` hook even though `agy`'s
|
||||||
|
own plugin docs describe a different model — and we nearly built a whole
|
||||||
|
context-file scaffold on the false premise that it had no session-start hook.
|
||||||
|
Test the cheap path first: install the existing Claude Code (or Gemini) plugin
|
||||||
|
and run the acceptance test (Part 3) *before* writing any harness-specific
|
||||||
|
bootstrap.
|
||||||
|
|
||||||
Then route to a shape:
|
Then route to a shape:
|
||||||
|
|
||||||
- Shell command at session start whose stdout is read → **Shape A**.
|
- Shell command at session start whose stdout is read → **Shape A**.
|
||||||
@@ -290,10 +307,12 @@ part of the installed extension** — never substitute "edit the user's global
|
|||||||
| runs a shell command at session start and reads its stdout | A (shell-hook) | Codex (`hooks/session-start-codex` + `hooks/hooks-codex.json` + `.codex-plugin/`) |
|
| runs a shell command at session start and reads its stdout | A (shell-hook) | Codex (`hooks/session-start-codex` + `hooks/hooks-codex.json` + `.codex-plugin/`) |
|
||||||
| is a JS/TS plugin host with session/message lifecycle callbacks | B (in-process) | OpenCode (`.opencode/`) — or pi (`.pi/`) if it has no native skill tool |
|
| is a JS/TS plugin host with session/message lifecycle callbacks | B (in-process) | OpenCode (`.opencode/`) — or pi (`.pi/`) if it has no native skill tool |
|
||||||
| ships an extension-declared context file it always loads | C (instructions-file) | Gemini (`gemini-extension.json` + `GEMINI.md` + `references/gemini-tools.md`) |
|
| ships an extension-declared context file it always loads | C (instructions-file) | Gemini (`gemini-extension.json` + `GEMINI.md` + `references/gemini-tools.md`) |
|
||||||
| has a plugin install command and a manifest `contextFileName` (or equivalent) the installer keeps | C via the plugin installer | Antigravity (`.antigravity-plugin/` — `agy plugin install` ships a generated context file; verify the installer preserves it — Part 6) |
|
| installs an existing harness's plugin directly, including its hook | none — no new files | Antigravity (`agy plugin install <repo-url>` imports the Claude Code `skills/` + `hooks/` and runs the `SessionStart` hook), Factory Droid |
|
||||||
|
|
||||||
Most real harnesses fit one row cleanly; the last is the hybrid case (rule 2 still
|
Most real harnesses fit one row cleanly. The last row is the cheapest outcome and
|
||||||
holds — the bootstrap rides the install mechanism, never a user-config edit).
|
the one to rule out first (Part 2): if the harness just runs the existing plugin,
|
||||||
|
you write almost nothing. Rule 2 still holds in every row — the bootstrap rides
|
||||||
|
the install mechanism, never a user-config edit.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -480,8 +499,9 @@ path field is **ignored** — one real harness only scanned a `skills/` sitting
|
|||||||
to `plugin.json`); an API/registration call (OpenCode, pi); or you stage an
|
to `plugin.json`); an API/registration call (OpenCode, pi); or you stage an
|
||||||
install dir that pairs the manifest with a **symlink to the repo's `skills/`** and
|
install dir that pairs the manifest with a **symlink to the repo's `skills/`** and
|
||||||
point the installer at the staging dir (verify the installer *dereferences* the
|
point the installer at the staging dir (verify the installer *dereferences* the
|
||||||
symlink and copies the real files — confirm with `agy plugin validate`/`install`
|
symlink and copies the real files — confirm with the harness's own
|
||||||
or the equivalent before relying on it). A `skills` path field is *not* portable.
|
`validate`/`install` command before relying on it). A `skills` path field is *not*
|
||||||
|
portable.
|
||||||
|
|
||||||
Where the mapping lives depends on shape:
|
Where the mapping lives depends on shape:
|
||||||
|
|
||||||
@@ -524,11 +544,14 @@ honors the rule rather than breaking it. Distinguish three cases:
|
|||||||
the file-read tool when the skill applies** — the sanctioned mechanism here,
|
the file-read tool when the skill applies** — the sanctioned mechanism here,
|
||||||
the way `references/pi-tools.md` states it.
|
the way `references/pi-tools.md` states it.
|
||||||
|
|
||||||
**For the bootstrap itself, prefer a declared context file (Part 6).** If the
|
**For the bootstrap itself, first check for an inherited hook (Part 2).** If the
|
||||||
harness has a `contextFileName`-style manifest field — as Antigravity does —
|
harness runs the existing Claude Code plugin's `SessionStart` hook — Antigravity
|
||||||
ship a generated context file through the installer: it's guaranteed-loaded and
|
does; test it before assuming otherwise — the bootstrap is already delivered and
|
||||||
carries both the `using-superpowers` content and the tool mapping. That is the
|
you ship nothing. Only if it doesn't, and the harness has a `contextFileName`-style
|
||||||
strong, preferred path.
|
manifest field, point that field at the real `using-superpowers/SKILL.md` (or a
|
||||||
|
tiny committed `@`-include file like `GEMINI.md`). **Never generate a wrapped copy
|
||||||
|
of the bootstrap at install time:** a generated copy needs an installer and drifts
|
||||||
|
from source. You get to name the file, so name the real one.
|
||||||
|
|
||||||
**Fallback — the surfaced skill index.** If there's no context-file field but
|
**Fallback — the surfaced skill index.** If there's no context-file field but
|
||||||
the harness surfaces each installed skill's name + description at session start,
|
the harness surfaces each installed skill's name + description at session start,
|
||||||
@@ -677,7 +700,7 @@ it. Distribution differs per harness ecosystem — find yours:
|
|||||||
| External marketplace fork, synced by script | Codex | `scripts/sync-to-codex-plugin.sh` rsyncs the tracked plugin files into a separate fork repo and opens a PR. Read its include/exclude list so you ship the right tree (it deliberately drops repo-internal dirs and other harnesses' dotdirs). |
|
| External marketplace fork, synced by script | Codex | `scripts/sync-to-codex-plugin.sh` rsyncs the tracked plugin files into a separate fork repo and opens a PR. Read its include/exclude list so you ship the right tree (it deliberately drops repo-internal dirs and other harnesses' dotdirs). |
|
||||||
| Git-URL extension install | Gemini, OpenCode | Users install from a git URL (`gemini extensions install …`; an `opencode.json` `plugin` array entry). Document the exact command. |
|
| Git-URL extension install | Gemini, OpenCode | Users install from a git URL (`gemini extensions install …`; an `opencode.json` `plugin` array entry). Document the exact command. |
|
||||||
| Package-manifest fields | pi | Declared through fields in the repo-root `package.json`; users install via the harness's package command. |
|
| Package-manifest fields | pi | Declared through fields in the repo-root `package.json`; users install via the harness's package command. |
|
||||||
| Local installer (plugin install) | Antigravity (`agy`) | A small `install.sh` that runs the harness's own `agy plugin install` against a staging dir holding the manifest, the skills, and a generated `contextFileName` context file (the bootstrap). Everything arrives through the install mechanism — *not* by editing the user's config (see below). |
|
| Existing plugin, installed directly | Antigravity (`agy`), Factory Droid | The harness installs another harness's committed plugin from a git URL — `agy plugin install <repo-url>` imports the Claude Code `skills/` + `hooks/` and runs the `SessionStart` hook. No installer script, no new manifest: document the one-line install command. |
|
||||||
|
|
||||||
Then:
|
Then:
|
||||||
|
|
||||||
@@ -685,29 +708,33 @@ Then:
|
|||||||
bootstrap a file the installer *recognizes*, never a user-config edit.** A
|
bootstrap a file the installer *recognizes*, never a user-config edit.** A
|
||||||
`plugin install` typically copies only the components it knows about
|
`plugin install` typically copies only the components it knows about
|
||||||
(skills/agents/commands/mcp/hooks/context) and discards anything else, so a
|
(skills/agents/commands/mcp/hooks/context) and discards anything else, so a
|
||||||
context file the manifest doesn't declare just vanishes from the install. The
|
file the manifest doesn't declare just vanishes from the install. The fix is
|
||||||
fix is **not** to give up and write into the user's config (**rule 2**) — it's
|
**not** to give up and write into the user's config (**rule 2**) — it's to make
|
||||||
to declare the bootstrap as a recognized component. In escalation order:
|
the bootstrap a recognized component. In escalation order:
|
||||||
- **Ship a context file the manifest declares.** If the harness has a
|
- **Check for an inherited hook first.** If `plugin install` imports an existing
|
||||||
`contextFileName`-style field (an extension-declared file it loads every
|
Claude Code plugin's `hooks/` and the harness runs the `SessionStart` hook,
|
||||||
session), that is the strongest clean bootstrap: declare it, and the installer
|
the bootstrap is already delivered and you ship nothing. Antigravity does
|
||||||
preserves it *and* the harness loads it. Generate it at install time from the
|
exactly this — `agy plugin install <repo-url>` brings in the skills and the
|
||||||
live `using-superpowers/SKILL.md` + the tool mapping (wrapped in
|
hook, and a clean session loads `using-superpowers`, triggers `brainstorming`,
|
||||||
`<EXTREMELY_IMPORTANT>`) so the installed bootstrap never drifts. This is what
|
and enters the brainstorming flow before any code. Verify with the acceptance
|
||||||
`.antigravity-plugin/install.sh` does — `agy plugin install` reports
|
test. This is the cheapest and most robust path; rule it out before the rest.
|
||||||
`✔ context : ANTIGRAVITY.md`, and a clean session reads `using-superpowers`'s
|
- **Otherwise, if the manifest declares a context file, point it at the real
|
||||||
SKILL.md, loads `brainstorming`, and enters the brainstorming flow before any
|
skill.** A `contextFileName`-style field names a file the installer preserves
|
||||||
code. **Verify with a marker** that the installer keeps the file and the
|
and the harness loads every session. Point it straight at
|
||||||
harness loads it: one porter wrongly concluded it couldn't, because they
|
`using-superpowers/SKILL.md`, or at a tiny committed `@`-include file like
|
||||||
shipped the file *without* declaring `contextFileName` and it was stripped as
|
`GEMINI.md` if the harness expands includes (prove the expansion — Shape C
|
||||||
unrecognized.
|
caveat). **Do not generate a wrapped copy of the bootstrap at install time:**
|
||||||
|
that needs an installer and can drift from source. You get to name the file,
|
||||||
|
so name the real one. **Verify with a marker** that the installer keeps the
|
||||||
|
file and the harness loads it — an undeclared file is stripped as unrecognized.
|
||||||
- **Otherwise lean on the installed `using-superpowers` skill itself.** If the
|
- **Otherwise lean on the installed `using-superpowers` skill itself.** If the
|
||||||
harness surfaces each installed skill's name + description at session start,
|
harness surfaces each installed skill's name + description at session start,
|
||||||
the `using-superpowers` description ("Use when starting any conversation…")
|
the `using-superpowers` description ("Use when starting any conversation…")
|
||||||
can prompt the model to load it — installing the skill *is* the bootstrap.
|
can prompt the model to load it — installing the skill *is* the bootstrap.
|
||||||
Softer (no guaranteed wrapper; it carries triggering but not the tool mapping
|
Softer (no guaranteed wrapper; it carries triggering but not the tool mapping
|
||||||
— see Step 5), so prefer the declared context file when available.
|
— see Step 5), so prefer an inherited hook or a declared context file when
|
||||||
- If neither works, the harness cannot be cleanly supported yet — **say so**
|
available.
|
||||||
|
- If none works, the harness cannot be cleanly supported yet — **say so**
|
||||||
and raise it, rather than hand-editing the user's config.
|
and raise it, rather than hand-editing the user's config.
|
||||||
|
|
||||||
- **Write install docs.** A `docs/README.<harness>.md` and/or a
|
- **Write install docs.** A `docs/README.<harness>.md` and/or a
|
||||||
@@ -785,6 +812,7 @@ Use this as the live index; when in doubt, read the files, not this table.
|
|||||||
| Harness | Entry point | Bootstrap mechanism | Tool mapping | Tests | Distribution |
|
| Harness | Entry point | Bootstrap mechanism | Tool mapping | Tests | Distribution |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| Claude Code | `.claude-plugin/plugin.json` + `hooks/hooks.json` | shell hook → `hooks/session-start` (`hookSpecificOutput.additionalContext`) | native `Skill` tool; `references/claude-code-tools.md` | `tests/hooks/` | marketplace |
|
| Claude Code | `.claude-plugin/plugin.json` + `hooks/hooks.json` | shell hook → `hooks/session-start` (`hookSpecificOutput.additionalContext`) | native `Skill` tool; `references/claude-code-tools.md` | `tests/hooks/` | marketplace |
|
||||||
|
| Antigravity (`agy`) | none — installs the Claude Code plugin | `agy plugin install <repo-url>` imports `skills/` + `hooks/` and runs the Claude Code `SessionStart` hook | no `Skill` tool; load via `view_file` on `SKILL.md`; `references/antigravity-tools.md` | `tests/antigravity/` | `agy plugin install <git-url>` (README) |
|
||||||
| Codex | `.codex-plugin/plugin.json` + `hooks/hooks-codex.json` | shell hook → `hooks/session-start-codex` | `references/codex-tools.md` | `tests/codex-plugin-sync/`, `tests/hooks/` | fork sync (`scripts/sync-to-codex-plugin.sh`) |
|
| Codex | `.codex-plugin/plugin.json` + `hooks/hooks-codex.json` | shell hook → `hooks/session-start-codex` | `references/codex-tools.md` | `tests/codex-plugin-sync/`, `tests/hooks/` | fork sync (`scripts/sync-to-codex-plugin.sh`) |
|
||||||
| Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | `references/claude-code-tools.md` | `tests/hooks/` | hand-authored |
|
| Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | `references/claude-code-tools.md` | `tests/hooks/` | hand-authored |
|
||||||
| Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | `references/copilot-tools.md` | `tests/hooks/` | — |
|
| Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | `references/copilot-tools.md` | `tests/hooks/` | — |
|
||||||
@@ -796,6 +824,14 @@ Use this as the live index; when in doubt, read the files, not this table.
|
|||||||
|
|
||||||
- **Opt-in isn't a port.** If your human partner has to do anything per session
|
- **Opt-in isn't a port.** If your human partner has to do anything per session
|
||||||
to get Superpowers, the acceptance test fails. Re-read Part 2.
|
to get Superpowers, the acceptance test fails. Re-read Part 2.
|
||||||
|
- **Assuming a derived harness lacks the parent's hook.** Antigravity runs the
|
||||||
|
Claude Code `SessionStart` hook; we nearly shipped a whole context-file scaffold
|
||||||
|
on the false premise that it didn't. Install the existing plugin and run the
|
||||||
|
acceptance test before building any bootstrap mechanism (Part 2).
|
||||||
|
- **Generating a wrapped bootstrap copy at install time.** If a manifest's
|
||||||
|
`contextFileName` lets you name the file, point it at the real
|
||||||
|
`using-superpowers/SKILL.md` (or a committed `@`-include file). A generated copy
|
||||||
|
needs an installer and drifts from source (Part 6).
|
||||||
- **Wrong JSON field → silent failure or double injection.** Shape A only.
|
- **Wrong JSON field → silent failure or double injection.** Shape A only.
|
||||||
Confirm the exact field/nesting; Claude Code reads two fields without dedup.
|
Confirm the exact field/nesting; Claude Code reads two fields without dedup.
|
||||||
- **Hook-config schema varies per harness.** Shape A. Cursor's `hooks-cursor.json`
|
- **Hook-config schema varies per harness.** Shape A. Cursor's `hooks-cursor.json`
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# Lint shell scripts in this repository.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# scripts/lint-shell.sh [--all] [--format] [--strict] [file ...]
|
|
||||||
#
|
|
||||||
# By default, runs ShellCheck and shell syntax checks on changed shell scripts.
|
|
||||||
# Use --format to format with shfmt before linting. Use --all for the full tracked
|
|
||||||
# baseline, or pass files explicitly to lint a smaller set.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
echo "error: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
require_tool() {
|
|
||||||
command -v "$1" >/dev/null 2>&1 || die "required tool '$1' is not on PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
is_shell_file() {
|
|
||||||
local path="$1"
|
|
||||||
local first_line=""
|
|
||||||
|
|
||||||
[[ -f "$path" ]] || return 1
|
|
||||||
|
|
||||||
case "$path" in
|
|
||||||
*.sh)
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
IFS= read -r first_line <"$path" || true
|
|
||||||
[[ "$first_line" =~ ^#!.*[/[:space:]](bash|dash|ksh|sh)([[:space:]]|$) ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_git_work_tree() {
|
|
||||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
|
||||||
|| die "run this from inside a git work tree, or pass files explicitly"
|
|
||||||
}
|
|
||||||
|
|
||||||
add_shell_file() {
|
|
||||||
local path
|
|
||||||
local existing
|
|
||||||
|
|
||||||
path="$1"
|
|
||||||
if ! is_shell_file "$path"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${#files[@]}" -gt 0 ]]; then
|
|
||||||
for existing in "${files[@]}"; do
|
|
||||||
if [[ "$existing" == "$path" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
files+=("$path")
|
|
||||||
}
|
|
||||||
|
|
||||||
collect_all_shell_files() {
|
|
||||||
local path
|
|
||||||
|
|
||||||
ensure_git_work_tree
|
|
||||||
|
|
||||||
while IFS= read -r -d '' path; do
|
|
||||||
add_shell_file "$path"
|
|
||||||
done < <(git ls-files -z)
|
|
||||||
}
|
|
||||||
|
|
||||||
collect_changed_shell_files() {
|
|
||||||
local path
|
|
||||||
|
|
||||||
ensure_git_work_tree
|
|
||||||
|
|
||||||
if git rev-parse --verify HEAD >/dev/null 2>&1; then
|
|
||||||
while IFS= read -r -d '' path; do
|
|
||||||
add_shell_file "$path"
|
|
||||||
done < <(git diff --name-only -z --diff-filter=ACMR HEAD)
|
|
||||||
|
|
||||||
while IFS= read -r -d '' path; do
|
|
||||||
add_shell_file "$path"
|
|
||||||
done < <(git diff --cached --name-only -z --diff-filter=ACMR)
|
|
||||||
else
|
|
||||||
collect_all_shell_files
|
|
||||||
fi
|
|
||||||
|
|
||||||
while IFS= read -r -d '' path; do
|
|
||||||
add_shell_file "$path"
|
|
||||||
done < <(git ls-files --others --exclude-standard -z)
|
|
||||||
}
|
|
||||||
|
|
||||||
collect_requested_shell_files() {
|
|
||||||
local path
|
|
||||||
|
|
||||||
for path in "$@"; do
|
|
||||||
add_shell_file "$path"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
syntax_shell_for() {
|
|
||||||
local path="$1"
|
|
||||||
local first_line=""
|
|
||||||
|
|
||||||
IFS= read -r first_line <"$path" || true
|
|
||||||
|
|
||||||
case "$first_line" in
|
|
||||||
*"/sh"* | *" env sh"* | *"/dash"* | *" env dash"*)
|
|
||||||
printf 'sh'
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
printf 'bash'
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
run_syntax_checks() {
|
|
||||||
local file
|
|
||||||
local shell_name
|
|
||||||
|
|
||||||
for file in "$@"; do
|
|
||||||
shell_name="$(syntax_shell_for "$file")"
|
|
||||||
case "$shell_name" in
|
|
||||||
sh)
|
|
||||||
sh -n "$file"
|
|
||||||
;;
|
|
||||||
bash)
|
|
||||||
bash -n "$file"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unsupported shell for syntax check: $shell_name"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
format=false
|
|
||||||
strict=false
|
|
||||||
all=false
|
|
||||||
requested_files=()
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--all)
|
|
||||||
all=true
|
|
||||||
;;
|
|
||||||
--format)
|
|
||||||
format=true
|
|
||||||
;;
|
|
||||||
--strict)
|
|
||||||
strict=true
|
|
||||||
;;
|
|
||||||
-h | --help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
--)
|
|
||||||
shift
|
|
||||||
requested_files+=("$@")
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
-*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
requested_files+=("$1")
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
require_tool shellcheck
|
|
||||||
if [[ "$format" == true ]]; then
|
|
||||||
require_tool shfmt
|
|
||||||
fi
|
|
||||||
|
|
||||||
files=()
|
|
||||||
if [[ "${#requested_files[@]}" -gt 0 ]]; then
|
|
||||||
collect_requested_shell_files "${requested_files[@]}"
|
|
||||||
elif [[ "$all" == true ]]; then
|
|
||||||
collect_all_shell_files
|
|
||||||
else
|
|
||||||
collect_changed_shell_files
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${#files[@]}" -eq 0 ]]; then
|
|
||||||
echo "No shell files found."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$format" == true ]]; then
|
|
||||||
echo "Formatting ${#files[@]} shell files"
|
|
||||||
shfmt_args=(-i 2 -ci -bn)
|
|
||||||
shfmt "${shfmt_args[@]}" -w "${files[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Linting ${#files[@]} shell files"
|
|
||||||
|
|
||||||
shellcheck_args=(--severity=warning --external-sources --source-path=SCRIPTDIR)
|
|
||||||
if [[ "$strict" == true ]]; then
|
|
||||||
shellcheck_args+=("--enable=check-extra-masked-returns,check-set-e-suppressed,quote-safe-variables,deprecate-which,avoid-nullary-conditions")
|
|
||||||
fi
|
|
||||||
|
|
||||||
shellcheck "${shellcheck_args[@]}" "${files[@]}"
|
|
||||||
run_syntax_checks "${files[@]}"
|
|
||||||
@@ -123,6 +123,16 @@ git branch -d <feature-branch>
|
|||||||
```bash
|
```bash
|
||||||
# Push branch
|
# Push branch
|
||||||
git push -u origin <feature-branch>
|
git push -u origin <feature-branch>
|
||||||
|
|
||||||
|
# Create PR
|
||||||
|
gh pr create --title "<title>" --body "$(cat <<'EOF'
|
||||||
|
## Summary
|
||||||
|
<2-3 bullets of what changed>
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
- [ ] <verification steps>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Do NOT clean up worktree** — user needs it alive to iterate on PR feedback.
|
**Do NOT clean up worktree** — user needs it alive to iterate on PR feedback.
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ If CLAUDE.md, GEMINI.md, or AGENTS.md says "don't use TDD" and a skill says "alw
|
|||||||
|
|
||||||
## Platform Adaptation
|
## Platform Adaptation
|
||||||
|
|
||||||
Skills speak in actions ("dispatch a subagent", "create a todo", "read a file") rather than naming any one runtime's tools. For per-platform tool equivalents and instructions-file conventions, see [claude-code-tools.md](references/claude-code-tools.md), [codex-tools.md](references/codex-tools.md), [copilot-tools.md](references/copilot-tools.md), [gemini-tools.md](references/gemini-tools.md), [pi-tools.md](references/pi-tools.md), and [antigravity-tools.md](references/antigravity-tools.md). Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.
|
Skills speak in actions ("dispatch a subagent", "create a todo", "read a file") rather than naming any one runtime's tools. For per-platform tool equivalents and instructions-file conventions, see [claude-code-tools.md](references/claude-code-tools.md), [codex-tools.md](references/codex-tools.md), [copilot-tools.md](references/copilot-tools.md), [gemini-tools.md](references/gemini-tools.md), and [pi-tools.md](references/pi-tools.md). Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.
|
||||||
|
|
||||||
# Using Skills
|
# Using Skills
|
||||||
|
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
# Antigravity CLI (`agy`) Tool Mapping
|
|
||||||
|
|
||||||
Skills speak in actions ("dispatch a subagent", "create a todo", "read a file"). On the Antigravity CLI (`agy`) these resolve to the tools below.
|
|
||||||
|
|
||||||
| Action skills request | Antigravity CLI equivalent |
|
|
||||||
|----------------------|----------------------|
|
|
||||||
| Read a file | `view_file` |
|
|
||||||
| Create a new file | `write_to_file` |
|
|
||||||
| Edit a file | `replace_file_content` |
|
|
||||||
| Edit a file in several places at once | `multi_replace_file_content` |
|
|
||||||
| Run a shell command | `run_command` |
|
|
||||||
| Search file contents | `grep_search` |
|
|
||||||
| Find files by name / list a directory | `list_dir` (no dedicated glob tool — combine `list_dir` with `grep_search`) |
|
|
||||||
| Fetch a URL | `read_url_content` |
|
|
||||||
| Search the web | `search_web` |
|
|
||||||
| Pose a structured question to your human partner | `ask_question` |
|
|
||||||
| Dispatch a subagent (`Subagent (general-purpose):` template) | `invoke_subagent` with a built-in `TypeName` — `self` for full-capability work, `research` for read-only (see [Subagent support](#subagent-support)) |
|
|
||||||
| Multiple parallel dispatches | Multiple entries in one `invoke_subagent` call's `Subagents` array |
|
|
||||||
| Task tracking ("create a todo", "mark complete") | a **task artifact** — `write_to_file` with `IsArtifact: true` and `ArtifactType: "task"` (see [Task tracking](#task-tracking)). **Not** `manage_task`, which manages background processes. |
|
|
||||||
|
|
||||||
## Invoking a skill — read its `SKILL.md`
|
|
||||||
|
|
||||||
Antigravity surfaces every installed skill's `name` + `description` to you at the
|
|
||||||
start of each session, but it has **no `Skill`/`activate_skill` tool**. To load a
|
|
||||||
skill, **read its `SKILL.md` with `view_file`, setting `IsSkillFile: true`** when
|
|
||||||
the skill applies — e.g. `view_file` on
|
|
||||||
`.../plugins/superpowers/skills/<skill-name>/SKILL.md` with `IsSkillFile: true`.
|
|
||||||
(`IsSkillFile` is agy's own signal that you're reading a file to *execute its
|
|
||||||
instructions*, not to edit or preview it — set it whenever you load a skill.)
|
|
||||||
|
|
||||||
This is the blessed skill-loading mechanism on this harness. The general rule
|
|
||||||
"never read skill files manually" means "don't bypass your platform's
|
|
||||||
skill-loading mechanism" — and on Antigravity, reading `SKILL.md` *is* that
|
|
||||||
mechanism. Reading it honors the rule rather than breaking it.
|
|
||||||
|
|
||||||
You already know which skills exist and what they're for: their names and
|
|
||||||
descriptions are in front of you at session start. When a description matches
|
|
||||||
what you're about to do, read that skill's `SKILL.md` before acting.
|
|
||||||
|
|
||||||
## Subagent support
|
|
||||||
|
|
||||||
Antigravity dispatches subagents with `invoke_subagent`, passing each one a
|
|
||||||
`TypeName` in the `Subagents` array. Two `TypeName`s are **built in** — use them
|
|
||||||
directly, no `define_subagent` needed:
|
|
||||||
|
|
||||||
- **`self`** — a full clone of you, with every tool you have (including
|
|
||||||
`write_to_file`/`replace_file_content`/`run_command`). The safe default for
|
|
||||||
general-purpose work: implementing, fixing, anything that edits files or runs
|
|
||||||
commands.
|
|
||||||
- **`research`** — read-only (file reading, `grep_search`, web/URL fetch; no write
|
|
||||||
or command access). Use it when you specifically want a subagent that can't make
|
|
||||||
changes — investigation and read-only review.
|
|
||||||
|
|
||||||
Call `define_subagent` only for a custom system prompt or capability mix: set
|
|
||||||
`enable_write_tools: true` to grant file edits **and** `run_command`,
|
|
||||||
`enable_subagent_tools` for nested dispatch, `enable_mcp_tools` for MCP. Then
|
|
||||||
invoke it by the name you gave it. (`manage_subagents` lists/kills running
|
|
||||||
subagents.)
|
|
||||||
|
|
||||||
Skills dispatch with `Subagent (general-purpose):` and either reference a
|
|
||||||
prompt-template file (e.g. `superpowers:subagent-driven-development`'s
|
|
||||||
`./implementer-prompt.md`) or supply an inline prompt. On Antigravity:
|
|
||||||
|
|
||||||
| Skill dispatch form | Antigravity equivalent |
|
|
||||||
|---------------------|----------------------|
|
|
||||||
| An implementer-style `*-prompt.md` template (writes code, runs tests) | Fill the template, then `invoke_subagent` with `TypeName: "self"` and the filled prompt |
|
|
||||||
| A read-only reviewer template (`spec-reviewer`, `code-quality-reviewer`, `code-reviewer`, `requesting-code-review`'s `./code-reviewer.md`) | `invoke_subagent` with `TypeName: "research"` and the filled review template |
|
|
||||||
| Inline prompt (no template referenced) | `invoke_subagent` with `TypeName: "self"` (or `"research"` if the task only reads) and your inline prompt |
|
|
||||||
|
|
||||||
### Prompt filling
|
|
||||||
|
|
||||||
Skills provide prompt templates with placeholders like `{WHAT_WAS_IMPLEMENTED}` or
|
|
||||||
`[FULL TEXT of task]`. Fill all placeholders before passing the complete prompt to
|
|
||||||
`invoke_subagent`. The prompt template itself contains the agent's role, review
|
|
||||||
criteria, and expected output format — the subagent will follow it.
|
|
||||||
|
|
||||||
### Parallel dispatch
|
|
||||||
|
|
||||||
Put multiple entries in a single `invoke_subagent` call's `Subagents` array to run
|
|
||||||
independent subagent work in parallel. Keep dependent tasks sequential, but do not
|
|
||||||
serialize independent subagent tasks just to preserve a simpler history.
|
|
||||||
|
|
||||||
## Task tracking
|
|
||||||
|
|
||||||
Antigravity has **no todo / `TodoWrite` tool** (`manage_task` manages background
|
|
||||||
processes — `list`/`kill`/`status`/`send_input` — it is *not* a checklist). When a
|
|
||||||
skill says to create a todo list or track tasks, maintain a **task artifact**: a
|
|
||||||
markdown checklist saved with `write_to_file` (`IsArtifact: true`,
|
|
||||||
`ArtifactMetadata.ArtifactType: "task"`), edited with `replace_file_content` /
|
|
||||||
`multi_replace_file_content` as you go.
|
|
||||||
|
|
||||||
At the start of any multi-step task, create the task artifact listing every step of
|
|
||||||
your plan. As you complete each step, edit the artifact to mark it done (`- [x]`).
|
|
||||||
If the plan changes, update the checklist. Keep it current — it is your source of
|
|
||||||
truth for what remains; once the conversation gets long, re-read it before starting
|
|
||||||
each step.
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Run all Antigravity (agy) integration tests.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
|
|
||||||
echo "=== Antigravity integration tests ==="
|
|
||||||
|
|
||||||
for t in "$SCRIPT_DIR"/test-*.sh; do
|
|
||||||
echo
|
|
||||||
echo ">>> $t"
|
|
||||||
bash "$t"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "=== All Antigravity tests passed ==="
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Validate the Antigravity (agy) integration. agy installs the existing plugin
|
|
||||||
# directly (`agy plugin install <repo-url>`): it loads the bundled skills and
|
|
||||||
# runs the SessionStart hook for bootstrap, so there is no agy-specific scaffold
|
|
||||||
# to test. What IS agy-specific is the tool mapping — agy has no `Skill` tool and
|
|
||||||
# loads skills by reading SKILL.md with view_file — and SKILL.md pointing at it.
|
|
||||||
#
|
|
||||||
# Mirrors tests/pi/test-pi-extension.mjs's "tools reference documents
|
|
||||||
# harness-specific mappings" check. CI-safe: does not require `agy` installed.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
||||||
|
|
||||||
MAPPING="$REPO_ROOT/skills/using-superpowers/references/antigravity-tools.md"
|
|
||||||
SKILL="$REPO_ROOT/skills/using-superpowers/SKILL.md"
|
|
||||||
|
|
||||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
||||||
|
|
||||||
echo "test-antigravity-tools: checking Antigravity tool mapping"
|
|
||||||
|
|
||||||
# --- Mapping exists ---------------------------------------------------------
|
|
||||||
[ -f "$MAPPING" ] || fail "tool mapping missing at $MAPPING"
|
|
||||||
|
|
||||||
# --- Skill-load mechanism: view_file on SKILL.md (IsSkillFile), no Skill tool -
|
|
||||||
grep -qiE "view_file" "$MAPPING" \
|
|
||||||
|| fail "mapping does not document view_file as the file/skill-read tool"
|
|
||||||
grep -qiE "SKILL\.md" "$MAPPING" \
|
|
||||||
|| fail "mapping does not document reading SKILL.md as the skill-load path"
|
|
||||||
grep -q "IsSkillFile" "$MAPPING" \
|
|
||||||
|| fail "mapping does not document setting IsSkillFile when loading a skill"
|
|
||||||
|
|
||||||
# --- Core action→tool mappings are documented -------------------------------
|
|
||||||
for tool in write_to_file replace_file_content run_command grep_search invoke_subagent; do
|
|
||||||
grep -q "$tool" "$MAPPING" \
|
|
||||||
|| fail "mapping does not document the '$tool' tool"
|
|
||||||
done
|
|
||||||
|
|
||||||
# --- Subagents use the built-in self/research types -------------------------
|
|
||||||
grep -q '`self`' "$MAPPING" \
|
|
||||||
|| fail "mapping does not document the built-in 'self' subagent type"
|
|
||||||
grep -q '`research`' "$MAPPING" \
|
|
||||||
|| fail "mapping does not document the built-in 'research' subagent type"
|
|
||||||
|
|
||||||
# --- Task tracking documents the 'task' artifact mechanism ------------------
|
|
||||||
grep -qE 'ArtifactType.*task|task. artifact' "$MAPPING" \
|
|
||||||
|| fail "mapping does not document task tracking as a 'task' artifact"
|
|
||||||
|
|
||||||
# --- SKILL.md Platform Adaptation links the mapping -------------------------
|
|
||||||
grep -q "antigravity-tools.md" "$SKILL" \
|
|
||||||
|| fail "SKILL.md Platform Adaptation does not reference antigravity-tools.md"
|
|
||||||
|
|
||||||
echo "PASS: Antigravity tool mapping valid (view_file skill-load, agy tools, SKILL.md link)"
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
||||||
SCRIPT_UNDER_TEST="$REPO_ROOT/scripts/lint-shell.sh"
|
|
||||||
|
|
||||||
FAILURES=0
|
|
||||||
TEST_ROOT="$(mktemp -d)"
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$TEST_ROOT"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
pass() {
|
|
||||||
echo " [PASS] $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo " [FAIL] $1"
|
|
||||||
FAILURES=$((FAILURES + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_contains() {
|
|
||||||
local haystack="$1"
|
|
||||||
local needle="$2"
|
|
||||||
local description="$3"
|
|
||||||
|
|
||||||
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
|
|
||||||
pass "$description"
|
|
||||||
else
|
|
||||||
fail "$description"
|
|
||||||
echo " expected to find: $needle"
|
|
||||||
echo " in:"
|
|
||||||
printf '%s\n' "$haystack" | sed 's/^/ /'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_not_contains() {
|
|
||||||
local haystack="$1"
|
|
||||||
local needle="$2"
|
|
||||||
local description="$3"
|
|
||||||
|
|
||||||
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
|
|
||||||
fail "$description"
|
|
||||||
echo " did not expect to find: $needle"
|
|
||||||
echo " in:"
|
|
||||||
printf '%s\n' "$haystack" | sed 's/^/ /'
|
|
||||||
else
|
|
||||||
pass "$description"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
configure_git_identity() {
|
|
||||||
local repo="$1"
|
|
||||||
|
|
||||||
git -C "$repo" config user.name "Test Bot"
|
|
||||||
git -C "$repo" config user.email "test@example.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
write_stub_tool() {
|
|
||||||
local path="$1"
|
|
||||||
local name="$2"
|
|
||||||
|
|
||||||
cat >"$path" <<EOF
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
{
|
|
||||||
printf '${name}:'
|
|
||||||
for arg in "\$@"; do
|
|
||||||
printf ' <%s>' "\$arg"
|
|
||||||
done
|
|
||||||
printf '\n'
|
|
||||||
} >> "\$SUPERPOWERS_SHELL_LINT_TEST_LOG"
|
|
||||||
exit 0
|
|
||||||
EOF
|
|
||||||
chmod +x "$path"
|
|
||||||
}
|
|
||||||
|
|
||||||
make_fixture_repo() {
|
|
||||||
local repo="$1"
|
|
||||||
|
|
||||||
git init -q -b main "$repo"
|
|
||||||
configure_git_identity "$repo"
|
|
||||||
|
|
||||||
mkdir -p "$repo/hooks"
|
|
||||||
cat >"$repo/tracked.sh" <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
echo "tracked"
|
|
||||||
EOF
|
|
||||||
cat >"$repo/hooks/session-start" <<'EOF'
|
|
||||||
#!/bin/sh
|
|
||||||
echo "extensionless"
|
|
||||||
EOF
|
|
||||||
cat >"$repo/README.md" <<'EOF'
|
|
||||||
# Fixture
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "not a shell script"
|
|
||||||
```
|
|
||||||
EOF
|
|
||||||
cat >"$repo/untracked.sh" <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
echo "untracked"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
git -C "$repo" add tracked.sh hooks/session-start README.md
|
|
||||||
git -C "$repo" commit -q -m "fixture"
|
|
||||||
|
|
||||||
printf '\necho "changed"\n' >>"$repo/tracked.sh"
|
|
||||||
printf '\necho "changed extensionless"\n' >>"$repo/hooks/session-start"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_lint_shell() {
|
|
||||||
local repo="$1"
|
|
||||||
local fakebin="$2"
|
|
||||||
local log="$3"
|
|
||||||
shift 3
|
|
||||||
|
|
||||||
(
|
|
||||||
cd "$repo"
|
|
||||||
PATH="$fakebin:$PATH" \
|
|
||||||
SUPERPOWERS_SHELL_LINT_TEST_LOG="$log" \
|
|
||||||
bash "$SCRIPT_UNDER_TEST" "$@"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Shell lint script tests"
|
|
||||||
|
|
||||||
fixture="$TEST_ROOT/repo"
|
|
||||||
fakebin="$TEST_ROOT/bin"
|
|
||||||
log="$TEST_ROOT/tool.log"
|
|
||||||
mkdir -p "$fixture" "$fakebin"
|
|
||||||
: >"$log"
|
|
||||||
write_stub_tool "$fakebin/shellcheck" "shellcheck"
|
|
||||||
write_stub_tool "$fakebin/shfmt" "shfmt"
|
|
||||||
make_fixture_repo "$fixture"
|
|
||||||
|
|
||||||
if output="$(run_lint_shell "$fixture" "$fakebin" "$log" 2>&1)"; then
|
|
||||||
pass "lint-shell check mode exits successfully with stub tools"
|
|
||||||
else
|
|
||||||
fail "lint-shell check mode exits successfully with stub tools"
|
|
||||||
printf '%s\n' "$output" | sed 's/^/ /'
|
|
||||||
fi
|
|
||||||
|
|
||||||
tool_log="$(cat "$log")"
|
|
||||||
assert_contains "$output" "Linting 3 shell files" "reports changed shell file count"
|
|
||||||
assert_not_contains "$tool_log" "shfmt:" "does not run shfmt in lint mode"
|
|
||||||
assert_contains "$tool_log" "shellcheck:" "runs ShellCheck"
|
|
||||||
assert_contains "$tool_log" "<--severity=warning>" "uses warning severity as the baseline"
|
|
||||||
assert_contains "$tool_log" "<--external-sources>" "allows ShellCheck to follow sourced files"
|
|
||||||
assert_contains "$tool_log" "<--source-path=SCRIPTDIR>" "resolves ShellCheck sources relative to each script"
|
|
||||||
assert_contains "$tool_log" "<hooks/session-start>" "includes changed extensionless shell shebang file"
|
|
||||||
assert_contains "$tool_log" "<tracked.sh>" "includes changed tracked .sh file"
|
|
||||||
assert_contains "$tool_log" "<untracked.sh>" "includes untracked shell files by default"
|
|
||||||
assert_not_contains "$tool_log" "README.md" "ignores Markdown with shell snippets"
|
|
||||||
|
|
||||||
: >"$log"
|
|
||||||
if output="$(run_lint_shell "$fixture" "$fakebin" "$log" --all --format 2>&1)"; then
|
|
||||||
pass "lint-shell --format exits successfully with stub tools"
|
|
||||||
else
|
|
||||||
fail "lint-shell --format exits successfully with stub tools"
|
|
||||||
printf '%s\n' "$output" | sed 's/^/ /'
|
|
||||||
fi
|
|
||||||
|
|
||||||
tool_log="$(cat "$log")"
|
|
||||||
assert_contains "$tool_log" "<-w>" "uses shfmt write mode with --format"
|
|
||||||
assert_contains "$tool_log" "shellcheck:" "runs ShellCheck after --format"
|
|
||||||
assert_contains "$tool_log" "<--severity=warning>" "keeps warning severity after --format"
|
|
||||||
assert_contains "$tool_log" "<hooks/session-start>" "--all includes tracked extensionless shell shebang file"
|
|
||||||
assert_contains "$tool_log" "<tracked.sh>" "--all includes tracked .sh file"
|
|
||||||
assert_not_contains "$tool_log" "untracked.sh" "--all ignores untracked shell files"
|
|
||||||
|
|
||||||
if [[ "$FAILURES" -eq 0 ]]; then
|
|
||||||
echo "All shell lint script tests passed"
|
|
||||||
else
|
|
||||||
echo "$FAILURES shell lint script test(s) failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
Reference in New Issue
Block a user