Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
da8e645
fix(native): close Compose-backend classifier gaps for ownership labe…
roodboi Oct 22, 2026
58ed88d
test: fail and contain process-state leaks between test files
roodboi Oct 22, 2026
9301b86
fix(native): keep policy refusal reasons past the storage deadline an…
roodboi Oct 22, 2026
33cebd7
test: catch proxy keys Bun keeps after a delete, and release the laun…
roodboi Oct 22, 2026
8b20708
Merge remote-tracking branch 'origin/next' into wf/g41-test-global-le…
roodboi Oct 22, 2026
d4e4500
chore: merge next into the Compose-backend classifier branch
roodboi Oct 22, 2026
f6284b0
fix(agents): read Git plumbing helpers as their porcelain in the task…
roodboi Oct 22, 2026
5ce8002
Merge origin/next into wf/g41-test-global-leak-guard
roodboi Oct 22, 2026
25c6191
fix(project): treat an info/exclude with no write bit as read-only, l…
roodboi Oct 23, 2026
f0c217a
fix(cli): resolve prompts by policy when stdin or stdout is not a TTY
roodboi Oct 23, 2026
e650edc
fix(doctor): name explicit recovery rollback for a literal override a…
roodboi Oct 23, 2026
ad09eff
Merge origin/next into wf/g41-test-global-leak-guard
roodboi Oct 23, 2026
9870275
fix(logs): remove the Loki follow SIGINT listener on every exit path
roodboi Oct 23, 2026
980aea6
fix(agents): name the permission fix and keep a value-free cause on i…
roodboi Oct 23, 2026
6268ea2
train: merge #404
roodboi Oct 24, 2026
6e2b28a
train: merge #446
roodboi Oct 24, 2026
09363a2
train: merge #430
roodboi Oct 24, 2026
cb021dc
train: merge #439
roodboi Oct 24, 2026
cce9abf
train: merge #421
roodboi Oct 24, 2026
1292b7d
train: merge #448
roodboi Oct 24, 2026
3c73ad7
train: merge #452
roodboi Oct 24, 2026
c76bd20
train: merge #405
roodboi Oct 24, 2026
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
12 changes: 12 additions & 0 deletions .ai/skills/hack-repo-verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ Host and container `dist` remain separate. This checks compatibility, not exact
source provenance. Raw `bun test <paths>` remains available for focused tests;
it bypasses the suite preflight and does not prepare compiler-dependent fixtures.

All test files share one Bun process. `bunfig.toml` preloads
[the global-state guard](../../../tests/helpers/global-state-guard-preload.ts) for
every `bun test` run: a test or file that leaves the environment, cwd, umask,
exit code, a guarded global function or a process listener changed fails with a
`[global-state-guard]` message naming the file. The guard restores the
environment, cwd, umask and exit code so later files run clean; it keeps
replaced functions and listeners, which may still protect unsettled work. Fix
the named file's restore (`try`/`finally` or `afterEach`), not the later failures.
Restore environment keys with `restoreEnv` from `tests/helpers/env.ts`: Bun keeps
a deleted `HTTP_PROXY`-style key for `fetch` and `Bun.spawn` without `env`, and
the guard reports that as `native env <KEY> changed`.

For release workflow edits, run the pinned syntax/context check also enforced by CI:
`mise x actionlint@1.7.12 -- actionlint -shellcheck= -pyflakes= .github/workflows/ci.yml .github/workflows/release.yml .github/workflows/prerelease.yml .github/workflows/bun-crash-experiment.yml`.
This validates event globs and reusable workflow contexts; Bun YAML parsing alone does not.
Expand Down
10 changes: 8 additions & 2 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
[test]
root = "./tests"
# Fails the run when a test creates or changes a file in the checkout.
preload = ["./tests/helpers/checkout-write-guard-preload.ts"]
# global-state-guard: fails a test or file that leaves PATH, HOME, cwd or other
# process state changed, and restores it so later files run clean (HACK-1327).
# checkout-write-guard: fails the run when a test creates or changes a file in
# the checkout (HACK-1343).
preload = [
"./tests/helpers/global-state-guard-preload.ts",
"./tests/helpers/checkout-write-guard-preload.ts",
]
19 changes: 16 additions & 3 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,11 @@ Historical remote guides are excluded from the supported documentation.
from `up`, `down`, `restart`, `ps`, `logs`, `run`, `exec`, `open`, `tui`, `branch ...`,
`config get`/`set`, `env ...`, `host exec`/`shell` and `project owner`/`target`/`task`, for
example `hack ps -p <dir> --json` and `hack branch list -p <dir>`. Under `--json` it is one
error envelope; without `--json` it is one `ERROR: E_PROJECT_NOT_FOUND: ...` line and no stack
trace. `project target` and `project task` use their own message, and `target` adds a
error envelope; without `--json` it is one closed error line, `E_PROJECT_NOT_FOUND: <message>`,
and no stack trace. The logger adds its own prefix: the console logger (`HACK_LOGGER=console`)
prints `ERROR: E_PROJECT_NOT_FOUND: ...` on stderr, gum prints a styled `ERROR` level on
stderr, and clack prints a `■` marker on stdout after its `│` guide line. `project target` and
`project task` use their own message, and `target` adds a
`detail`. These commands report something else:
- `tui` without a terminal refuses for that reason first.
- `env get` never prints a failure reason, since its output is raw values: it prints one fixed
Expand All @@ -150,6 +153,14 @@ Historical remote guides are excluded from the supported documentation.
trace; set `HACK_DEBUG=1` to add the stack. Unexpected exceptions always print their stack.
- `--no-interactive` (or `HACK_NO_INTERACTIVE=1`) is a global flag: commands never prompt — they
apply documented defaults or fail fast with `E_INTERACTIVE_REQUIRED`.
- A run whose stdin or stdout is not a TTY (piped input, an agent harness, CI) is treated the same
way, with or without the flag: the command resolves every prompt by its documented
non-interactive policy within milliseconds and never waits on a prompt. Confirmations that have
a safe default apply it (`hack env unset KEY` removes the key; `hack doctor --fix` applies safe
remediations and skips destructive ones with a note); prompts that need a real answer fail
closed with `E_INTERACTIVE_REQUIRED`, naming the flag or argument that makes the invocation
scriptable (for example `hack update --yes`, `hack mcp --all`, `hack session attach <name>`).
Set `HACK_NO_INTERACTIVE=1` explicitly in scripts anyway so the intent is visible.
- `NO_COLOR` (or `HACK_NO_COLOR`) disables colored/decorated output.

Generated agent docs, Cursor rules, Codex skills, and the shared `~/.ai/skills/hack-cli` skill carry
Expand Down Expand Up @@ -354,7 +365,9 @@ The two variables bound different things:

When a carrier budget or the whole-phase cap runs out, the command reports the
closed effect reason `storage-budget` in `error.detail.native_compose_effect_refusal`,
and the human summary names `HACK_NATIVE_STORAGE_TIMEOUT_MS`. Exhaustion leaves the
and the human summary names `HACK_NATIVE_STORAGE_TIMEOUT_MS`. A refusal with its own
cause, such as a helper policy refusal, keeps that reason even when it is caught after
the carrier deadline; raising the storage budget does not address it. Exhaustion leaves the
same retained, uncertain state as any other storage refusal; inspect saved state
before retrying.

Expand Down
10 changes: 8 additions & 2 deletions docs/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@ How it is maintained:
- a read-only `.hack/.gitignore` is never replaced, the same policy as for
`info/exclude`. A file with no write permission bit (for example mode 0444)
is read-only for every user, root included; otherwise Hack asks the
operating system whether you may write it. When it lacks the managed block,
`hack doctor` warns and names the fix (make it writable, then run
operating system whether you may write it. Hack checks the mode bits first,
so an access control list (ACL) that grants you write access to a 0444 file
does not make it writable for Hack; add a write bit instead. When it lacks
the managed block, `hack doctor` warns and names the fix (make it writable,
then run
`hack doctor --fix`), and `--fix` leaves it unchanged. `hack env set --local`
and `hack env hydrate` still write the local layer, print the same warning
on stderr (so `--json` output on stdout stays valid JSON), and leave the
Expand All @@ -93,6 +96,9 @@ How it is maintained:
clone and never committed. The update
is best-effort: if git is unavailable, or `info/` or `info/exclude` is a
symlink or not writable, Hack leaves it alone and the command still runs.
"Not writable" follows the `.hack/.gitignore` rule above: an `info/exclude`
with no write permission bit is left alone even for root or when an ACL
grants write.
- never commit `.hack/.internal/`: it holds machine-local generated state that
runtime commands rewrite. If it is tracked, `hack doctor --fix` untracks it.
- the entries live inside a marked, hack-managed block; anything you add
Expand Down
25 changes: 16 additions & 9 deletions docs/guides/native-candidate.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,16 +190,23 @@ Compose backend. Native support for them is planned after v5.
| Compose feature | Kept on the Compose backend when |
| --- | --- |
| External network | A top-level network sets `external: true` or `external: {name: ...}`. A reviewed `x-hack-isolated: true` replacement is not external: it creates an owned internal bridge. A network that the `HACK_NATIVE_ADAPTATION` selection (see [Frontend integration status](#frontend-integration-status)) lists in `isolatedNetworks` gets the same replacement, so it is not external either. |
| Reserved ownership label | A service, network or volume label key starts with `com.docker.compose.`, `io.hack-local.` or `io.hack.native-config.`. Compose, the native runtime and native-config Compose use these labels to prove which resources they own. |
| External volume | A top-level volume sets `external: true` or `external: {name: ...}`. The native runtime only creates volumes it owns, and there is no native replacement for an external volume. |
| Reserved ownership label | A service, network or volume label key starts with `com.docker.compose.`, `io.hack-local.`, `io.hack.native-config.`, `io.hack.native-file.` or `io.hack.storage-witness.`. Compose, the native runtime and native-config Compose use these labels to prove which resources they own. |
| Build outside the basic form | A service `build` is not the basic form that [`hack config import --dry-run`](../reference/native-config-import.md#pure-basic-build-preview) accepts (a relative context and optional relative `dockerfile` and `target`), or the service also sets `image`. Build arguments, secrets, SSH, cache options, platforms and other build options are outside the basic form. |
| Non-native platform | A service sets a `platform` other than `linux/arm64` or `linux/arm64/v8`, for example `linux/amd64`. The native VM runs Linux arm64 only. |
| Non-native platform | A service sets a `platform` other than `linux/arm64` or `linux/arm64/v8`, for example `linux/amd64`. The native VM runs Linux arm64 only. A missing or `null` `platform` is unset and passes. |

Native `hack up` and `hack restart` check for these features after selecting the
project and before branch, environment, lifecycle, storage or runtime work. They
refuse with `E_NATIVE_PROJECT_UNSUPPORTED` and one line that names each feature
found, without network names, label keys, paths or platform values. Nothing is
started, stopped or written. Every service is checked, including services outside
the selected profiles.
found, without network or volume names, label keys, paths or platform values.
Nothing is started, stopped or written. Every service is checked, including
services outside the selected profiles.

The check reads only `.hack/docker-compose.yml`, as native start does. It does not
merge Compose override files, follow `extends` or `include`, or read environment
defaults such as `DOCKER_DEFAULT_PLATFORM`: a platform counts only when a service
sets its `platform` field. The native runtime's own review reports `extends` and
`include` as unsupported fields.

The check applies the explicit `HACK_NATIVE_ADAPTATION` selection first, as native
start does. An unchanged Compose file whose external network is listed in
Expand Down Expand Up @@ -227,10 +234,10 @@ authored native format.

`hack doctor` reports the same features for the current project in its
`compose backend features` check. With `--json`, the check's `composeBackend.features`
lists the closed names `external_network`, `reserved_label`, `unsupported_build`
and `foreign_platform`. On the native backend a found feature is a warning; on
the Compose backend it is informational. Native doctor applies the same
`HACK_NATIVE_ADAPTATION` selection before it reports.
lists the closed names `external_network`, `external_volume`, `reserved_label`,
`unsupported_build` and `foreign_platform`. On the native backend a found feature
is a warning; on the Compose backend it is informational. Native doctor applies
the same `HACK_NATIVE_ADAPTATION` selection before it reports.

Native `up` and `restart` also stay in the foreground in every project:
`--detach` refuses with one `E_USAGE` line. Run `hack up` in its own terminal and
Expand Down
24 changes: 15 additions & 9 deletions docs/reference/ax-target-result-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,18 @@ absolute path.

The shared project input guards that every project command runs are mapped
too. Native or conflicting project files are `project_inputs_unsupported`. A
legacy adoption state that is unparseable, interrupted, or cannot be checked
(for example a project directory the caller cannot search) is
`adoption_state_unsafe`. A non-directory where the project directory belongs is
`input_unreadable`. When the guard cannot check whether a project input exists
(for example a `.dev` file, or a `.dev` directory the caller cannot search,
beside a valid `.hack`), the refusal is `input_permission_denied` (`EACCES`,
`EPERM`) or `input_unreadable` (any other fault), like a read fault.
`.hack` or `.hack/.internal` the caller cannot search (for example mode `000`
or `0644`) is a permission fault, not an adoption problem: it is
`input_permission_denied` even when the project has never used adoption, and
its message names the permission fix (make the directories readable and
searchable by your user, for example `chmod u+rwx .hack`) before anything
else. A legacy adoption state that is unparseable or interrupted is
`adoption_state_unsafe`, whose message points at explicit adoption recovery.
A non-directory where the project directory belongs is `input_unreadable`.
When the guard cannot check whether a project input exists (for example a
`.dev` file, or a `.dev` directory the caller cannot search, beside a valid
`.hack`), the refusal is `input_permission_denied` (`EACCES`, `EPERM`) or
`input_unreadable` (any other fault), like a read fault.

`retry` is advice for the caller. Like suggested actions and application logs,
it carries no execution authority.
Expand Down Expand Up @@ -219,8 +224,9 @@ WU21). The inspector itself only produces snapshots.
| Two registrations owning one checkout | `ambiguous_project` | same | paired fixture |
| Malformed Hack config or Compose file, oversized input | `config_parse_failed`, `compose_parse_failed`, `generation_input_too_large` | same | paired fixture |
| Unreadable input (directory in place of a file, no read permission) | `input_unreadable`, `input_permission_denied` | same | paired fixture (permission case skipped as root) |
| Input guard refusals (native/legacy conflict, unsafe adoption state, unsearchable or non-directory project dir) | `project_inputs_unsupported`, `adoption_state_unsafe`, `input_unreadable` | same | paired fixture (unsearchable case skipped as root) |
| Uncheckable input presence (`.dev` file or unsearchable `.dev` beside a valid `.hack`) | `input_unreadable`, `input_permission_denied` | same | paired fixture and domain test (unsearchable case skipped as root) |
| Input guard refusals (native/legacy conflict, unsafe adoption state, non-directory project dir) | `project_inputs_unsupported`, `adoption_state_unsafe`, `input_unreadable` | same | paired fixture |
| Unsearchable `.hack` or `.hack/.internal` on a never-adopted project | `input_permission_denied` (permission fix named first) | same | paired fixture and domain test (skipped as root) |
| Uncheckable input presence (`.dev` file or unsearchable `.dev` beside a valid `.hack`) | `input_unreadable`, `input_permission_denied` | same | paired fixture and domain test (unsearchable case skipped as root; `EPERM` through the classifier seam) |
| Detached linked worktree, no project, missing path | closed refusals | same | paired fixture |
| Unknown service | `service_not_found` | same | paired fixture |
| Config changed during read | `generation_changed_during_read` | same code path | domain test |
Expand Down
10 changes: 9 additions & 1 deletion docs/reference/ax-task-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ until its recipes are re-reviewed.
| An attached `-m` module (`python -mpytest`) as the program's operand | accepted, also after a value option's value (`python -W ignore -mpytest`); `python -mcode`, `python -m`, `python -Ximporttime` and a `-m` taken as an option's value (`python -W -m --`, `python -X -mpytest`) still refused (stdin) | domain test |
| `psql -o`/`--output` piped to a command (`-o '\|cmd'`, `-o\|cmd`, `-qo\|cmd`, `--output=\|cmd`, `--out=\|cmd`) | `inline_command`; `psql -o out.txt`, `psql -F'\|'`, `psql -L '\|log'` (a file name) still pass | domain test |
| Dashed Git builtins (`/usr/lib/git-core/git-clone -u cmd`, `git-config alias.x '!cmd'`, `git-ls-remote --upload-pack=cmd`, `git-fetch-pack --exec=cmd`) | `inline_command`; `git-clone url dir`, `git-push -u origin main`, `git-config alias.st status` still pass | domain test |
| Git plumbing helpers read as their porcelain (`git-bisect--helper --bisect-run cmd`, `/usr/lib/git-core/git-bisect--helper.exe run cmd`, `git bisect--helper --bisect-run cmd`, `git-submodule--helper foreach cmd`) | `inline_command`; `git-submodule--helper status`, `git-difftool--helper a b`, `git add bisect--helper.txt` still pass | domain test |
| `duckdb -storage-version VER db` | accepted; a SQL operand after the database is still refused | domain test |
| `git clone -u` (also after `--attr-source <tree>` or an unknown global option), abbreviated or attached `--upload-pack`/`--receive-pack`/`--exec`, `clone --config`, and `!` alias values (`git config alias.x '!cmd'`, `git -c alias.x=!cmd`) | `inline_command`; `git clone url dir`, `git push -u origin main`, `git add -u`, `git config alias.st status` still pass | domain test |
| Caller `GIT_DIR`/`GIT_INDEX_FILE`/`GIT_WORK_TREE` naming another index that tracks a secret | `input_path_invalid` with no digest; a foreign `GIT_DIR` does not hide the checkout's own index | domain test |
Expand Down Expand Up @@ -409,7 +410,14 @@ until its recipes are re-reviewed.
because a one-letter word is too common as an ordinary argument. A SQL
client that reads statements from stdin (`psql db`, `mysql db`,
`sqlite3 app.db` with no SQL operand) is not refused; only a statement
string in argv is. Git config writes other than `!` aliases (for example
string in argv is. Code a program loads from a file is not inspected
either: for example, a reviewed script that loads a data file with
`pickle` or `marshal`, or reads a config file that it evaluates. A Git
`<sub>--helper` plumbing command (`git-bisect--helper`,
`git submodule--helper`) is read as `git <sub>`, so it meets the rules for
that subcommand, but a helper's own options are not modeled: the
`--add-exec-commands` mode of older Git's `rebase--helper` is not
refused. Git config writes other than `!` aliases (for example
`git config core.pager cmd` or `include.path`) are not detected. Nor are
`mysql`/`mariadb` option files named in argv (`--defaults-extra-file`,
which can set `init-command`) or `--pager`, which runs a command only in
Expand Down
8 changes: 8 additions & 0 deletions docs/reference/native-compose-adoption.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,14 @@ publication and `--recover` switch completion refuse at the `publication-inputs`
boundary with the closed `literal-env-override` reason, before any original moves.
The switch stays journaled and the fixed message names
`hack config adopt --recover --rollback`; rollback stays allowed (HACK-1318).
A repeated `--recover --stop` after that refusal does not reach the publication
boundary again: the switch is already journaled, so the retried preparation stop
refuses it as a pending publication with the generic `E_LEGACY_ADOPTION_BUSY`
code ("busy or requires explicit interrupted-lock recovery") and no publication
diagnostic; the literal-env message appears only on the first refusal.
`hack doctor` reports the pinned literal override and names
`hack config adopt --recover --rollback` as the next step while the switch is
pending, since `hack down` and plain `--rollback` refuse in that state (HACK-1391).

Unmodified v4 clients cannot be universally fenced: they ignore future legacy
versions and may select ancestor Compose inputs. Adoption requires the upgraded
Expand Down
8 changes: 5 additions & 3 deletions docs/reference/native-compose-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,11 @@ expectation admission/write, journal, enrollment read, provision, root discovery
seed, verification, closing root proof, completion write and publication. Carrier
transport additionally distinguishes target, create, policy, start, response and
cleanup; policy reasons identify shape, command, labels, host policy, mount
cardinality, program/storage mounts and policy stability. A refusal that finds
its carrier's storage budget exhausted, and not cancelled, reports
`storage-budget`, which names `HACK_NATIVE_STORAGE_TIMEOUT_MS`; a start phase
cardinality, program/storage mounts and policy stability. A policy refusal keeps
its policy reason even when it is caught after the carrier's deadline. A timeout,
budget or otherwise unclassified refusal that finds its carrier's storage budget
exhausted, and not cancelled, reports `storage-budget`, which names
`HACK_NATIVE_STORAGE_TIMEOUT_MS`; a start phase
that spends its whole-phase storage cap reports the same reason at
`guard-storage`. Unknown errors
remain `unclassified`. Diagnostics contain no paths, resource names, helper replies,
Expand Down
Loading
Loading