Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .agents/skills/shell-codestyle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ name: shell-codestyle
description: >-
Governs Bash/shell script style for ptr727/ProjectTemplate fleet repos: when a bootstrap or
host-tool script may be shell instead of Python, the mandatory set -Eeuo pipefail header, the
pipefail-versus-early-reader pitfall, self-locating scripts, shellcheck cleanliness, and the
why-not-what comment rule. Use this whenever writing, reviewing, or editing a .sh file, whenever
pipefail-versus-early-reader pitfall, self-locating scripts, the shellcheck-plus-shfmt
clean-compile, and the why-not-what comment rule. Use this whenever writing, reviewing, or
editing a shell script (a `.sh` file, or an extensionless bash/sh shebang script), whenever
deciding whether a new script should be Bash or Python, or whenever a pipeline built from
`curl`/`grep`/`jq`-style commands looks like it silently swallowed a failure. Triggers even when
the task looks like a one-line tweak to an existing script, because a missing `-e`/`pipefail`,
Expand All @@ -31,6 +32,12 @@ depend on one. Everything else is Python, with a test under the scripts tree's `

## Rules

- **`shellcheck` is the linter and `shfmt` the formatter.** The clean-compile is `shellcheck`
clean at default severity plus `shfmt -d`, both reporting nothing before a commit. CI enforces
both, per `GOVERNANCE.md` "Running the Linters Locally", and `scripts/docker_lint.py` runs the
same pair headless. Neither is scoped to the `*.sh` glob alone: a tracked, extension-less
script whose shebang names bash or sh (the shape a script meant to run as a bare command takes)
joins the target list too.
- **`set -Eeuo pipefail`, before the first command the script runs.** A header comment sits above
it, as `repo-config/configure.sh` and the `host-setup/` scripts do, since what matters is that
nothing executes unguarded rather than which line number it lands on. Without `-e` a failed
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/fleet-skills/.source-digest
Original file line number Diff line number Diff line change
@@ -1 +1 @@
b8df12b7c8013f3d
85b7db1858ecd00a
11 changes: 9 additions & 2 deletions .claude-plugin/fleet-skills/skills/shell-codestyle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ name: shell-codestyle
description: >-
Governs Bash/shell script style for ptr727/ProjectTemplate fleet repos: when a bootstrap or
host-tool script may be shell instead of Python, the mandatory set -Eeuo pipefail header, the
pipefail-versus-early-reader pitfall, self-locating scripts, shellcheck cleanliness, and the
why-not-what comment rule. Use this whenever writing, reviewing, or editing a .sh file, whenever
pipefail-versus-early-reader pitfall, self-locating scripts, the shellcheck-plus-shfmt
clean-compile, and the why-not-what comment rule. Use this whenever writing, reviewing, or
editing a shell script (a `.sh` file, or an extensionless bash/sh shebang script), whenever
deciding whether a new script should be Bash or Python, or whenever a pipeline built from
`curl`/`grep`/`jq`-style commands looks like it silently swallowed a failure. Triggers even when
the task looks like a one-line tweak to an existing script, because a missing `-e`/`pipefail`,
Expand All @@ -31,6 +32,12 @@ depend on one. Everything else is Python, with a test under the scripts tree's `

## Rules

- **`shellcheck` is the linter and `shfmt` the formatter.** The clean-compile is `shellcheck`
clean at default severity plus `shfmt -d`, both reporting nothing before a commit. CI enforces
both, per `GOVERNANCE.md` "Running the Linters Locally", and `scripts/docker_lint.py` runs the
same pair headless. Neither is scoped to the `*.sh` glob alone: a tracked, extension-less
script whose shebang names bash or sh (the shape a script meant to run as a bare command takes)
joins the target list too.
- **`set -Eeuo pipefail`, before the first command the script runs.** A header comment sits above
it, as `repo-config/configure.sh` and the `host-setup/` scripts do, since what matters is that
nothing executes unguarded rather than which line number it lands on. Without `-e` a failed
Expand Down
11 changes: 9 additions & 2 deletions .github/skills/shell-codestyle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ name: shell-codestyle
description: >-
Governs Bash/shell script style for ptr727/ProjectTemplate fleet repos: when a bootstrap or
host-tool script may be shell instead of Python, the mandatory set -Eeuo pipefail header, the
pipefail-versus-early-reader pitfall, self-locating scripts, shellcheck cleanliness, and the
why-not-what comment rule. Use this whenever writing, reviewing, or editing a .sh file, whenever
pipefail-versus-early-reader pitfall, self-locating scripts, the shellcheck-plus-shfmt
clean-compile, and the why-not-what comment rule. Use this whenever writing, reviewing, or
editing a shell script (a `.sh` file, or an extensionless bash/sh shebang script), whenever
deciding whether a new script should be Bash or Python, or whenever a pipeline built from
`curl`/`grep`/`jq`-style commands looks like it silently swallowed a failure. Triggers even when
the task looks like a one-line tweak to an existing script, because a missing `-e`/`pipefail`,
Expand All @@ -31,6 +32,12 @@ depend on one. Everything else is Python, with a test under the scripts tree's `

## Rules

- **`shellcheck` is the linter and `shfmt` the formatter.** The clean-compile is `shellcheck`
clean at default severity plus `shfmt -d`, both reporting nothing before a commit. CI enforces
both, per `GOVERNANCE.md` "Running the Linters Locally", and `scripts/docker_lint.py` runs the
same pair headless. Neither is scoped to the `*.sh` glob alone: a tracked, extension-less
script whose shebang names bash or sh (the shape a script meant to run as a bare command takes)
joins the target list too.
- **`set -Eeuo pipefail`, before the first command the script runs.** A header comment sits above
it, as `repo-config/configure.sh` and the `host-setup/` scripts do, since what matters is that
nothing executes unguarded rather than which line number it lands on. Without `-e` a failed
Expand Down
42 changes: 40 additions & 2 deletions .github/workflows/validate-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,51 @@ jobs:
run: docker run --rm --pull=always -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest

# The file list comes from git rather than a glob, and the docker run is skipped rather than invoked on an empty argument list.
# That is how a repo with no shell scripts stays clean instead of failing on shellcheck's own no-file usage error.
# That is how a repo with no shell scripts stays clean instead of failing on a linter's own no-file usage error.
# A `*.sh` glob alone misses a bare-command script with no extension, so a shebang-matched file joins the list too.
- name: Check shell scripts step
run: |
set -Eeuo pipefail
# Tokenizes rather than substring-matches, so `#!/usr/bin/python bash` is not mistaken for a shell script.
# `docker_lint.py`'s shell_shebang_interpreter is the same logic; keep both in sync on a change here.
is_shell_shebang() {
local line="$1"
[[ "$line" == '#!'* ]] || return 1
local -a tokens
read -ra tokens <<< "${line#\#!}"
[ "${#tokens[@]}" -gt 0 ] || return 1
local interpreter="${tokens[0]##*/}"
if [[ "$interpreter" == "bash" || "$interpreter" == "sh" ]]; then
return 0
fi
[[ "$interpreter" == "env" ]] || return 1
local -a args=("${tokens[@]:1}")
while [ "${#args[@]}" -gt 0 ] && [[ "${args[0]}" == -* ]] && [[ "${args[0]}" != "--" ]]; do
if [[ "${args[0]}" == "-S" ]]; then
args=("${args[@]:1}")
break
fi
args=("${args[@]:1}")
done
if [ "${#args[@]}" -gt 0 ] && [[ "${args[0]}" == "--" ]]; then
args=("${args[@]:1}")
fi
[ "${#args[@]}" -gt 0 ] || return 1
local cmd="${args[0]##*/}"
[[ "$cmd" == "bash" || "$cmd" == "sh" ]]
}
mapfile -t scripts < <(git ls-files '*.sh')
mapfile -d '' -t candidates < <(git ls-files -z)
for file in "${candidates[@]}"; do
base="${file##*/}"
if [[ "$base" != *.* ]] && [ -f "$file" ] && IFS= read -r first_line < "$file" \
&& is_shell_shebang "$first_line"; then
scripts+=("$file")
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
fi
done
if [ "${#scripts[@]}" -gt 0 ]; then
docker run --rm --pull=always -v "$PWD":/mnt --workdir /mnt koalaman/shellcheck:stable "${scripts[@]}"
docker run --rm --pull=always -v "$PWD":/mnt --workdir /mnt koalaman/shellcheck:stable -- "${scripts[@]}"
docker run --rm --pull=always -v "$PWD":/mnt --workdir /mnt mvdan/shfmt:latest -d -- "${scripts[@]}"
else
echo "no shell scripts are tracked"
fi
Expand Down
2 changes: 1 addition & 1 deletion CODESTYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ This is packaged as the `python-codestyle` Skill at `.agents/skills/python-codes

## Shell

Bash, and only where a program cannot be Python: a bootstrap that installs the interpreter cannot be written in it, and a host tool that must run before a development toolchain exists cannot depend on one. Everything else is Python, with a test under the scripts tree's `tests/` directory. The mandatory `set -Eeuo pipefail` header, the pipefail-versus-early-reader pitfall, self-locating scripts, `shellcheck` cleanliness, and the why-not-what comment rule are packaged as the `shell-codestyle` Skill at `.agents/skills/shell-codestyle/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. Read the skill for the full rules.
Bash, and only where a program cannot be Python: a bootstrap that installs the interpreter cannot be written in it, and a host tool that must run before a development toolchain exists cannot depend on one. Everything else is Python, with a test under the scripts tree's `tests/` directory. The mandatory `set -Eeuo pipefail` header, the pipefail-versus-early-reader pitfall, self-locating scripts, the `shellcheck`-plus-`shfmt` clean-compile, and the why-not-what comment rule are packaged as the `shell-codestyle` Skill at `.agents/skills/shell-codestyle/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo. Read the skill for the full rules.

<!-- Repo -->

Expand Down
4 changes: 2 additions & 2 deletions GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i

**Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`):

- **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** via Docker `:latest` (its action only installs the CLI, so the Docker one-liner is what actually runs the check), **shellcheck** the same way for a repo that carries `.sh` files, and, **for a repo that carries `.ps1` files**, **PSScriptAnalyzer** the same way (neither one has an action). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below. This whole block is the hub's `validate-task.yml` reusable workflow, so a fleet repo reaches it rather than carrying a copy of these steps.
- **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** via Docker `:latest` (its action only installs the CLI, so the Docker one-liner is what actually runs the check), **shellcheck** and **shfmt** the same way for a repo that carries a shell script, and, **for a repo that carries `.ps1` files**, **PSScriptAnalyzer** the same way (none of the three has an action). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below. A "shell script" here is a tracked `.sh` file, plus a tracked, extension-less file whose shebang names bash or sh, the shape a script meant to run as a bare command takes: a bare `*.sh` glob misses that second case, so both discovery paths run before either tool does. This whole block is the hub's `validate-task.yml` reusable workflow, so a fleet repo reaches it rather than carrying a copy of these steps.
- **The `.husky/pre-commit` hook** runs **language formatting** and the **diff-scoped doc gates**, never Docker and never a network call, so it stays fast. The formatting half is whatever the repo's own language needs, CSharpier and `dotnet format` for .NET or ruff for Python, via native tooling. A repo adds each half once its tree passes that half, since a gate that fails on the corpus it guards blocks every commit from the moment it lands, so a hook running one half is a repo mid-convergence rather than a repo out of conformance. The doc half runs each gate at the scope that fits it. The prose gate is scoped to what the commit changes rather than swept over the tree, which is the difference between about 2.2 seconds and about 0.13 and is what makes it affordable in a hook at all. A whole-repo check belongs there too when it is already fast and takes no file list, which the line-ending consistency check is, so scope is a property of the gate rather than a rule the hook applies to all of them. `repo_gate.py --check sha-pin` stays out, since it resolves a same-owner pin against the GitHub API and a hook that needs a network fails offline. A repo enables the hook per clone with `git config core.hooksPath .husky`, and CI remains the authoritative run either way.
- **The VS Code Lint tasks** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks.

Expand All @@ -242,7 +242,7 @@ After all pulls, the wrapper reports that repository mounts are about to begin.

Every primary Docker command has a five-minute timeout by default. Use `--timeout` to select another positive bound. The wrapper emits a start and completion line for each primary command. Timeout cleanup has a separate maximum of 30 seconds and emits its result through the failed lint step. The wrapper reports the checked-file count for every linter, including tools that produce no success output. Timeout, container failure, zero-target execution, and successful quiet completion have distinct result lines. The wrapper names each lint container and removes it after a timeout.

Use repeated `--linter` options for a subset. The supported names are `editorconfig-checker`, `actionlint`, `markdownlint`, `cspell`, `shellcheck`, and `PSScriptAnalyzer`. editorconfig-checker reads the mounted tree. actionlint reads eligible workflows and includes shellcheck for `run:` blocks. markdownlint reads tracked and unignored Markdown files. CSpell reads `README.md` and `HISTORY.md` only. shellcheck and PSScriptAnalyzer run only when matching scripts are tracked or unignored.
Use repeated `--linter` options for a subset. The supported names are `editorconfig-checker`, `actionlint`, `markdownlint`, `cspell`, `shellcheck`, `shfmt`, and `PSScriptAnalyzer`. editorconfig-checker reads the mounted tree. actionlint reads eligible workflows and includes shellcheck for `run:` blocks. markdownlint reads tracked and unignored Markdown files. CSpell reads `README.md` and `HISTORY.md` only. shellcheck, shfmt, and PSScriptAnalyzer run only when matching scripts are tracked or unignored. shellcheck and shfmt share one target list, a tracked `.sh` file plus a tracked, extension-less file whose shebang names bash or sh.

In a configured editor the `DavidAnson.vscode-markdownlint` extension is enough for Markdown. Use the wrapper for a headless run or before pushing.

Expand Down
1 change: 1 addition & 0 deletions catalog/snippets/husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/sh
# shellcheck disable=SC1091 # Generated by Husky install, not present at lint time.
. "$(dirname "$0")/_/husky.sh"

# Local pre-commit: language formatting and style only (no Docker).
Expand Down
12 changes: 8 additions & 4 deletions host-setup/agent-safety/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# The installer and the hook use Python 3 syntax, so a bare `python` that is Python 2 is rejected rather than handed the script, which would fail on import.
py=""
for c in python3 python; do
if command -v "$c" >/dev/null 2>&1 && "$c" -c 'import sys; raise SystemExit(0 if sys.version_info[0] == 3 else 1)' 2>/dev/null; then
py="$c"; break
fi
if command -v "$c" >/dev/null 2>&1 && "$c" -c 'import sys; raise SystemExit(0 if sys.version_info[0] == 3 else 1)' 2>/dev/null; then
py="$c"
break
fi
done
[ -n "$py" ] || { echo "Python 3 is required and was not found on PATH (tried python3, python)." >&2; exit 1; }
[ -n "$py" ] || {
echo "Python 3 is required and was not found on PATH (tried python3, python)." >&2
exit 1
}

exec "$py" "$here/install.py" "$@"
Loading
Loading