Skip to content

train b24 integration gate (do not merge) - #376

Closed
roodboi wants to merge 34 commits into
nextfrom
train/b24
Closed

roodboi wants to merge 34 commits into
nextfrom
train/b24

Conversation

@roodboi

@roodboi roodboi commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Integration gate for merge batch b24: origin/next f2278c7 + #344 (HACK-1195) + #340 (HACK-1152) + #353 (HACK-1157). Tree 77e037df. Closed after the batch squashes.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

roodboi and others added 30 commits October 19, 2026 11:41
…erve

An exactly clean stopped Development pool reuses its retained disks on restart,
so admission charges only the 16 GiB host reserve instead of the 58 GiB fresh
allocation budget. Clean requires a quiesced stop of the last boot, a creation
receipt, rootfs pin, both disk identities and declared sizes, the recorded
executable and user, a dead provider, a free VM lock, no open disk handle or
owned listener, and no pending owner update. Foreign, active, dirty and stale
owners refuse with closed codes and keep the fresh-allocation budget. The
selection is rechecked around every sample, under the startup lease, around the
pre-boot sample and immediately before machine start, and never reaches
machine create. Running and Research admission are unchanged.

Refs: HACK-1195

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setProjectEnvValue now keeps an entry's existing ciphertext when it already
decrypts to the requested value under the current key, so repeating
`hack env set --secret --value-stdin` with the same value leaves the tracked
env file byte-identical and git status clean. A different value, a plain
entry becoming secret, or an entry that does not open under the current key
is re-encrypted with a fresh IV.

Env layer writes now commit by rename from an exclusive, fsynced sibling, so
a write interrupted before the commit leaves the previous layer intact and
resolvable. Existing permission bits and symlink targets are preserved.

HACK-P08 moves from deferred to partial: per-key no-op detection and atomic
layer writes exist; a multi-key hydration import remains missing.

Refs HACK-1152

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nspector

Add a provisional target/result decision record and a read-only target
inspector with CLI and MCP parity (HACK-1144, WU31).

- `hack project target [--json]` resolves the exact project, checkout,
  runtime instance, optional service, env overlay selection and a Hack
  config generation from local files only. It performs no registry write,
  engine call or Git mutation, and reports runtime as not probed.
- `hack.project.target` (read-only MCP tool) spawns the same command and
  passes selection through unresolved, so every resolution refusal is the
  CLI's. Its data is the CLI envelope re-validated against the contract.
- Refusals are a closed, value-free catalogue (reason, stage, retry) that
  reuses existing error codes. `--expect-generation` refuses stale config;
  a config change during the read is refused instead of snapshotted.
- Extract the Compose project name resolver from commands/project.ts so
  the inspector and commands share one implementation.
- Paired CLI/MCP fixtures: two projects, a linked-worktree branch and an
  explicit instance, conflicting selectors, stale generation, ambiguous
  equal names, a moved checkout, detached worktree and no-write checks.

Names, fields and reasons are provisional pending compatibility review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vior

A `--local` env write killed before its commit rename left a
`.hack.env*.<uuid>.tmp` staging file that git did not ignore, so a later
`git add -A` could commit machine-local values, and a tracked-layer orphan
left the tree dirty. The managed `.hack/.gitignore` now ignores
`.hack.env*.tmp`, and `hack doctor` flags a tracked staging file.

Tests now cover symlink preservation, staging cleanup after a failed
rename, and clean git status after a killed tracked or local write. The
argv-env-value recipe skips flags, so the P08 card can name
`hack env set <key> --local --value-stdin --secret`. Docs describe the
first-repeat normalization of hand-edited files and the rename edge cases.

Refs HACK-1152

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs HACK-1152

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n gaps

The read-only target inspector now refuses a Compose file that is not YAML,
whose top level is not a mapping, or whose services key is not a mapping, as
compose_parse_failed, instead of reporting a target with no services.

Paired CLI/MCP fixtures now cover env selection (project default, explicit
overlay, base), explicit instance slugging (Feature/X -> <base>--feature-x),
two registrations owning one checkout, config_parse_failed,
compose_parse_failed and generation_input_too_large. A stubbed CLI proves the
MCP tool re-validates the envelope, and the validator's code/message
consistency check has direct tests. Restores the buildEnvExplainArgs JSDoc.

Refs HACK-1144

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A stopped Development pool qualifies for the 16 GiB retained-disk reserve
only with a clean-stop marker that `down` records in the same receipt
write as its `stopped` phase. Every other phase write and every new boot
clear it, so a pre-boot refusal that rewrites the phase to `stopped`
after an uncertain or unclean stop no longer relabels it as clean.

The stopped-pool checks now have two layers. The receipt checks are
read-only and lock-free, and run around every sample and in `runtime
probe`. The release checks (VM lock, lsof, sockets and the provider
config audit) run only under the startup lease. `machine create` and
`machine start` now need typed authorities (`FreshCreate`, `StartPermit`)
that only the admission pool issues, after the pre-boot sample on the
qualifying basis and a final leased recheck. Fresh samples next to a
created stopped pool report the closed `retained_reserve_refusal` code.

Refs: HACK-1195

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With stable ciphertext, setting an unchanged secret keeps the stored
ciphertext, so the shared cipher fixture returned identical values and the
ciphertext-only change tests no longer produced a change. Encrypt each
fixture value in its own store so every call is a fresh encryption.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add `hack project task list|inspect` and the MCP tools
`hack.project.task.list|inspect` (HACK-1157, WU26-CATALOG). They read
reviewed recipes for existing repository scripts from `.hack/tasks.json`
and judge each one against the exact WU31 target: exact argv, Compose
service, workdir, env scope, required capabilities, effects,
prerequisites, checks, cleanup and pinned provenance (file and JSON
pointer SHA-256 digests plus the canonical service definition digest).

Nothing is executed. Results carry `authority: "none"` and
`execution: "unavailable"`; there is no shell endpoint or execution path.
Closed refusals cover ambiguous ids, untrusted (log/suggested) provenance,
inline code or shell syntax in argv, host runners (no approved profile),
missing capabilities, wrong env scope, unknown services, missing or
escaping inputs and stale pins. Refused recipes are reported without
their argv or text. The repository's own toolchain recipes (models,
check, rust-check) are pinned and checked by the new test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new read-only target inspector accepts --env and --service, so the env
matrix source check requires a row for it. It reports only the overlay
selection and never reads env files or values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Choice accessors used only by the macOS-only admission pool tests were
compiled for every test build, so Linux test builds failed -D dead-code.
Use the same cfg as the tests module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Inline-code detection no longer stops at the first operand: an inline or
  stdin flag anywhere after a shell or interpreter refuses the recipe, so
  option values before -c/-e (bash -o pipefail -c, python3 -W ignore -c,
  node -r x -e, the GitHub Actions bash form) are caught.
- env, eval, xargs, su, ssh, awk and sed are refused at any argv position;
  watch with a command, Rscript/R -e, REPL/eval subcommands, '-' and a
  missing script operand (code from stdin) are refused too.
- Pin paths exclude every .git segment, all .hack machine-local entries
  (.env, .env.state.json, local overlays, tickets) and secret/key files,
  compared case-insensitively for APFS.
- readPinnedDigest re-checks the path with directory symlinks resolved, so a
  symlinked directory can neither leave the checkout nor alias excluded
  state; new fixtures cover both.

Refs HACK-1157

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rename-committed env write treated any read failure as an absent file,
so `hack env set` on a layer it could not read (mode 000, or root-owned
0600 from an earlier `sudo hack`) built a layer holding only the new key and
renamed it over the old one, dropping every other value. Rename also skips
the write-permission check an in-place write got, so a read-only or foreign
layer was silently replaced with a new owner.

writeTextFileAtomicIfChanged now classifies the target before staging:
only a missing path or a dangling symlink is absent. An existing target
that cannot be read, is not a regular file, is not writable, or whose owner
and group the staging file cannot take is refused with a closed,
value-free AtomicWriteRefusedError, and the file is left as it was. A
rewrite keeps the file's owner and group as well as its mode.

Document the refusals and that rename detaches hard links, and list the
staging pathspec in the doctor check docstring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A clean stopped Development pool selected the 16 GiB retained reserve on every
restart, so its leased release checks (VM lock, `lsof` over the owned disks,
listeners, provider configuration) ran even when free disk fit the unchanged
58 GiB fresh budget. On a loaded host the 5 s `lsof` bound timed out and the
start was refused as `retained_reserve_active_owner`, where the base branch
restarted the same pool on the fresh budget without any `lsof` scan.

Once the startup lease is held, `settle` now drops a stopped selection whose
every qualifying sample, including the lease recheck, also fits the fresh
budget. Those samples are recorded on the fresh basis and the start continues
exactly as one without a selection, with no release checks. Only a start whose
free disk lies between the host reserve and the fresh budget keeps the
selection; its release checks bound `lsof` at 30 s instead of `down`'s 5 s.
`down`, `recover` and other stop paths keep the 5 s bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aluators

Task catalog pins must now be Git-tracked files, checked with one read-only
`git ls-files` before any pinned byte is read, so a gitignored secrets file
can never have its digest returned in a stale refusal. Machine-local entries
are excluded under the legacy `.dev` project directory as well as `.hack`.

The inline-command denylist now covers package runners (`npx -c`,
`npm exec -c`, `pnpm -c`, `yarn exec`, `bun exec`), TypeScript runners,
privilege wrappers, `git -c`/`--exec`/`submodule foreach`, `make --eval`,
`deno run -`, Elixir and editor forms. Docs no longer describe the check as
fail-closed beyond known programs. The registration test now proves the
spawn recorder saw git calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The canary in tests/project-task-catalog.test.ts proves smuggled env values are never echoed. Gitleaks flagged its introducing commit, which the secret-scan job rescans on every push, so allowlist that exact line the same way as the MCP inspect-tool canary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both commands accept --env, so tests/env-command-matrix.test.ts requires a row for each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A filesystem fault while reading target inputs (a mode-000 or directory
Compose file) escaped as E_UNEXPECTED with the raw OS message and absolute
path. Map errno errors to the closed input_permission_denied and
input_unreadable refusals, and let the adapter validator pass a detail-less
error only for parser E_USAGE refusals.

Strengthen the no-registry-write proof by comparing against the registry
bytes written in beforeAll, and cover the validator's authority and
runtime.observed checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alifies for

`runtime probe` takes no startup lease and does not settle a basis, so for a
qualifying clean stopped pool it reports the host-reserve basis even when a start
with the same headroom would drop the selection and use the fresh budget. State
that the start's `admission.json` records the basis the start actually used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve plugins/hack/skills/hack-cli/references/patterns.md by
regenerating it (bun run generate:agent-plugins) from the merged
src/agents/pattern-catalogue.ts, carrying this branch's pattern wording
and the changes merged in train b18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up origin/next through the F17 branch so the task catalog is checked against the current base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ulia -e in task argv

The task catalog's inline-command denylist missed three string-to-code
forms: a `data:` URL module handed to Node, Bun, Deno or tsx
(`node --import data:text/javascript,...`, `deno run data:...`),
`git bisect run` and `git filter-branch`, which run their command strings
through a shell, and Julia's `-e`/`--print`/stdin REPL. Recipes using them
are now refused as `inline_command`; ordinary `node --import ./file.js`,
`git log` and `julia script.jl` argv still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`start_pool` is not run end to end by any test, so removing its call to
`settle` or `restart_only` failed no test: the other gates still refused
create and boot, but a start could silently lose the fresh-budget fallback
or the leased restart check.

`settle` now returns a `Settled` basis that `restart_only` requires, and
`restart_only` returns the `Restart` authority that `fresh_create` and
`admit_boot` require. Only those functions can construct either value, so
skipping or reordering a step in `start_pool` no longer compiles. Behaviour
is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve generated plugin conflicts (plugins/hack/skills/hack-cli/references/patterns.md ) by regenerating
them with bun run generate:agent-plugins from the merged canonical source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner_not_preserved refusal in writeTextFileAtomicIfChanged had no
test, because a non-root user cannot create a file owned by someone else.
A disposable child now reports the target as owned by another uid, so the
staging file's fchown really fails with EPERM. The test expects the
closed refusal, the old bytes and inode kept, and no staging file left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the squash-merged AX target contract (#346) so the PR no longer
conflicts with next. Conflicts were additive: the task catalog's command,
MCP tools and docs rows are kept beside next's content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README described the typed create and boot authorities but not that the
leased settle step and the restart-only check are now also typed, so a start
cannot skip or reorder them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi and others added 4 commits October 21, 2026 11:43
…d interpreters in task argv

`git fetch`, `clone` and `ls-remote` run their `--upload-pack` value, and
`git push` its `--receive-pack` value, as a shell command, so a recipe using
them is now refused as `inline_command`. A dash version suffix
(`node-22 -e`) now names the same interpreter as `node22`. Ordinary
`git push -u origin main`, `git fetch origin` and `node-22 script.js` argv
still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi roodboi closed this Oct 21, 2026
@roodboi
roodboi deleted the train/b24 branch October 21, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant