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
31 changes: 9 additions & 22 deletions .agnostic-ai/AGNOSTIC_AI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion .agnostic-ai/agents/code-locator.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
2 changes: 1 addition & 1 deletion .agnostic-ai/agents/code-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
81 changes: 2 additions & 79 deletions .agnostic-ai/rules/architecture-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down
37 changes: 0 additions & 37 deletions .agnostic-ai/rules/bash-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
40 changes: 40 additions & 0 deletions .agnostic-ai/rules/return-slots.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading