Skip to content

train b25 integration gate (do not merge) - #385

Closed
roodboi wants to merge 26 commits into
nextfrom
train/b25
Closed

roodboi wants to merge 26 commits into
nextfrom
train/b25

Conversation

@roodboi

@roodboi roodboi commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Integration gate for merge batch b25: origin/next 2f43421 + #351 (HACK-1152 F8) + #365 (HACK-1291) + #368 (HACK-1276) + #370 (HACK-1293). Tree b42d11f8. 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 26 commits October 19, 2026 11:50
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>
…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>
Add `hack env hydrate`, which reads KEY=value entries (or one JSON object
with --format json) from piped stdin or --file and writes them to the
machine-local env layer in one rename-committed write.

- Only the effective local layer is written; a git-tracked one is refused.
- An entry that already holds its value is kept (secret ciphertext and
  scalar YAML type), and a no-op import does not rewrite the file. A
  changed value gets new ciphertext with a fresh IV.
- Local entries outside the input, other scopes and removal markers for
  other keys are kept.
- A secret in the layer that does not open under the reader key, a
  key-file mint that would shadow HACK_ENV_SECRET_KEY, a held or stale
  hydration lock, and a layer changed by another writer (compare-and-swap
  recheck before the rename) all refuse without changing the layer.
- Input is bounded (1 MiB), strict and all-or-nothing; refusals are
  closed and value-free and name at most a line number.
- Unexpected positional arguments are now reported by count, not echoed,
  so `hack env hydrate KEY=value` cannot print the value.

Refs HACK-1158.

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>
gitleaks flagged the literal HACK_ENV_SECRET_KEY fixture as a generic API
key. Build it at runtime like the repo's other key-shaped fixtures and
record the fingerprint of the branch commit that still holds the literal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Refuse hydration as tracking_unknown when the project is inside a git
  checkout but git cannot report whether it tracks the local layer (exit
  128 from a safe.directory refusal or damaged index, or git missing).
  Outside any checkout, hydration still proceeds.
- Refuse a plain import (no --secret) of a key the target scope stores as
  a secret (secret_downgrade), instead of silently replacing ciphertext
  with plaintext.
- Refuse repeated top-level keys in --format json input (duplicate_key),
  matching the env format, instead of keeping the last value.
- Pin the unreadable_layer refusal (invalid YAML, wrong shape, wrong
  version) and the key-file-before-HACK_ENV_SECRET_KEY reader priority
  with regression tests; share one runtime-built synthetic key helper.
- docs/env.md lists every hydration refusal.

Refs: HACK-1158

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>
Resolve src/lib/fs.ts: keep F7's target inspection, which refuses an
unreadable, non-regular, unwritable or unownable target instead of
reading it as absent, and apply F8's expectedExisting compare-and-swap
to the inspected text. The pre-rename check also uses the inspection, so
a target that became unreadable is refused rather than matching an
expected absence.

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>
The F7 branch now includes next, which switched one private-file write in
project-env-config.ts to publishPrivateFileExclusive and dropped the
createPrivateFileExclusive import. This branch's hydration lock still
creates its lock file exclusively, so import both.

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>
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>
`hack run --profile X` on the native runtime refused every --env/--profile,
even when X was exactly what `hack up --profile X` started (HACK-1276).

Native run now compares a given --env or --profile with the selection the
saved run mapping recorded at startup: profiles as a normalized set, env by
its resolved name. An equal selection runs the one-off; a different one, or a
legacy mapping without recorded selectors, refuses with a one-line E_USAGE
before any runtime request. Omitted options still inherit the running
selection, and run never restarts the project.

The P02 README and the native candidate guide document the native flow.

Refs HACK-1276

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
argv was parsed once against the union of every command's options keyed by
long name, so the last declaration of a short flag won everywhere: `-p` became
`ssh --port` for every command that declares `--path -p`, and run.ts refused
`hack up -p <dir>` with `Option(s) not valid for "up": --port`.

Parse in two passes instead. The first resolves the command path (and the
child-command boundary) reading each option with the options of the command
resolved so far; the second parses with that command's options plus the
global and built-in ones. Options only other commands declare are still parsed
by long name so they keep their "not valid for <command>" refusal; their short
flags never leak. The per-scope guard test replaces the cross-tree type guard.

Refs HACK-1293

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

Native image-project `up` and `restart` now refuse an external Compose
network, a reserved ownership label (com.docker.compose.*, io.hack-local.*,
io.hack.native-config.*), a build outside the import preview's basic form,
or a non-arm64 service platform before branch, env, lifecycle, storage or
runtime work. Each refusal is one value-free E_NATIVE_PROJECT_UNSUPPORTED
line that names the feature and the HACK_RUNTIME_BACKEND=compose selection.

`hack doctor` reports the same features in a `compose backend features`
check, and the native candidate guide lists which Compose features keep a
project on the Compose backend.

Refs HACK-1291

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se-feature-refusals-b21

# Conflicts:
#	src/commands/doctor.ts
…ation

The HACK-1291 Compose-backend check read only the original Compose file, so an
external network that a valid HACK_NATIVE_ADAPTATION selection isolates (the
documented `isolatedNetworks` path) was refused before native start applied
the adaptation. Native up, restart, restart --recover and native doctor now
apply the selection's isolatedNetworks first. The selection is read only when
the file has an external network; an invalid selection refuses with the same
closed E_CONFIG_INVALID line native start gives, before any runtime request.

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

Under heavy host load the stub runtime's reply can time out or arrive
unreadable. Each of those lines is only reachable after the Compose-backend
check passed, so the adaptation case accepts all three.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#340 is now squash-merged into next (9a80dbd), so next and the F7 head
(e99f68c) hold the same F7 text. Conflicts were resolved against that
head as the effective base, keeping F8's changes and next's other work:

- docs/env.md, src/agents/pattern-catalogue.ts,
  src/lib/project-env-config.ts: F8's text. Next's side was F7's text,
  which F8 had already extended (hydrate pointer, P08 card, CAS
  expectedExisting option).
- src/cli/command.ts: keep #359's single-valued passthrough refusal
  exactly; other extra positionals are still reported by count only.
- plugins/.../patterns.md: next's version, then regenerated (output
  equals F8's render).
- src/lib/fs.ts merged without a conflict marker but duplicated
  writeTextFileAtomicIfChanged, AtomicWriteRefusedError and
  inspectAtomicWriteTarget; restored F8's single CAS-aware copy, which
  next's F7 version is a subset of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi roodboi closed this Oct 22, 2026
@roodboi
roodboi deleted the train/b25 branch October 22, 2026 02:57
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