From b4c97fbfb6ab95b8a648294c82c0ab1ac2886342 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Wed, 28 Oct 2026 22:59:34 +0100 Subject: [PATCH] chore(specs): tighten the agnostic-ai specs Claude startup drops from 709 to 492 words, a src/ edit loads 1,050 words of rules instead of 1,973, and the Codex root AGENTS.md drops from 4,173 to 2,505 bytes. - Gate runs make test, make test/strict, make sa, make lint, as CI does - Rename the issue skills to bashunit-gh-issue and bashunit-gh-issues - Move the module table to a fork-census reference and fix stale rows - Move return slots out of the tests/** scope into their own rule - One home each for the gate, commit types and the shfmt warning - Fix the pr labels and the fix-test fixtures path - Ask before editing acceptance snapshots - Add lint budgets; drop the session-wide model pin - Hook falls back to agnostic-ai on PATH and exits 0 without it Refs #1396 --- .agnostic-ai/AGNOSTIC_AI.md | 31 ++----- .agnostic-ai/agents/code-locator.md | 2 +- .agnostic-ai/agents/code-reviewer.md | 2 +- .agnostic-ai/rules/architecture-map.md | 81 +---------------- .agnostic-ai/rules/bash-style.md | 37 -------- .agnostic-ai/rules/return-slots.md | 40 +++++++++ .agnostic-ai/rules/tdd-workflow.md | 2 +- .agnostic-ai/rules/testing.md | 4 +- .agnostic-ai/scripts/shellcheck-after-edit.sh | 6 +- .agnostic-ai/settings/models.yaml | 1 - .../settings/protected-snapshots.yaml | 7 ++ .../{gh-issue => bashunit-gh-issue}/SKILL.md | 2 +- .../SKILL.md | 10 +-- .../skills/commit/references/formatting.md | 4 +- .agnostic-ai/skills/fix-test/SKILL.md | 2 +- .agnostic-ai/skills/fork-census/SKILL.md | 4 +- .../fork-census/references/module-map.md | 90 +++++++++++++++++++ .agnostic-ai/skills/pr/SKILL.md | 2 +- .agnostic-ai/skills/review/SKILL.md | 2 +- .gitignore | 1 + .worktreeinclude | 1 + agnostic-ai.yaml | 4 + 22 files changed, 177 insertions(+), 158 deletions(-) create mode 100644 .agnostic-ai/rules/return-slots.md delete mode 100644 .agnostic-ai/settings/models.yaml create mode 100644 .agnostic-ai/settings/protected-snapshots.yaml rename .agnostic-ai/skills/{gh-issue => bashunit-gh-issue}/SKILL.md (98%) rename .agnostic-ai/skills/{gh-issues => bashunit-gh-issues}/SKILL.md (66%) create mode 100644 .agnostic-ai/skills/fork-census/references/module-map.md diff --git a/.agnostic-ai/AGNOSTIC_AI.md b/.agnostic-ai/AGNOSTIC_AI.md index c126f03a..be14f86f 100644 --- a/.agnostic-ai/AGNOSTIC_AI.md +++ b/.agnostic-ai/AGNOSTIC_AI.md @@ -4,42 +4,29 @@ bashunit is a Bash 3.0+ testing framework with assertions, spies, mocks, data pr ## Required guidance -All paths are relative to the repository root, including from generated skill and agent directories. Before editing, read the rules for the task below; some clients do not load nested instructions after the session starts. +Paths are relative to the repository root, also from generated skill and agent directories. Codex loads a directory's `AGENTS.md` only when started inside it; before editing under `src/` or `tests/`, read that directory's `AGENTS.md`. -| Task | Read | -|------|------| -| Changes to `src/` or `bashunit` | `.agnostic-ai/rules/architecture-map.md` | -| Shell changes under `src/` or `tests/`, including entrypoint changes | `.agnostic-ai/rules/bash-style.md` | -| Changes to `src/`, `bashunit`, or fork census acceptance tests | `.agnostic-ai/rules/perf-fork-budget.md` | -| Test changes | `.agnostic-ai/rules/testing.md` | - -Bash 3.0 is the compatibility floor, including parse-time behavior. Use existing patterns. Study the relevant tests before adding assertions, doubles, providers, hooks, or CLI behavior. Do not invent commands or features. - -Use the TDD rule for runtime changes and its `tdd-cycle` skill for nontrivial plans. Preserve public APIs unless the task includes documentation and CHANGELOG updates; record architectural decisions in an ADR. +Bash 3.0 is the compatibility floor, including parse-time behavior. Follow existing patterns; study the matching tests before adding behavior. Do not invent commands or features. Preserve public APIs unless the task includes docs and CHANGELOG updates; record architectural decisions in an ADR. ## Quality gate -The main session runs this gate once after the complete change set, before committing. Targeted checks support RED/GREEN or diagnosis; reuse passing checks while content is unchanged: +The main session runs this gate once after the complete change set, before committing. Reuse passing checks while content is unchanged: ```bash -./bashunit tests/ -./bashunit --parallel tests/ +make test +make test/strict make sa make lint ``` -`make lint` is the formatting authority. Match surrounding 2-space indentation by hand. Never run `shfmt -w`; it conflicts with EditorConfig. The `commit` skill bundles the detailed formatting constraints and optional drift comparison. +`make test/strict` runs what CI's strict job runs. `make lint` is the formatting authority. ## Delegation -Handle small edits directly. Keep one acceptance scenario and a short plan in the main session. Delegate independent lookups to `code-locator`, bounded edits to `code-implementer`, and verified defect review to `code-reviewer` before pushing. Assign a question or file scope, a base revision, and a validation budget. Reuse findings. Keep at most two delegates active. - -Workers share the checkout: assign separate files, preserve others' edits, and run only assigned targeted checks. The coordinator runs the full gate. Return concise file and line evidence; save longer reports under ignored `.agnostic-ai/local/`. +Handle small edits directly. Delegate lookups to `code-locator`, bounded edits to `code-implementer`, and review to `code-reviewer` before pushing, each with a scope, a base revision, and a validation budget. Keep at most two delegates active, on separate files; they run only assigned targeted checks. Save long reports under ignored `.agnostic-ai/local/`. ## Specs and delivery -Edit `.agnostic-ai/` or `agnostic-ai.yaml`, never generated instructions, agents, skills, hooks, or settings. Install the pinned contributor dependencies with `npm ci` (use `npm ci --force` for a Windows source checkout because the published npm package restricts its OS). Run `npm run agents:sync` and `npm run agents:check`, which use the project-local CLI. - -Keep unrelated changes out of the delivery. Use a PR unless the user requests a direct commit to main. Update CHANGELOG for user-visible changes. Use the `pr` skill for pull requests. Use Conventional Commits: `feat`, `fix`, `perf`, `docs`, `style`, `refactor`, `test`, or `chore`, with an optional domain scope such as `assert`, `runner`, `cli`, or `docs`. +Edit `.agnostic-ai/` or `agnostic-ai.yaml`, never generated files. Install the pinned CLI with `npm ci` (`npm ci --force` on a Windows source checkout), then run `npm run agents:sync` and `npm run agents:check`. -Never commit secrets, skip Git hooks, amend published commits, or use destructive Git commands. Pushing requires user authorization; force-pushing main requires an explicit request. All local gates must pass before committing. +Keep unrelated changes out. Use a PR unless the user asks for a direct commit to main. Update CHANGELOG for user-visible changes. Use the `commit` and `pr` skills. Never commit secrets, skip Git hooks, amend published commits, or run destructive Git commands. Pushing needs user authorization; force-pushing main needs an explicit request. diff --git a/.agnostic-ai/agents/code-locator.md b/.agnostic-ai/agents/code-locator.md index 3e9d8f43..bfcc136d 100644 --- a/.agnostic-ai/agents/code-locator.md +++ b/.agnostic-ai/agents/code-locator.md @@ -5,4 +5,4 @@ model: fast can: [read, shell] --- -Find code without editing it or proposing a redesign. Use rg, then read only enough surrounding code to verify each match. Return a file:line table and the searches used when nothing matches. Do not run tests, builds, installs, or Git mutations. +Find code without editing it or proposing a redesign. Use rg, then read only enough surrounding code to verify each match. Return a file:line table and the searches used when nothing matches. Do not run tests, builds, installs, or Git mutations. For module ownership under `src/`, start from `.agnostic-ai/skills/fork-census/references/module-map.md`. diff --git a/.agnostic-ai/agents/code-reviewer.md b/.agnostic-ai/agents/code-reviewer.md index 7372fa31..4db318a6 100644 --- a/.agnostic-ai/agents/code-reviewer.md +++ b/.agnostic-ai/agents/code-reviewer.md @@ -5,4 +5,4 @@ model: strong can: [read, shell] --- -Review without editing files, committing, or posting to GitHub. Read the diff against the supplied base and the applicable source rules listed in .agnostic-ai/AGNOSTIC_AI.md. Trace changed behavior through callers and existing tests. Check Bash 3.0 parsing, dynamic scope, subshell state, parallel isolation, cleanup, and hot-path process costs when relevant. For configuration changes, check clean-clone generation, canonical references, model resolution, and preserved native behavior. Do not run the full quality gate. Return only verified findings as file:line, severity, concrete failing input, and the smallest fix. If none remain, say so and list the scope checked. +Review without editing files, committing, or posting to GitHub. Read the diff against the supplied base and the rules under .agnostic-ai/rules/ that match the changed paths. Trace changed behavior through callers and existing tests. Check Bash 3.0 parsing, dynamic scope, subshell state, parallel isolation, cleanup, and hot-path process costs when relevant. For configuration changes, check clean-clone generation, canonical references, model resolution, and preserved native behavior. Do not run the full quality gate. Return only verified findings as file:line, severity, concrete failing input, and the smallest fix. If none remain, say so and list the scope checked. diff --git a/.agnostic-ai/rules/architecture-map.md b/.agnostic-ai/rules/architecture-map.md index fe9fa32a..b9c03629 100644 --- a/.agnostic-ai/rules/architecture-map.md +++ b/.agnostic-ai/rules/architecture-map.md @@ -4,7 +4,7 @@ scope: src # Architecture map -Repository paths are relative to the root. Module paths below are under `src/` unless stated otherwise. Use function names as anchors; verify the current caller before changing a path. +Repository paths are relative to the root. Module paths below are under `src/` unless stated otherwise. Use function names as anchors; verify the current caller before changing a path. The per-module ownership table is in `.agnostic-ai/skills/fork-census/references/module-map.md`. ## Run lifecycle @@ -15,86 +15,9 @@ Repository paths are relative to the root. Module paths below are under `src/` u 5. `runner/result.sh` handles synchronous results or writes parallel `.result` files. Under `--parallel`, file dispatch also runs in a background subshell; results and explicit files carry state back. `state/parallel.sh` aggregates worker results in the parent. 6. Console and report modules render results. Rerun persistence, temporary-output cleanup and exit status complete the run. -## Module ownership - -| Module | Owns | -|--------|------| -| `bashunit` | entry: version gate, early flag scan, sources every module, subcommand routing | -| `main/index.sh` | aggregator only, sources the `src/main/` module below | -| `main/validate.sh` | shared parser validation: unknown options, integer/path checks, shard parsing | -| `main/test.sh` / `main/bench.sh` / `main/subcommands.sh` | flag parsing per subcommand | -| `main/assert.sh` | the standalone `bashunit assert` path | -| `main/watch.sh` / `main/run.sh` | the `--watch` loop; the run lifecycle, exit codes and cleanup | -| `runner/index.sh` | aggregator only, sources the `src/runner/` module below | -| `runner/context.sh` | workdir restore, test identity/location exports, title interpolation, capability probes | -| `runner/payload.sh` | the `_BASHUNIT_RUNNER_*_OUT` return slots; encode/decode of the per-test result payload | -| `runner/diagnostics.sh` | runtime-error detection, kill-signal classification, profiling, verbose/file headers | -| `runner/result.sh` | `parse_result{,_sync,_parallel}`, failure source context, failed/skipped/incomplete/risky writers | -| `runner/parallel.sh` | job-slot waiting (`wait -n` or poll), running-job count, spinner | -| `runner/hooks.sh` | set_up/tear_down (test + script scope), hook failure records, mock clearing, EXIT cleanup | -| `runner/provider.sh` | `@data_provider` argument parsing | -| `runner/exec.sh` | `run_test`, the capture-subshell body, retry, timeout watchdog, per-file dispatch, `order_functions_for_script` (the run order a seed produces) | -| `runner/list.sh` | `--list`/`--dry-run`: emits selected tests instead of running them; hooks in after all filtering, before any hook | -| `runner/discovery.sh` | `load_test_files` (the per-file loop), `functions_for_script` | -| `runner/bench.sh` | benchmark file loop and bench function dispatch | -| `helper/index.sh` | aggregator only, sources the `src/helper/` module below | -| `helper/naming.sh` | resolving and normalising test-function and variable names | -| `helper/discovery.sh` | `find_files_recursive`, `find_total_tests`, fn filtering, duplicate check, line lookup | -| `helper/provider.sh` / `helper/tags.sh` | `@data_provider` map; `@tag` extraction and matching | -| `helper/encoding.sh` / `helper/functions.sh` / `helper/git.sh` | base64 + ids; generic fn utilities; remote tag lookup for `upgrade` | -| `state/index.sh` | aggregator only, sources the `src/state/` module below | -| `state/counters.sh` | test and assertion tallies and their get/add accessors | -| `state/duplicates.sh` | duplicate test-function detection state | -| `state/context.sh` | per-test output buffer, exit code, title, hook failure, and the per-test reset | -| `state/payload.sh` | the encoded per-test result payload and the base64 capability probe | -| `state/parallel.sh` | aggregating per-test result files after a `--parallel` run | -| `config/index.sh` | aggregator only, sources the `src/config/` module below | -| `config/env.sh` | all `BASHUNIT_*` defaults/config files, scratch dirs (`_BASHUNIT_RUN_OUTPUT_DIR` + EXIT-trap cleanup); executes at source time | -| `config/rerun.sh` | the `.bashunit/last-failed` cache for `--rerun-failed` | -| `config/parallel.sh` | is this run parallel; worker temp tree and stop-on-failure flag file (not `runner/parallel.sh`, which waits on job slots) | -| `console/index.sh` | aggregator only, sources the `src/console/` module below | -| `console/colors.sh` | the `_BASHUNIT_COLOR_*` palette and `bashunit::sgr` | -| `console/header.sh` | the "Running N tests" header | -| `console/line.sh` | `print_line`, the primitive every result line goes through, and its TAP variant | -| `console/duration.sh` / `console/diff.sh` | duration formatting; unified and line-by-line diffs under a failure | -| `console/test_line.sh` | the per-test result lines (passed/failed/skipped/incomplete/snapshot/risky/error) | -| `console/deferred.sh` | end-of-run blocks buffered during the run (scratch files under the run dir) | -| `console/summary.sh` | run totals, execution time, hook completion | -| `assert/index.sh` | aggregator only, sources the `src/assert/` module below, plus `skip_todo.sh` and `test_doubles.sh` | -| `assert/core.sh` | `assert::should_skip`, `assert::fail_with`, `assert::join_to_slot` and the comparison assertions the other files build on | -| `assert/{arrays,assertions,dates,duration,files,folders,json,once,snapshot}.sh` | the per-topic assertions; the per-assertion path must stay fork-free | -| `util/index.sh` | aggregator only, sources the `src/util/` module below | -| `util/clock.sh` | time impl selection (EPOCHREALTIME > date > perl > …), return-slot reads | -| `util/str.sh` / `util/math.sh` | pure-bash string and arithmetic helpers | -| `api/index.sh` | aggregator only, sources the `src/api/` module below | -| `api/globals.sh` | `temp_file`/`temp_dir`/`current_dir`/`data_set`; repeats `set -euo pipefail` at file scope for anything sourcing it alone, the entrypoint already set it | -| `api/skip_todo.sh` / `api/test_title.sh` | `skip`/`todo` markers; `set_test_title` | -| `api/bashunit.sh` | the custom-assert facade (`assert_that`, `assertion_failed`/`passed`) | -| `doubles/index.sh` | aggregator only, sources the `src/doubles/` module below | -| `doubles/mock.sh` | `mock`/`unmock` and the `_BASHUNIT_MOCKED_FUNCTIONS` registry `runner/hooks.sh` unwinds per test | -| `doubles/spy.sh` | `spy` and its `_BASHUNIT_SPY_*` state slots + files | -| `doubles/assertions.sh` | the `assert_have_been_called*` family | -| `coverage/index.sh` | aggregator only, sources the `src/coverage/` modules below | -| `coverage/config.sh` | data-file locations, tracked-file roots, engine selection (`init` resets state owned by several modules) | -| `coverage/paths.sh` | `normalize_path`, `should_track` and the hot-path track/path caches | -| `coverage/engine.sh` | DEBUG-trap and xtrace capture, buffering, `finalize`/`cleanup`, parallel merge; only active under `--coverage` | -| `coverage/lines.sh` | executable-line classification (fork-free; the report phase's hot path) and hit-data reading | -| `coverage/stats.sh` | percentages, the precomputed per-file stats cache, threshold gate | -| `coverage/functions.sh` | function definitions and their line spans | -| `coverage/branches.sh` | branch extraction + hit computation; one file, the `_branch_*` helpers mutate `extract_branches`'s locals via dynamic scoping | -| `coverage/report_text.sh` / `report_lcov.sh` / `report_html.sh` | the three renderers | -| `coverage/html_index.sh` / `html_file.sh` | HTML page emitters; the only two files exempt from `max_line_length` | -| `reports.sh` | JUnit/HTML/TAP/JSON writers | -| `system/index.sh` | aggregator only, sources the `src/system/` module below | -| `system/check_os.sh` / `system/dependencies.sh` | one-fork OS detect; `command -v` probes (builtins, not forks) | -| `system/io.sh` | small I/O helpers | -| `cli/index.sh` | aggregator only, sources the `src/cli/` module below | -| `cli/{upgrade,watch,doc,init}.sh` | the non-`test` subcommand implementations; main subcommand routing calls them | -| `benchmark/index.sh` | the remaining non-`test` subcommand; shared with `runner/bench.sh` | - ## Cross-cutting invariants -- Follow `.agnostic-ai/rules/bash-style.md` for Bash 3.0 parsing and return slots. `local` is dynamically scoped; helper locals can shadow caller-named outputs. +- Follow `.agnostic-ai/rules/bash-style.md` for Bash 3.0 parsing and `.agnostic-ai/rules/return-slots.md` for return values. `local` is dynamically scoped; helper locals can shadow caller-named outputs. - Follow `.agnostic-ai/rules/perf-fork-budget.md` for process costs. Preserve the acceptance budgets rather than assuming every path has the same allowance. - Background file dispatch cannot return counters or flags through shell state. Put whole-file validation before dispatch or return failures through explicit results. Always test sequential and parallel modes when changing this boundary. - Use dispatcher ordinals for worker identity. Subshells share `$$`; `RANDOM` reseeding varies by platform and nesting and cannot guarantee uniqueness. diff --git a/.agnostic-ai/rules/bash-style.md b/.agnostic-ai/rules/bash-style.md index 0acc14af..905a1018 100644 --- a/.agnostic-ai/rules/bash-style.md +++ b/.agnostic-ai/rules/bash-style.md @@ -25,40 +25,3 @@ Runtime code must parse and work on Bash 3.0+. `bashunit` checks both version co `[[ =~ ]]` exists on 3.0, but regex quoting changed in 3.2. Follow existing `grep -E` helpers and the process-budget rules rather than introducing version-dependent matching. Prefer `[ ]` over `[[ ]]` for ordinary tests; that is house style, not a compatibility requirement. Use 2-space indentation, quoted variables, `$()` instead of backticks, and existing function namespaces. Public helpers use `bashunit::*`, private helpers `_name`, locals `lowercase_with_underscores`, and globals `UPPERCASE_WITH_UNDERSCORES`. Match sibling file structure and source dependencies relative to the script. `make lint` is the formatting authority; never run `shfmt -w`. Use a comment only for a constraint the code cannot express. ShellCheck suppressions need a specific reason. - -## Return values and dynamic scope - -Bash `local` is dynamically scoped. A callee's local can hide a caller variable accessed by name through `${!name}`, `eval`, `printf -v` or `export`. Hot-path `$()` captures also fork. - -Prefer one reserved global return slot per helper: - -```bash -_BASHUNIT_PKG_THING_OUT="" - -function bashunit::pkg::do_thing() { - local input=$1 - _BASHUNIT_PKG_THING_OUT=${input%%]*} -} - -bashunit::pkg::do_thing "$payload" -local thing=$_BASHUNIT_PKG_THING_OUT -``` - -Use natural local names and `_BASHUNIT_*` slots; distinct slots avoid nested-call clobbering. See `src/runner/payload.sh` and `src/coverage/branches.sh`. - -For mock or spy state, namespace the constructed variable itself, for example `_BASHUNIT_SPY_${variable}_TIMES_FILE`. See `src/doubles/spy.sh`. - -Only when a dedicated slot or namespaced state cannot fit, accept an output variable name and prefix every helper local with `__bu_`: - -```bash -function bashunit::pkg::do_thing() { - local __bu_out=$1 - local __bu_in=$2 - local __bu_val="${__bu_in%%]*}" - eval "$__bu_out=\$__bu_val" -} -``` - -Test caller names that collide with helper locals. Do not introduce unprefixed callee locals in this pattern. - -The `_branch_*` helpers in `src/coverage/branches.sh` deliberately mutate `extract_branches` locals. Keep them together (ADR-010); new intentional dynamic-scope mutation needs an explicit one-line constraint. diff --git a/.agnostic-ai/rules/return-slots.md b/.agnostic-ai/rules/return-slots.md new file mode 100644 index 00000000..daf353bb --- /dev/null +++ b/.agnostic-ai/rules/return-slots.md @@ -0,0 +1,40 @@ +--- +scope: src +--- + +# Return values and dynamic scope + +Bash `local` is dynamically scoped. A callee's local can hide a caller variable accessed by name through `${!name}`, `eval`, `printf -v` or `export`. Hot-path `$()` captures also fork. + +Prefer one reserved global return slot per helper: + +```bash +_BASHUNIT_PKG_THING_OUT="" + +function bashunit::pkg::do_thing() { + local input=$1 + _BASHUNIT_PKG_THING_OUT=${input%%]*} +} + +bashunit::pkg::do_thing "$payload" +local thing=$_BASHUNIT_PKG_THING_OUT +``` + +Use natural local names and `_BASHUNIT_*` slots; distinct slots avoid nested-call clobbering. See `src/runner/payload.sh` and `src/coverage/branches.sh`. + +For mock or spy state, namespace the constructed variable itself, for example `_BASHUNIT_SPY_${variable}_TIMES_FILE`. See `src/doubles/spy.sh`. + +Only when a dedicated slot or namespaced state cannot fit, accept an output variable name and prefix every helper local with `__bu_`: + +```bash +function bashunit::pkg::do_thing() { + local __bu_out=$1 + local __bu_in=$2 + local __bu_val="${__bu_in%%]*}" + eval "$__bu_out=\$__bu_val" +} +``` + +Test caller names that collide with helper locals. Do not introduce unprefixed callee locals in this pattern. + +The `_branch_*` helpers in `src/coverage/branches.sh` deliberately mutate `extract_branches` locals. Keep them together (ADR-010); new intentional dynamic-scope mutation needs an explicit one-line constraint. diff --git a/.agnostic-ai/rules/tdd-workflow.md b/.agnostic-ai/rules/tdd-workflow.md index fe4cf71b..fc0d8180 100644 --- a/.agnostic-ai/rules/tdd-workflow.md +++ b/.agnostic-ai/rules/tdd-workflow.md @@ -4,4 +4,4 @@ alwaysApply: true # TDD -Use test-driven development for runtime changes: prove the smallest test fails for the right reason, make it pass with minimal code, then refactor while keeping tests green. Use {{$SKILL:tdd-cycle}} for the full workflow and task-file template. Run the complete quality gate once after the change set is complete, before committing. +Use test-driven development for runtime changes: prove the smallest test fails for the right reason, make it pass with minimal code, then refactor while keeping tests green. Use {{$SKILL:tdd-cycle}} for the full workflow and task-file template. diff --git a/.agnostic-ai/rules/testing.md b/.agnostic-ai/rules/testing.md index dfd81385..d197fe91 100644 --- a/.agnostic-ai/rules/testing.md +++ b/.agnostic-ai/rules/testing.md @@ -12,7 +12,7 @@ Read sibling tests before writing a new fixture. Repository paths are relative t | Functional | `tests/functional/` | Real component interactions | | Acceptance | `tests/acceptance/` | CLI and end-to-end behavior | -Files end in `_test.sh`; name tests for the behavior and trigger. Use targeted RED/GREEN checks during implementation, then the root quality gate once the complete change set is ready. Test success, failure and cleanup paths that the change affects; verify parallel behavior when state or workers change. +Files end in `_test.sh`; name tests for the behavior and trigger. Run the quality gate from `AGNOSTIC_AI.md` once when the change set is complete. Test success, failure and cleanup paths that the change affects; verify parallel behavior when state or workers change. ## Assertions and doubles @@ -41,6 +41,6 @@ Use `assert_match_snapshot` or `assert_match_named_snapshot`; update snapshots d - Use `$(bashunit::temp_file)` or `$(bashunit::temp_dir)` for framework-cleaned fixtures. No shared fixed temporary paths or global state between tests. - Mock network calls and nondeterministic external dependencies. Tests must work under `./bashunit --parallel tests/`. - Embedded test-function definitions are scanned textually by the duplicate check, even inside strings. Give every embedded definition a unique name across its containing file; quote-state heuristics cannot safely hide them. -- Strict CI uses `set -euo pipefail`: initialize arrays, guard optional elements, use `if` for conditional side effects, and isolate `extdebug` in a subshell. Simple output contains markers, so test rendering helpers rather than assuming full `print_line` output. +- Strict CI (`make test/strict`) uses `set -euo pipefail`: initialize arrays, guard optional elements, use `if` for conditional side effects, and isolate `extdebug` in a subshell. Simple output contains markers, so test rendering helpers rather than assuming full `print_line` output. Start with `tests/unit/assert/`, `tests/functional/doubles_test.sh`, `tests/functional/provider_test.sh`, `tests/unit/runner/hooks_test.sh`, and `tests/acceptance/bashunit_test.sh`. Use `rg` to locate the matching function tests before adding a pattern. diff --git a/.agnostic-ai/scripts/shellcheck-after-edit.sh b/.agnostic-ai/scripts/shellcheck-after-edit.sh index c1e0d622..ed2f005d 100644 --- a/.agnostic-ai/scripts/shellcheck-after-edit.sh +++ b/.agnostic-ai/scripts/shellcheck-after-edit.sh @@ -6,7 +6,11 @@ if [ -n "${1:-}" ]; then fi project_root=$(git rev-parse --show-toplevel) -files=$("$project_root/node_modules/.bin/agnostic-ai" hook paths) || exit 1 +agnostic_ai="$project_root/node_modules/.bin/agnostic-ai" +if [ ! -x "$agnostic_ai" ]; then + agnostic_ai=$(command -v agnostic-ai) || exit 0 +fi +files=$("$agnostic_ai" hook paths) || exit 1 printf '%s\n' "$files" | while IFS= read -r file; do case "$file" in *.sh) diff --git a/.agnostic-ai/settings/models.yaml b/.agnostic-ai/settings/models.yaml deleted file mode 100644 index c0dc6825..00000000 --- a/.agnostic-ai/settings/models.yaml +++ /dev/null @@ -1 +0,0 @@ -model: balanced diff --git a/.agnostic-ai/settings/protected-snapshots.yaml b/.agnostic-ai/settings/protected-snapshots.yaml new file mode 100644 index 00000000..e9af8d17 --- /dev/null +++ b/.agnostic-ai/settings/protected-snapshots.yaml @@ -0,0 +1,7 @@ +name: protected-snapshots +description: Acceptance snapshots change only on purpose. +protected: + paths: + - tests/acceptance/snapshots/** + decision: ask + reason: Snapshots record rendered output. Update them with ./bashunit --snapshot-update only when the output change is intended. diff --git a/.agnostic-ai/skills/gh-issue/SKILL.md b/.agnostic-ai/skills/bashunit-gh-issue/SKILL.md similarity index 98% rename from .agnostic-ai/skills/gh-issue/SKILL.md rename to .agnostic-ai/skills/bashunit-gh-issue/SKILL.md index 6abdb0df..a282ecc9 100644 --- a/.agnostic-ai/skills/gh-issue/SKILL.md +++ b/.agnostic-ai/skills/bashunit-gh-issue/SKILL.md @@ -1,5 +1,5 @@ --- -name: gh-issue +name: bashunit-gh-issue description: Fetch GitHub issue, create branch, plan and implement with TDD, then open PR argument-hint: "" allowed-tools: shell, read, edit, write, Grep, Glob, Agent, WebFetch diff --git a/.agnostic-ai/skills/gh-issues/SKILL.md b/.agnostic-ai/skills/bashunit-gh-issues/SKILL.md similarity index 66% rename from .agnostic-ai/skills/gh-issues/SKILL.md rename to .agnostic-ai/skills/bashunit-gh-issues/SKILL.md index adbb83fe..ca97c75a 100644 --- a/.agnostic-ai/skills/gh-issues/SKILL.md +++ b/.agnostic-ai/skills/bashunit-gh-issues/SKILL.md @@ -1,6 +1,6 @@ --- -name: gh-issues -description: Work open GitHub issues that are unassigned or mine, one at a time, with gh-issue. +name: bashunit-gh-issues +description: Work open unassigned or own GitHub issues one at a time. argument-hint: "[--limit N] [--label foo] [--dry-run]" disable-model-invocation: true allowed-tools: read, shell, Skill @@ -8,7 +8,7 @@ allowed-tools: read, shell, Skill # Process an issue queue -Process open issues that are unassigned or assigned only to the authenticated user, oldest first, using `gh-issue` sequentially. Accept `--limit N`, `--label foo`, and `--dry-run` (discovery only). +Process open issues that are unassigned or assigned only to the authenticated user, oldest first, using {{$SKILL:bashunit-gh-issue}} sequentially. Accept `--limit N`, `--label foo`, and `--dry-run` (discovery only). ## Discover @@ -23,8 +23,8 @@ Exclude pull requests, issues assigned to anyone else, and tracker issues whose ## Process 1. Check `git status --porcelain`. Preserve dirty work and stop or use an isolated worktree; never auto-stash or reset it. Fetch main and update a clean main with `git pull --ff-only`. A divergent branch needs a deliberate resolution. -2. Choose the validation mode in step 3 before invoking `gh-issue`. Re-read each issue's state and assignees before starting; skip closed or newly assigned issues. Invoke `gh-issue`, which owns implementation, the canonical quality gate, commit, and PR. The coordinator runs that gate once per complete issue; delegates run only focused checks. -3. If strict-mode coverage is required by CI, use `./bashunit --parallel --simple --strict tests/` in place of the ordinary parallel gate before committing. Inspect the current CI command first. Simple mode emits marker output; assert on helpers when testing full messages. Under strict mode, use `if` instead of failing `[ condition ] && assignment`, initialize arrays with `=()`, read potentially empty elements as `${arr[i]:-}`, and keep `extdebug` inside a capture subshell. +2. Re-read each issue's state and assignees before starting; skip closed or newly assigned issues. Invoke {{$SKILL:bashunit-gh-issue}}, which owns implementation, the canonical quality gate, commit, and PR. The coordinator runs that gate once per complete issue; delegates run only focused checks. +3. The gate includes `make test/strict`, which is what CI's strict job runs. Simple mode emits marker output; assert on helpers when testing full messages. Under strict mode, use `if` instead of failing `[ condition ] && assignment`, initialize arrays with `=()`, read potentially empty elements as `${arr[i]:-}`, and keep `extdebug` inside a capture subshell. 4. Wait for `gh pr checks --watch`. Investigate red checks, including Bash 3.0 compatibility. Re-run an identified infrastructure failure with `gh run rerun --failed`; do not classify every registry failure as transient. If a local failure appears unrelated, compare the relevant test on unchanged main in an isolated checkout before claiming it predates the change. 5. Merge only when the user's authorization covers it and checks and branch protection allow it. Do not bypass protection. After an authorized merge, verify issue closure; close a completed issue if needed and update its parent's checklist. Escape Markdown brackets as literals when editing checkbox text and preserve the rest of the body. 6. Delete merged branches and worktrees according to the global cleanup rules, fetch and prune, and fast-forward main before the next issue. Keep branches with open PRs or unpushed commits. diff --git a/.agnostic-ai/skills/commit/references/formatting.md b/.agnostic-ai/skills/commit/references/formatting.md index 4c337474..6420c483 100644 --- a/.agnostic-ai/skills/commit/references/formatting.md +++ b/.agnostic-ai/skills/commit/references/formatting.md @@ -1,8 +1,8 @@ # Formatting Match the surrounding 2-space style by hand and verify with `make lint`. There is -no `shfmt` target, and **`shfmt -w .` must not be run**, it fights `.editorconfig` -two ways: +no `shfmt` target. `.agnostic-ai/rules/bash-style.md` forbids `shfmt -w` because it +fights `.editorconfig` two ways: - `.editorconfig` sets `indent_size = unset` for `tests/acceptance/**.sh` and `src/console/header.sh`; shfmt reads `unset` as "use my default", which is tabs, diff --git a/.agnostic-ai/skills/fix-test/SKILL.md b/.agnostic-ai/skills/fix-test/SKILL.md index f1ce74a7..1093c6dc 100644 --- a/.agnostic-ai/skills/fix-test/SKILL.md +++ b/.agnostic-ai/skills/fix-test/SKILL.md @@ -8,7 +8,7 @@ allowed-tools: read, edit, shell, Grep, Glob Repository paths are relative to the repository root. Start with reported output and the failing file; use `./bashunit --filter "test_name" ` to reproduce narrowly. Discover with the full suite only when no narrower failure is known. -1. Trace the failure to its source and classify it: incorrect expectation/setup, implementation defect, environment/fixture issue, parallel race, or flaky external dependency. Check fixtures under `tests/fixtures/` and mocks with `assert_have_been_called ` where relevant. Temporary diagnostics go to stderr and must be removed. +1. Trace the failure to its source and classify it: incorrect expectation/setup, implementation defect, environment/fixture issue, parallel race, or flaky external dependency. Check fixtures under `tests/acceptance/fixtures/` and the fixtures beside each test, and mocks with `assert_have_been_called ` where relevant. Temporary diagnostics go to stderr and must be removed. 2. Preserve a failing regression for the actual defect. Correct an expectation only when the intended behavior proves it wrong; otherwise make the smallest implementation fix with TDD. Repair fixtures or setup for environment defects, use the existing `$temp_dir` isolation and wait for asynchronous work for races, and mock external dependencies for flaky tests. 3. Run the focused test until it passes for the right reason. Reproduce relevant parallel behavior when investigating isolation. If the failure may predate the change, verify the same focused test on unchanged main in an isolated checkout. 4. Add a relevant edge case if it protects the root cause. After the complete change set, the coordinator runs the canonical quality gate once; delegates run only assigned targeted checks. diff --git a/.agnostic-ai/skills/fork-census/SKILL.md b/.agnostic-ai/skills/fork-census/SKILL.md index 72e349fa..f95ef62d 100644 --- a/.agnostic-ai/skills/fork-census/SKILL.md +++ b/.agnostic-ai/skills/fork-census/SKILL.md @@ -1,12 +1,12 @@ --- name: fork-census -description: Measure process costs and fork budgets when changing startup, per-file, per-test, parallel workers, reporting, or coverage paths; use for fork census and performance comparisons. +description: Measure fork budgets and process costs when changing startup, per-test, parallel, report, or coverage paths. allowed-tools: read, shell --- # Fork census -Read [the fork-budget reference](references/fork-budget.md) before changing process costs. Its historical timings are evidence of earlier measurements, not current baselines. All repository paths below are relative to the repository root. +Read [the fork-budget reference](references/fork-budget.md) before changing process costs, and [the module map](references/module-map.md) to find which `src/` module owns a path. Its historical timings are evidence of earlier measurements, not current baselines. All repository paths below are relative to the repository root. 1. Identify the actual path: cold start, sequential run, parallel worker, lifecycle hook, report, or coverage. Read the matching existing fixture in `tests/acceptance/bashunit_coldstart_forks_test.sh`, `tests/acceptance/bashunit_run_forks_test.sh`, or `tests/acceptance/bashunit_coverage_forks_test.sh`. Include the options and hooks the change exercises. 2. For a timing claim, measure current main before editing and record its commit, Bash version, OS, fixture, options, and sampling method. Land any new benchmark harness before the PR that measures a delta. Alternate baseline and candidate rounds in the same location and repeat anomalous readings. diff --git a/.agnostic-ai/skills/fork-census/references/module-map.md b/.agnostic-ai/skills/fork-census/references/module-map.md new file mode 100644 index 00000000..3b2a6191 --- /dev/null +++ b/.agnostic-ai/skills/fork-census/references/module-map.md @@ -0,0 +1,90 @@ +# Module map + +Module paths are under `src/` unless stated otherwise. Verify the current caller before relying on a row. + +| Module | Owns | +|--------|------| +| `bashunit` | entry: version gate, early flag scan, sources every module, subcommand routing | +| `main/index.sh` | aggregator only, sources the `src/main/` module below | +| `main/validate.sh` | shared parser validation: unknown options, integer/path checks, shard parsing | +| `main/test.sh` / `main/bench.sh` / `main/subcommands.sh` | flag parsing per subcommand | +| `main/assert.sh` | the standalone `bashunit assert` path | +| `main/watch.sh` / `main/run.sh` | the `--watch` loop; the run lifecycle, exit codes and cleanup | +| `runner/index.sh` | aggregator only, sources the `src/runner/` module below | +| `runner/context.sh` | workdir restore, test identity/location exports, title interpolation, capability probes | +| `runner/payload.sh` | the `_BASHUNIT_RUNNER_*_OUT` return slots; encode/decode of the per-test result payload | +| `runner/diagnostics.sh` | runtime-error detection, kill-signal classification, profiling, verbose/file headers | +| `runner/result.sh` | `parse_result{,_sync,_parallel}`, failure source context, failed/skipped/incomplete/risky writers | +| `runner/parallel.sh` | job-slot waiting (`wait -n` or poll), running-job count, spinner | +| `runner/hooks.sh` | set_up/tear_down (test + script scope), hook failure records, mock clearing, EXIT cleanup | +| `runner/provider.sh` | `@data_provider` argument parsing | +| `runner/exec.sh` | `run_test`, the capture-subshell body, retry, timeout watchdog, per-file dispatch, `order_functions_for_script` (the run order a seed produces) | +| `runner/list.sh` | `--list`/`--dry-run`: emits selected tests instead of running them; hooks in after all filtering, before any hook | +| `runner/sandbox.sh` | `--sandbox`: fails a test that reaches an unmocked external command (ADR-012) | +| `runner/discovery.sh` | `load_test_files` (the per-file loop), `functions_for_script` | +| `runner/bench.sh` | benchmark file loop and bench function dispatch | +| `helper/index.sh` | aggregator only, sources the `src/helper/` module below | +| `helper/naming.sh` | resolving and normalising test-function and variable names | +| `helper/discovery.sh` | `find_files_recursive`, `find_total_tests`, fn filtering, duplicate check, line lookup | +| `helper/provider.sh` / `helper/tags.sh` | `@data_provider` map; `@tag` extraction and matching | +| `helper/annotations.sh` | per-test `@timeout`, `@retry` and `@skip` records; the scan lives in `build_provider_map` | +| `helper/metadata.sh` | the indexed metadata map lookups | +| `helper/encoding.sh` / `helper/functions.sh` / `helper/git.sh` | base64 + ids; generic fn utilities; remote tag lookup for `upgrade` | +| `state/index.sh` | aggregator only, sources the `src/state/` module below | +| `state/counters.sh` | test and assertion tallies and their get/add accessors | +| `state/duplicates.sh` | duplicate test-function detection state | +| `state/context.sh` | per-test output buffer, exit code, title, hook failure, and the per-test reset | +| `state/payload.sh` | the encoded per-test result payload and the base64 capability probe | +| `state/parallel.sh` | aggregating per-test result files after a `--parallel` run | +| `config/index.sh` | aggregator only, sources the `src/config/` module below | +| `config/env.sh` | all `BASHUNIT_*` defaults/config files, scratch dirs (`_BASHUNIT_RUN_OUTPUT_DIR` + EXIT-trap cleanup); executes at source time | +| `config/rerun.sh` | the `.bashunit/last-failed` cache for `--rerun-failed` | +| `config/suites.sh` | named `[suite:]` sections in `.bashunitrc` | +| `config/parallel.sh` | is this run parallel; worker temp tree and stop-on-failure flag file (not `runner/parallel.sh`, which waits on job slots) | +| `console/index.sh` | aggregator only, sources the `src/console/` module below | +| `console/colors.sh` | the `_BASHUNIT_COLOR_*` palette and `bashunit::sgr` | +| `console/header.sh` | the "Running N tests" header | +| `console/line.sh` | `print_line`, the primitive every result line goes through, and its TAP variant | +| `console/duration.sh` / `console/diff.sh` | duration formatting; unified and line-by-line diffs under a failure | +| `console/test_line.sh` | the per-test result lines (passed/failed/skipped/incomplete/snapshot/risky/error) | +| `console/deferred.sh` | end-of-run blocks buffered during the run (scratch files under the run dir) | +| `console/summary.sh` | run totals, execution time, hook completion | +| `assert/index.sh` | aggregator only, sources the `src/assert/` module below, plus `skip_todo.sh` and `test_doubles.sh` | +| `assert/core.sh` | `assert::should_skip`, `assert::fail_with`, `assert::join_to_slot` and the comparison assertions the other files build on | +| `assert/{arrays,assertions,dates,duration,files,folders,json,once,snapshot}.sh` | the per-topic assertions; the per-assertion path must stay fork-free | +| `util/index.sh` | aggregator only, sources the `src/util/` module below | +| `util/clock.sh` | time impl selection (EPOCHREALTIME > date > perl > …), return-slot reads | +| `util/str.sh` / `util/math.sh` | pure-bash string and arithmetic helpers | +| `api/index.sh` | aggregator only, sources the `src/api/` module below | +| `api/globals.sh` | `temp_file`/`temp_dir`/`current_dir`/`data_set`; repeats `set -euo pipefail` at file scope for anything sourcing it alone, the entrypoint already set it | +| `api/skip_todo.sh` / `api/test_title.sh` | `skip`/`todo` markers; `set_test_title` | +| `api/bashunit.sh` | the custom-assert facade (`assert_that`, `assertion_failed`/`passed`) | +| `doubles/index.sh` | aggregator only, sources the `src/doubles/` module below | +| `doubles/mock.sh` | `mock`/`unmock` and the `_BASHUNIT_MOCKED_FUNCTIONS` registry `runner/hooks.sh` unwinds per test | +| `doubles/spy.sh` | `spy` and its `_BASHUNIT_SPY_*` state slots + files | +| `doubles/assertions.sh` | the `assert_have_been_called*` family | +| `coverage/index.sh` | aggregator only, sources the `src/coverage/` modules below | +| `coverage/config.sh` | data-file locations, tracked-file roots, engine selection (`init` resets state owned by several modules) | +| `coverage/paths.sh` | `normalize_path`, `should_track` and the hot-path track/path caches | +| `coverage/engine.sh` | DEBUG-trap and xtrace capture, buffering, `finalize`/`cleanup`, parallel merge; only active under `--coverage` | +| `coverage/lines.sh` | executable-line classification (fork-free; the report phase's hot path) and hit-data reading | +| `coverage/stats.sh` | percentages, the precomputed per-file stats cache, threshold gate | +| `coverage/functions.sh` | function definitions and their line spans | +| `coverage/branches.sh` | branch extraction + hit computation; one file, the `_branch_*` helpers mutate `extract_branches`'s locals via dynamic scoping | +| `coverage/report_text.sh` / `report_lcov.sh` / `report_html.sh` | the three renderers | +| `coverage/report_cobertura.sh` | the Cobertura XML renderer | +| `coverage/rules_awk.sh` | the line classifier as awk source; `coverage/lines.sh` stays the reference implementation | +| `coverage/diff.sh` | diff coverage: restricts the report to lines changed against a base ref | +| `coverage/html_index.sh` / `html_file.sh` | HTML page emitters; the only two files exempt from `max_line_length` | +| `reports/index.sh` | aggregator only, sources the `src/reports/` module below | +| `reports/collect.sh` | the shared per-test result arrays every writer reads, and the API the runner fills them through | +| `reports/{junit,html,tap,json,markdown,gha}.sh` | the report writers: JUnit, HTML, TAP, JSON, the Markdown run summary, GitHub Actions annotations | +| `system/index.sh` | aggregator only, sources the `src/system/` module below | +| `system/check_os.sh` / `system/dependencies.sh` | one-fork OS detect; `command -v` probes (builtins, not forks) | +| `system/io.sh` | small I/O helpers | +| `system/bash.sh` | what the running Bash can do, decided once at load time | +| `cli/index.sh` | aggregator only, sources the `src/cli/` module below | +| `cli/{upgrade,watch,doc,init}.sh` | the non-`test` subcommand implementations; main subcommand routing calls them | +| `benchmark/index.sh` | aggregator only, sources the `src/benchmark/` module below; shared with `runner/bench.sh` | +| `benchmark/{annotations,run,results,baseline,reports}.sh` | `@revs`/`@its`/`@max_ms` parsing, running one bench function, the results table, baseline comparison, JSON and JUnit writers | +| `dev/debug.sh` | `bashunit::dump` and `bashunit::dd` for local debugging, not used by the runner | diff --git a/.agnostic-ai/skills/pr/SKILL.md b/.agnostic-ai/skills/pr/SKILL.md index c9498acb..f37500eb 100644 --- a/.agnostic-ai/skills/pr/SKILL.md +++ b/.agnostic-ai/skills/pr/SKILL.md @@ -14,7 +14,7 @@ Use this skill for PR creation. Repository paths are relative to the repository 3. Complete the quality gate and commit workflow in `.agnostic-ai/AGNOSTIC_AI.md` and {{$SKILL:commit}}. Reuse passing checks for unchanged content. Do not rely on an optional Git hook being installed. Obtain the required diff review before pushing. 4. Push with `git push -u origin HEAD` when authorized. If it fails, diagnose and fix the cause; never bypass hooks or branch protection. 5. Read `.github/PULL_REQUEST_TEMPLATE.md` and follow its current headings and checklist. Use a conventional title under 70 characters. Link an actual issue with `Related #N`, never invent one. Explain the problem and resulting behavior in 1-2 background sentences and 2-4 change bullets, including concise validation. Avoid file inventories and conversational history. -6. Write the exact PR body to a temporary file; create with `gh pr create --title --assignee @me --label <label> --body-file <file>`. Select one label: `bug`, `enhancement`, `documentation`, `refactor`, `ui`, or `investigation`, according to the change. Report the URL. +6. Write the exact PR body to a temporary file; create with `gh pr create --title <title> --assignee @me --label <label> --body-file <file>`. Select one label: `bug`, `enhancement`, `documentation`, or `refactoring`, according to the change. Report the URL. 7. If an issue has a configured project in `.agnostic-ai/local/github-project.json`, use its actual IDs to move the issue to In Review. Do not assume project access or invent a missing configuration; report an unavailable update separately. Respect existing publishing authorization. Creating a PR does not authorize merging it. diff --git a/.agnostic-ai/skills/review/SKILL.md b/.agnostic-ai/skills/review/SKILL.md index 3dd3b70a..dff2c8f0 100644 --- a/.agnostic-ai/skills/review/SKILL.md +++ b/.agnostic-ai/skills/review/SKILL.md @@ -1,6 +1,6 @@ --- name: review -description: Review a GitHub pull request; defaults to the current branch's PR when no number is given +description: Review a GitHub pull request, by default the current branch's argument-hint: "[#N]" allowed-tools: shell, read, Grep, Glob --- diff --git a/.gitignore b/.gitignore index e2b5499a..22298fb7 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ dev.log /.agnostic-ai/.sync-state /.agnostic-ai/local/ /.agnostic-ai/packs/ +/.claude/.agnostic-ai-permissions.json /.claude/agent-memory-local/ /.claude/agents/ /.claude/hooks/ diff --git a/.worktreeinclude b/.worktreeinclude index 86886535..f3b07b65 100644 --- a/.worktreeinclude +++ b/.worktreeinclude @@ -7,6 +7,7 @@ /.agents/skills/ /.agnostic-ai/local/ /.agnostic-ai/packs/ +/.claude/.agnostic-ai-permissions.json /.claude/agent-memory-local/ /.claude/agents/ /.claude/hooks/ diff --git a/agnostic-ai.yaml b/agnostic-ai.yaml index 9085d41f..55a81dbc 100644 --- a/agnostic-ai.yaml +++ b/agnostic-ai.yaml @@ -3,6 +3,10 @@ version: 1 requires: "0.87.0" builtins: [] +lint: + instructions-words: 1000 + codex-chain-bytes: 24576 + targets: - claude - codex