Repository navigation
feat(agents): shared AX target and result contract with a read-only inspector - #346
Merged
Merged
Conversation
…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>
…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>
This comment has been minimized.
This comment has been minimized.
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>
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>
roodboi
marked this pull request as ready for review
October 20, 2026 17:59
roodboi
added a commit
that referenced
this pull request
Oct 21, 2026
Brings #355 (G4, HACK-1272: retire authored starts refused before journal reservation), #352, #341, #342 and #346 into the G1 closed-refusal branch. Both sides' behavior is kept. Conflict resolutions: - docs/reference/native-authored-commands.md: keep G1's explanation of `pending` (in-progress start or failed start with retained evidence; only the owner's cleanup proof may retire it) and append G4's pointer that `down --recover` retires an attempt that never reserved runtime state when no startup is running. - src/lib/native-authored-command.ts: the `pending` status note keeps G1's wording and adds G4's `hack down --recover` hint. Auto-merged code keeps G4's canceled `unreserved` exit 130 ahead of G1's stopped-runtime classification. Semantic fix: G1's stoppedRuntimeStartup message map had no entry for G4's new `unreserved` outcome, so a runtime_not_running start that was retired as unreserved would have produced an undefined message. It now reports E_DOCKER_UNAVAILABLE with an `unreserved` message (intent retired, `hack ps` reports not_started). G4's `down --recover` pending-start path already refuses through commandFailure as a closed coded HackCliError (E_LIFECYCLE_FAILED, values omitted), and unreserved startup failures map to E_STARTUP_INCOMPLETE or E_DOCKER_UNAVAILABLE, so no stack or help is printed. - tests/native-authored-project-start.test.ts: the fixture keeps G1's `stoppedDuringServe` and G4's `failBeforeReceiptCode` and `unreservedInspection` options. The stub serve emits G4's configurable failBeforeReceipt code and G1's wrapped stopped-runtime refusal. The stub inspect answers both failure modes, with `native_graph_unreserved` only when requested. The duplicate native-runtime-client import is merged into one. Semantic fix: G4's HACK-1272 call-log expectation now includes G1's read-only `runtime status --json` preflight between plan and serve. Both sides' tests are kept. - tests/native-authored-command.test.ts (auto-merged): add an `unreserved` case to the stopped-runtime startup table. Verification (Bun 1.4.2, macOS): - packages/cli typecheck and check: 0 errors. - native-authored-command, native-refusal-cli, native-authored-command-dispatch and cli-spawn-env-isolation: 124 pass. - Other native-authored-* and native-runtime-* suites: 409 pass, 5 fail. 2 are load-timing failures that pass when rerun alone. 3 are host live-session census failures in the recovery and incarnation suites, which fail identically on origin/next and ead8383 on this host. - native-authored-project-start (--timeout 60000): 48 pass. The 3 host-metadata cases are intermittent (HACK-1299). They also fail intermittently on origin/next and ead8383 and pass when run alone. - Six scripts/check-native-*-plan-cli.ts: all pass after build and build:config-compiler. - docs:cli-reference and generate:agent-plugins: no drift. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
added a commit
that referenced
this pull request
Oct 21, 2026
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>
roodboi
added a commit
that referenced
this pull request
Oct 21, 2026
Linear: HACK-1157 (WU26-CATALOG). Stacked on #346 (`wf/f17-ax-contracts-b16`, WU31 target contract); review that first. ## Final behavior - New read-only commands `hack project task list` and `hack project task inspect <task>`, plus MCP tools `hack.project.task.list` and `hack.project.task.inspect`. Names and schemas are provisional; see `docs/reference/ax-task-catalog.md`. - Source of truth is a checked-in `.hack/tasks.json` (`hack.tasks.v0`), kept separate from `hack.config.json` so the published config schema and the WU31 generation do not change. Each recipe names a Compose service and exact argv, workdir, env scope (overlay plus key names only), required capabilities, effects, prerequisites, checks, cleanup, and provenance pins: SHA-256 of whole files, of one JSON value (RFC 6901 pointer, for example one `package.json` script), and of the canonical Compose service definition. - Every result embeds the full `hack.target.v0` target. Target refusals pass through unchanged, and a config generation change during the read is refused. - Nothing is executed. Results say `authority: "none"` and `execution: "unavailable"`. There is no shell endpoint and no execution path. Refused recipes are reported by index, id and reason only; their argv, text and any smuggled env values are never echoed. - Closed per-recipe refusals: `recipe_invalid`, `ambiguous_task`, `untrusted_provenance`, `inline_command`, `unapproved_host_profile`, `missing_capability`, `env_scope_mismatch`, `service_not_found`, `input_path_invalid`, `input_missing`, `stale_recipe` (with observed digests). Whole-catalog refusals: `invalid_task_id`, `task_not_found`, `catalog_parse_failed`, `catalog_invalid`, `catalog_too_large`, `catalog_changed_during_read`. - **Inline-command detection (reworked, fail-closed).** Every argv position is checked, because an entrypoint may `exec` any word (this repo's toolchain `run.sh` does). An inline-code or stdin flag anywhere after a shell or interpreter refuses the recipe, even after an option value or a script path. So `bash -o pipefail -c`, `sh -O extglob -c`, `bash --rcfile x -c`, `python3 -W ignore -c`, `node -r ./x.js -e`, `perl -I lib -e` and the GitHub Actions form `exec bash --noprofile --norc -e -o pipefail -c …` are all `inline_command`. `env` (including `env -S`), `eval`, `xargs`, `su`, `ssh`, awk and sed are refused at any position; `watch <cmd>`, `Rscript -e`, `R -e`, `flock -c`, `bun repl`, `deno eval|repl`, `sh -s`, `-i`, `-` and a shell or interpreter with no script operand (code from stdin) are refused too. Reviewed script argv still passes, for example `bash -o pipefail scripts/check.sh`, `python3 -m pytest` and `bun run check`. - **Pin paths (reworked).** Pins may not enter any `.git` segment, any `.hack` directory's machine-local entries (`.internal`, `.branch`, `.env`, `.env.state.json`, `hack.env.local.yaml`, `hack.env.*.local.yaml`, `tickets`), or name a secret or key file (`.hack.secret.key`, legacy Hack secrets files, `.env`/`.env.*`, `*.key`, `*.pem`, `*.p8`, `*.p12`, `*.pfx`). A stale refusal reports the observed digest, so these exclusions keep value-derived digests out of output. Names are compared NFC-normalized and lowercased for case-insensitive APFS (`.GIT/config`, `.hack/.Internal/x`). - **Resolved-path check (reworked).** `readPinnedDigest` re-runs the same path rules on the checkout-relative path with directory symlinks resolved. This replaces the old prefix containment check: a symlinked directory can neither leave the checkout nor alias excluded state such as `.hack/tickets`. - Capabilities a Compose-service runner may grant: `checkout_read`, `checkout_write`, `named_volumes`, `network_egress`, `managed_env`. Never granted: host ports/network, Docker socket, home mount, privileged, SSH agent, credential store. No host profile is approved, so host recipes are refused rather than run unconfined. - This repository's catalog has three reviewed recipes for existing toolchain scripts: `models`, `check` and `rust-check`. They are unaffected by the stricter rules and are still `reviewed`. ## Validation (local, macOS, Bun 1.4.3) Rework head `267f62be`: - `bun test tests/project-task-catalog.test.ts tests/integration-ownership.test.ts tests/cli- spawn-env-isolation.test.ts tests/mcp.test.ts`: 44 pass, 0 fail. The catalog file alone: 17 pass, including the repo-catalog check. - `bun run --cwd packages/cli typecheck`: 0 errors. `bun run --cwd packages/cli check`: 0 errors (34 existing warnings). Biome on the changed files: clean. - `bun run docs:cli-reference`: no diff (the CLI surface did not change). No agent guidance or plugin render changed. From the original head `ee3704fd` (not rerun on the rework head): `tests/mcp-inspect- tools.test.ts`, `tests/project-target-inspect.test.ts`, `tests/cli-reference-docs.test.ts`, `tests/agent-instruction-source.test.ts`, `cli-help`, `doctor-generated-files`, `project-target- argument-redaction-cli` and `pattern-examples` passed; `bun run typecheck` and `bun run check` had 0 errors. ## Negative controls (each proved by a mutation that makes its test fail) Rework mutations (all killed): - Inline flags matched only in leading options (the old behavior): 2 tests fail (grammar table and the paired `log-actions` fixture). - `env`/`eval`/`xargs`/awk/sed refused only at argv[0]; stdin flags ignored; missing script operand ignored; `-` operand ignored; REPL/eval subcommands ignored; `watch <cmd>` ignored: each fails the grammar test. - Path segments compared case-sensitively; `.git` refused only as the first segment; secret basenames accepted; `.hack` local entries cut back to `.internal`/`.branch`: each fails the path test. - Resolved-path guard in `readPinnedDigest` disabled: the discovery fixture fails (`escape-dir` through `linked -> ../outside`, and `alias-local` through `hack-link -> .hack` reaching `.hack/tickets/notes.md`, become `stale_recipe` instead of `input_path_invalid`). This closes the verifier's surviving mutation. Original controls (unchanged): - **Registration runs nothing:** under the spawn recorder, listing, inspecting a reviewed `touch <canary>` recipe and inspecting a refused `sh -c` recipe run only `git`; the canary file is never created. - **Stale recipe:** an edited script or service definition is refused with its observed digest. Editing `.hack/toolchain/run.sh` fails the repo-catalog check. - **Log-injected command:** `provenance.source: "log"` is `untrusted_provenance`; `sh -c …` and the Actions-style line with repository provenance are `inline_command`. No argv or canary text appears in CLI stdout or stderr. - **Wrong env scope:** a `staging` recipe is refused under base and passes with `--env staging`; a base recipe is refused under `staging`. - **Missing capability:** `docker_socket` plus an unknown token is refused with `missing_capabilities: ["docker_socket"]` and `unknown_capabilities: 1`. - **Other closed refusals:** ambiguous ids, host runner, unknown service, missing input, file symlink escaping the checkout, invalid id, malformed/foreign/oversized/symlinked catalogs, catalog or config edits during the read, forged or drifted envelopes (dropped by MCP re- validation), and a malformed task id (refused by the CLI; the MCP tool refuses it without spawning). ## Remaining gaps (host-bound or owned elsewhere) - No execution. Running a reviewed recipe, managed env injection, mount/network/resource enforcement, timeouts, cleanup and output policy remain WU26 gates. No container was started for this PR. - Inline-code detection is a denylist of known shells, interpreters and string-to-shell wrappers. A program it does not know, or a reviewed script, can still evaluate an argument; the reviewer must check argv, and WU26 must not treat this check as a sandbox. Being fail- closed, it can also refuse a script argument that looks like an inline flag (`script.sh -c`). - `reviewed` means the pins match. It does not prove who reviewed the recipe, so execution must not treat it as authorization. - Pins cover only the named files and JSON values. Nested scripts, dependencies, lockfiles and base images are not covered. The service definition is pinned but not analyzed for policy. - Env-scope checks compare only the overlay selection. Whether the overlay exists and supplies the scoped keys is not checked. - No host profile is approved, so host-side scripts cannot be catalogued yet. - Pinned inputs are read once. The catalog file and the config generation are re-read and compared. - The full `bun run test` suite and hosted CI were not run locally. ## Rework 2 Addresses the independent verifier's round-1 findings. Commits on top of `267f62be`: `356f6e3b` (fix), `aab02f85` (gitleaks allowlist for the existing synthetic canary), `2dd831e1` (docs/env.md rows), and merges of the current base `wf/f17-ax-contracts-b16` (`a3eb34e9`, then `5352415d`). - **Legacy `.dev` project directory (blocking).** Machine-local entries are now excluded after either project directory name, `.hack` or the legacy `.dev` (from `HACK_PROJECT_DIR_PRIMARY`/`HACK_PROJECT_DIR_LEGACY`). `.dev/.internal/state.json`, `.dev/hack.env.local.yaml`, `.dev/hack.env.prod.local.yaml`, `.dev/tickets/x.md` and `apps/web/.dev/.internal/x` are `recipe_invalid` in any letter case; `.dev/docker-compose.yml` and `.dev/tasks.json` stay accepted. - **Secret files the name rules cannot list (medium).** Every pin must now be a Git-tracked file. One read-only `git --literal-pathspecs -C <root> -c core.fsmonitor=false ls-files -z --cached -- <pins>` runs per pinned recipe, after the lexical and symlink checks and before any pinned byte is read. An untracked or gitignored file (`.npmrc`, `.netrc`, `secrets.json`, ...), a file removed from the index, or a checkout without Git gives `input_path_invalid` with no digest. The docs no longer claim the denylist alone keeps value-derived digests out of output. Known gap, documented: the digest is of the working-tree bytes, so an uncommitted local edit to a tracked file can still appear as a digest in a stale refusal. - **Inline-command forms (non-blocking).** Added `npx -c`/`--call`, `npm exec -c`, `pnpm -c`/`--shell-mode`, `yarn exec`, `bun exec`, `nodemon --exec`, `tsx`/`ts-node` (`-e`, `-p`, `-i`, no operand), `sudo -s|-i`, `doas -s`, `git -c`/`--config- env`/`--exec`/`-x`/`--extcmd`/`submodule foreach`, `nix --expr`/`eval`/`repl`/`develop`/`shell`, `make --eval`/`-E`/`-f -`, `deno run -`, `elixir`/`mix -e`, `erl -eval`, `entr -s`, `parallel`/`sg` with a command, and `runuser`, `nix-shell`, `concurrently`, `watchexec`, `tmux`, `iex`, vi/Vim/Neovim and Emacs at any position. Every repro in the verifier's list is now `inline_command`. The docs now call the check a denylist of known programs that is fail-closed only for the programs it knows. - **Vacuous registration test (minor).** The spawn-recorder test now requires at least one recorded `git` call and a recorded `ls-files`, so a recorder that fails to load cannot pass. - **Not changed here:** the base branch `wf/f17-ax-contracts-b16` is still based on `3b530068`, behind current `next`; that belongs to #346. ### Rework 2 validation (local, macOS, Bun 1.4.3; package.json pins 1.4.2) Head ``6400eec4``: - `bun test tests/project-task-catalog.test.ts tests/project-target-inspect.test.ts`: 40 pass, 0 fail. - Full `bun run test`: 7253 pass, 126 skip, 8 fail, 7387 tests across 524 files (1h16m, host load average 12-18). Every failure is in code this stack does not touch. Rerun serially with `--timeout 60000`: `agent-onboard-command`, `init-assistant`, `native-compose-file-command` and `native-env-values` pass. Three native census tests still fail on deadline errors ("Native process census expired", "live-session observation refused (unknown)"): one in `native- authored-process-incarnation` and two in `native-authored-project-recovery`. I treat these as load-only and have not masked them; hosted CI is the clean signal. - `bun run --cwd packages/cli typecheck`: 0 errors. `bun run --cwd packages/cli check`: 0 errors (34 existing warnings). Root `bun run typecheck` and `bun run check`: pass. Biome on the changed files: clean. - `gitleaks git --config .gitleaks.toml --redact --log-opts='origin/next..HEAD'` (8.30.1): no leaks. Mutations, each killed by the named test: - Skip the tracked-file check: 3 tests fail (paired discovery fixture `untracked-secret`, registration `ls-files` assertion, domain tracking test). - Exclude local entries only after `.hack`: the path grammar test fails. - Remove `npx`, `git` `foreach`, `sudo`, `ts-node` or `runuser` from the tables: the inline grammar test fails for each. - Do not load the spawn recorder: the registration test fails (it passed vacuously before). Hosted CI: CI run 39441252597 (pull_request) was queued at 2026-10-20T14:39Z for head `6400eec4`. Earlier runs on this branch took 3 to 4 hours, so the result was not available when this section was written. ### Rework 2, round 2 (head `14676d5a`) Re-checked the verifier's round-1 findings against the remote head `6400eec4`. All four in-scope items were already fixed there by `356f6e3b`; each was re-proved by a mutation on the new head: - **Legacy `.dev` local state (blocking): fixed.** `isSafeInputPath` excludes machine-local entries after `.hack` or `.dev`. Mutation: drop `HACK_PROJECT_DIR_LEGACY` from the project-dir set → the path grammar test fails. - **Untracked secret files (medium): fixed.** Pins must be Git-tracked (one read-only `git ls- files` before any byte is read). Mutation: make the tracked check always true → 3 tests fail (paired `untracked-secret` fixture, registration `ls-files` assertion, domain tracking test). - **Inline-command forms (non-blocking): fixed for every listed repro.** Re-ran the verifier's 14 repros through the real classifier: all are `inline_command`. - **Vacuous registration test (minor): fixed.** The test requires a recorded `git` call and `ls- files`. - **Base behind `next` (informational): resolved.** #346 was squash-merged into `next` (`8c790004`) and this PR showed `CONFLICTING`. Merged `origin/next` (`c7336989`). Every conflict was additive (the task command, the MCP tools, and the docs, CLI reference and env- matrix rows); the merged tree differs from `next` only in the 14 F18 files. New commits: - `125e2127` (from an earlier unpushed rework pass, reviewed and kept): refuses `data:` URL modules for Node, Bun, Deno and tsx, `git bisect`/`filter-branch`, and `julia -e`/`--print`/no operand. Mutation of those rules → the grammar test fails. - `14676d5a`: refuses `git fetch`/`clone`/`ls-remote --upload-pack` and `git push --receive- pack`, because git runs both values as shell commands. Also maps a dash version suffix (`node-22 -e`) to the same interpreter as `node22`. `git push -u origin main`, `git fetch origin` and `node-22 script.js` still pass. Each rule has its own mutation, and each mutation makes the grammar test fail. Validation (local macOS, Bun 1.4.3; package.json pins 1.4.2): - `bun test tests/project-task-catalog.test.ts`: 18 pass, 0 fail. - `bun run --cwd packages/cli typecheck` / `check`: 0 errors (34 warnings that were already there). Root `bun run typecheck` / `bun run check`: pass. - `gitleaks git --config .gitleaks.toml --redact --log-opts='origin/next..HEAD'` (8.30.1): 11 commits, no leaks. - Full `bun run test`: 7124 pass, 149 skip, 255 fail across 533 files (1h31m, load average 14-28). Every failing file was rerun serially with `--timeout 60000`. These pass: `mcp`, `mcp- prompts`, `agent-plugin-generation`, `env-selection-parity`, `e2e-shard`, `mcp-publication- readiness`, `env-command-matrix`, the task catalog and target inspector suites, and 38 other files. Nine native files still fail: `native-authored-project-start`, `native-authored- process-incarnation`, `native-authored-project-recovery`, `native-compose-adoption- generation`, `native-compose-file-command`, `native-compose-proxy-probe`, `native-compose- retained-storage-command`, `native-compose-storage-activation-command` and `native-compose- storage-fence-cost`. Run serially on a plain export of `origin/next` on the same host, these same files also fail with the same errors: census expiry, `engine:false`, and "Completed-job worktree acceptance refused". `native-authored-project-start` passed once on `next` and failed on the next run. These failures come from the host and its load, not from F18. They are not masked. Hosted CI is the clean signal; `6400eec4` passed every hosted check. Not fixed here (needs a product decision): SQL clients that take a statement string (`psql -c`, `sqlite3 db "…"`) and `python -m code` still pass. `psql`'s `\!` and `sqlite3`'s `.shell` can reach a shell. Refusing every `psql -c` would also refuse common reviewed database tasks. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
roodboi
added a commit
that referenced
this pull request
Oct 23, 2026
…#379) Follow-up to #346 for HACK-1332 (target inspector contract, HACK-1144 / WU31). ## What changed `hack project target` (and the `hack.project.target` MCP tool) could still return a detail-less error envelope when a shared project input guard refused. The MCP adapter turned that into `data: null`. This PR maps every input-guard fault to a closed, value-free refusal. 1. **Guard refusals (cherry-picked rework 077825fe).** The inspector maps the shared guards' refusals wherever they are raised, including inside config reads: - native or conflicting inputs become `project_inputs_unsupported` - an unparseable, interrupted or uncheckable legacy adoption state (for example an unsearchable `.hack` or `.hack/.internal`) becomes the new provisional `adoption_state_unsafe` - a `.hack` file becomes `input_unreadable` `LegacyComposeAdoptionSelectionError` now carries its `kind`. 2. **Uncheckable input presence (new).** `inputPresent()` in `src/lib/project-input-selection.ts` treats only `ENOENT` as absence. Any other fault threw a plain `Error`, so a `.dev` file, or a mode-000 `.dev` directory, beside a valid `.hack` still produced `E_UNEXPECTED` with no detail. The guard now throws a typed `ProjectInputAccessError`. It is value-free: no OS message, path or `cause`. It classifies the fault as `permission` (`EACCES`/`EPERM`) or `unreadable`. The inspector maps these to `input_permission_denied` and `input_unreadable`. **Side effect on other commands:** the same fault now reports `E_CONFIG_INVALID` instead of `E_UNEXPECTED`. The message text is unchanged except for a trailing "Values omitted.", which matches the adoption guard's input-access refusal. The contract doc (`docs/reference/ax-target-result-contract.md`) and its proof table are updated. ## Validation - Before the fix, the new paired CLI/MCP fixtures returned the off-contract envelope `{"code":"E_UNEXPECTED","message":"Cannot inspect Hack project inputs. …"}`. After the fix, they return the closed refusal, and the envelope contains neither the fixture path nor the errno. - Each new test failed under a mutation: - always classifying as `unreadable` failed the permission domain test and the paired test - restoring the plain `Error` failed both domain tests - disabling the inspector mapping failed both paired tests - Local checks: `packages/cli` typecheck passed. `packages/cli` check reported 0 errors (pre- existing warnings only). Biome on the changed files passed. `privacy:check` passed. gitleaks over `origin/next..HEAD` found no leaks. `tests/project-target-inspect.test.ts` and `tests/project-input-selection.test.ts` gave 52 pass, 0 fail. - Full suite on the M3 at the exact head `85c8ec23`: 7579 pass, 126 skip, 2 fail, across 7707 tests. The mode-000 cases are skipped when running as root. - Both failures are in `tests/native-authored-project-recovery.test.ts`: "new no-host death uses real read-only quiescence facts…" and "new no-host recovery retains original bindings on last-await". - Both also fail on the untouched `next` base `f2278c79` in the same M3 environment (16 pass, 2 fail). The error is "Native process census expired". The real host process census keeps refusing its sessions phase while concurrent suites change the process table, so the 3 s budget expires. - These failures predate this PR and do not touch project inputs. They are reported separately, not fixed here. ## Not changed (noted) `readProjectsRegistry` still treats an unreadable registry as empty. That is pending a product decision (HACK-1332 item 1) and is intentionally left alone here. Refs HACK-1332, HACK-1144. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Linear: HACK-1144 (WU31, shared AX target and result contracts).
This adds a provisional target/result decision record and a read-only target inspector that behaves the same through the CLI and MCP.
hack project target [--project=…|--path=…] [--branch=…] [--service=…] [--env=…] [--expect-generation=…] [--json]resolves the exact project, checkout (primary, linked or non-Git), runtime instance (explicit, worktree-derived or default, plus its Compose project name), optional service, env overlay selection and a Hack config generation. It reads local files only. It does not write the registry (not even the last-seen touch), call the engine or Docker, start or repair anything, or change Git. It reportsruntime: { observed: false, reason: "not_probed" }andauthority: "none".hack.project.target(read-only) spawns the same command and passesprojectName/paththrough without resolving them first. Every resolution refusal therefore comes from the CLI.datais the CLI envelope, re-validated against the contract: off-contract output becomesnulland foreign fields are dropped.stdoutis withheld becausedatacarries the same envelope.stageand aretrydisposition, and reuses an existingHACK_ERROR_CODEScode, so no new code or guidance render is needed.--expect-generationrefuses stale config withobserved_generation. If the config changes during the read, the inspector refuses instead of returning a mixed snapshot.servicesis not a mapping is refused ascompose_parse_failed(E_CONFIG_PARSE, stageproject, retryfix_project). It is no longer reported as a target with no services, and--serviceno longer turns it intoservice_not_found. An absent Compose file or an emptyservices:key still givesservices: [].--branchis slugged likehack up --branch, soFeature/Xgives<base>--feature-x..hack/hack.config.json,hack.tomlanddocker-compose.ymlonly. Ordinary source edits do not change it.ambiguous_project, and so is a checkout that two registrations own.src/commands/project.tstosrc/lib/compose-project-name.ts, so the inspector and the commands share one implementation. Behavior is unchanged.docs/reference/ax-target-result-contract.md(decision record, schema, refusal table, compatibility matrix, known gaps), ahack.project.targetsection and row indocs/reference/mcp-tools.md, and a regenerateddocs/reference/cli.md.All names, fields, reasons and retry values are marked provisional for the compatibility review.
Rework after verification (head
bc49c11a)compose_parse_failedrefusal and documented it, together with a "Compose validity" known gap: the inspector does not rundocker compose config, so a file that parses but that Compose would reject is still reported.base), explicit instance slugging, two registrations owning one checkout,config_parse_failed,compose_parse_failed(with and without--service, and a list-valuedservices) andgeneration_input_too_large.HACK_MCP_COMMANDprints a stored envelope. It proves the tool re-validates the payload instead of passing it on. Added direct validator tests for the code/message consistency check.buildEnvExplainArgsJSDoc back above its function insrc/mcp/server.ts.Validation
bun test tests/project-target-inspect.test.ts: 18 pass. Each paired case runs the source CLI directly and through the MCP tool, whose child is the same source CLI viaHACK_MCP_COMMAND. Both useprivateCliEnv, a private HOME/HACK_HOME/registry and an isolated Docker client config. The envelopes must be equal apart fromobserved_at. Cases:alpha--feature-login), an explicit instance overriding it with the Git branch unchanged, andFeature/Xslugged toalpha--feature-xbaseinvalid_selector, MCPinvalid_argumentswith no spawn.hackcontents and the Git worktree list are unchangednull, foreign fields are dropped, and a mismatched message becomesnulltests/integration-ownership.test.ts,tests/cli-spawn-env-isolation.test.ts,tests/mcp-inspect-tools.test.ts,tests/agent-instruction-source.test.ts,tests/cli-help.test.tsandtests/experimental-gating.test.ts(78 pass together with the focused file).tests/mcp.test.ts: 11 pass.bun run --cwd packages/cli typecheck: clean.bun run --cwd packages/cli check: 0 errors. The 34 warnings it reports are in existing files.ultracite checkon the changed files reports nothing.bun run docs:cli-reference: no drift (the CLI surface did not change in the rework).bun run privacy:check: ok. No Rust changed.Negative controls (mutation proofs)
Each mutation was applied alone and then reverted. The focused suite failed every time.
First round:
--project/--pathconflict checkpathwhenprojectNameis set (pre-resolution)Rework (all 12 killed):
resolveEnvSelectionreturns base for an explicit overlayresolveEnvSelectionignores the project default--envargv entry frombuildTargetInspectArgssanitizeBranchSlugcandidates.length > 1)config_parse_failedrefusalparseJson(stdout)without re-validationservicesreturns no servicesRemaining gaps (host-bound or out of scope)
docker compose configis not run.hack.project.jsonprojects are refused asproject_inputs_unsupported.--project/--envgrammar refusals stayE_USAGEwith nodetail.reason, as for every command.Rework 2
Fixes the three open verifier findings from round 1.
Registry no-write proof (should-fix).
inspection writes nothingnow comparesprojects.jsonagainst the bytes and mtime thatbeforeAllwrote, not a baseline read inside the test after earlier paired cases had already run the CLI. Mutant: injecttouchProjectRegistration({ project: context })afterselectProjectinresolveTarget. Round 1: survived. Now: killed (registry text differs).Unreadable inputs leaked OS text and paths (blocking). A filesystem fault while reading inputs used to escape
inspectTargetasE_UNEXPECTEDwith the raw message, for exampleEACCES: permission denied, open '<abs path>/.hack/docker-compose.yml'. It is now one of two closed, value-free refusals:input_permission_denied(E_PERMISSION,project,fix_project) forEACCES/EPERMinput_unreadable(E_CONFIG_INVALID,project,fix_project) for any other errno error. New repro found in this round: when.hack/docker-compose.ymlis a directory, the generation read treats it as absent, and the Compose read then threwEISDIR.The adapter validator now accepts a detail-less error only when its code is
E_USAGE(the parser refusals). Any other detail-less error, such as anE_UNEXPECTEDraised by a defect, becomesdata: null. The contract doc andmcp-tools.mdnow say so.Validator data checks untested (minor). New tests mutate a real inspector success envelope:
authorityset to something other than"none"or removed,runtime.observed: true, a wrongruntime.reason, andruntime: null. The authority case is also checked through the stubbed MCP CLI.New paired CLI/MCP fixtures:
dir-compose, which is deterministic and works as root, andlocked-compose(mode 000; skipped when running as root, because root can read the file). Both also assert that the CLI envelope contains neither the fixture root nor the errno name.Mutations, all killed by the named test:
touchProjectRegistrationafter project selectionEACCEStoinput_unreadableE_USAGEonly, drifted CLI outputauthority === "none"runtime.observed === falseGates run locally on head
5352415d:bun run typecheckandbun run checkpass, as dobun run --cwd packages/cli typecheckandcheck.origin/next..HEADfinds no leaks.mcpandenv-command-matrixsuites pass: 37 pass, 0 fail.bun run test(7369 tests, run on a host under load average 17–20) had 28 failures in 18 files. Serial reruns with--timeout 60000passed in 10 of those files. The other 8 files are host-native tests (native-authored-*,native-compose-adoption-*,native-compose-proxy-probe,native-compose-retained-storage-command,native-compose-storage-activation-command), and they keep failing serially. The same native failures reproduce on the unmodified basea3eb34e9(checked: proxy-probe, retained-storage-command, process-incarnation). They are pre-existing host-environment failures, not caused by this change.Still open:
E_UNEXPECTEDwith free text. MCP returnsdata: nullfor it. Mapping every thrown value to a closed refusal would hide defects, so it was not done.gitchild, if one were thrown, would also be reported asinput_unreadable.Rework 2: independent re-check (head
5352415d, no new commits)A second rework pass started from the current remote head,
5352415d. All three round-1 findings are already fixed there, so this pass pushes nothing.bun test tests/project-target-inspect.test.ts: 22 pass, 0 fail. This includes the permission-denied case, which runs because the host user is not root.touchProjectRegistrationafter project selection: "inspection writes nothing"EACCESmapped toinput_unreadable: permission-denied inputE_USAGEonly, drifted CLI outputauthority === "none"removed: authority/readiness, drifted CLI outputruntime.observed === falseremoved: authority/readinessruntime.reason === "not_probed"removed: authority/readinessbuildTargetInspectArgs) does not hide a dropped flag. Removing any one of--project,--path,--branch,--service,--envor--expect-generationfrom the helper fails 1 to 8 paired tests through their per-field assertions.5352415d: all 16 checks pass, includingtest,secret-scanand everydocker-e2eshard.next(099877a0). It was not updated, because force-pushing and mergingnextwere both outside this pass.Open (not fixed; each needs a product decision):
projects.jsonis present but unreadable (mode 000) or is not valid JSON, the shared lenient reader returns an empty registry.--paththen reportsregistration: null, and--projectrefuses withproject_not_found. Lifecycle commands read the registry the same way, so the answer matches what they would do. The caller just cannot tell this case from a project that is genuinely unregistered. The options are a closed refusal, an explicitregistrationstate, or keeping parity with the commands.EMFILE,EAGAIN,EIO,EINTR) map toinput_unreadable, whose retry isfix_project. For these errors,reinspectis the more accurate advice.Rework 2: re-confirmation on merged head
8395b342(no new commits)The branch now includes
next(b431f0e7, merged by the main session). That merge does not touch the inspector, its tests or its docs.project-target-inspect,env-command-matrix,mcp,mcp-inspect-tools,cli-spawn-env-isolation,integration-ownership): 69 pass, 0 fail.packages/clitypecheck and check: 0 errors.docs:cli-reference: no drift. gitleaks 8.30.1 over theorigin/next..HEADpatches: no leaks.5352415d, whose inspector files are identical on8395b342(registry touch, detail-less pass-through for any code, authority check removed,runtime.observedcheck removed, filesystem-fault mapping removed,EACCESmapped toinput_unreadable). Each one was killed by the named test.🤖 Generated with Claude Code