Skip to content

chore(deps-dev): bump typescript from 5.9.3 to 6.0.3#6

Open
dependabot[bot] wants to merge 1 commit intomainfrom
dependabot/npm_and_yarn/typescript-6.0.3
Open

chore(deps-dev): bump typescript from 5.9.3 to 6.0.3#6
dependabot[bot] wants to merge 1 commit intomainfrom
dependabot/npm_and_yarn/typescript-6.0.3

Conversation

@dependabot
Copy link
Copy Markdown

@dependabot dependabot Bot commented on behalf of github Apr 18, 2026

Bumps typescript from 5.9.3 to 6.0.3.

Release notes

Sourced from typescript's releases.

TypeScript 6.0.3

For release notes, check out the release announcement blog post.

Downloads are available on:

TypeScript 6.0

For release notes, check out the release announcement blog post.

Downloads are available on:

TypeScript 6.0 Beta

For release notes, check out the release announcement.

Downloads are available on:

Commits
  • 050880c Bump version to 6.0.3 and LKG
  • eeae9dd 🤖 Pick PR #63401 (Also check package name validity in...) into release-6.0 (#...
  • ad1c695 🤖 Pick PR #63368 (Harden ATA package name filtering) into release-6.0 (#63372)
  • 0725fb4 🤖 Pick PR #63310 (Mark class property initializers as...) into release-6.0 (#...
  • 607a22a Bump version to 6.0.2 and LKG
  • 9e72ab7 🤖 Pick PR #63239 (Fix missing lib files in reused pro...) into release-6.0 (#...
  • 35ff23d 🤖 Pick PR #63163 (Port anyFunctionType subtype fix an...) into release-6.0 (#...
  • e175b69 Bump version to 6.0.1-rc and LKG
  • af4caac Update LKG
  • 8efd7e8 Merge remote-tracking branch 'origin/main' into release-6.0
  • Additional commits viewable in compare view

@dependabot @github
Copy link
Copy Markdown
Author

dependabot Bot commented on behalf of github Apr 18, 2026

Labels

The following labels could not be found: dependencies. Please create it before Dependabot can add it to a pull request.

Please fix the above issues or remove invalid values from dependabot.yml.

@dependabot dependabot Bot requested a review from himerus as a code owner April 18, 2026 14:38
himerus pushed a commit that referenced this pull request Apr 18, 2026
…correct git config read

Codex finding #5 (HIGH): rea init followed destination symlinks via
copyFile + chmod, so a malicious symlink at e.g. .claude/hooks/foo.sh
could redirect a subsequent --force install to an arbitrary path and
chmod its target 0o755.

- Resolve install root once with realpath; assert every destination
  resolves inside it.
- lstat every destination before writing. Any symlink raises
  UnsafeInstallPathError naming the offending path and its link target.
- Use COPYFILE_EXCL on fresh creates. Intentional overwrites unlink
  first to defeat symlink-swap TOCTOU between lstat and copyFile.

Codex finding #6 (MEDIUM): two callers in the same process calling
appendAuditRecord with different-looking paths to the same directory
used different writeQueues keys, breaking the per-process hash-chain
serialization the file header promised.

- Normalize baseDir with path.resolve + best-effort realpath at function
  entry; cache resolved keys at module scope.

Codex finding #8 (MEDIUM): fs.rename on Windows throws EEXIST when the
destination exists. rea init could not update an existing
.claude/settings.json on Windows.

- Try rename; on EEXIST/EPERM, unlink dest and retry. No runtime dep
  added.

Codex finding #9 (MEDIUM): the hooksPath regex matched any
hooksPath = X line anywhere in .git/config regardless of section, so a
[worktree] or [alias] block with the key redirected the installer.

- Shell out to git config --get core.hooksPath via execFile. Fall back
  to .git/hooks when unset or errored.

Adds symlink-refusal tests, concurrent-append serialization tests,
Windows rename-retry simulation, and section-aware hooksPath tests.
All quality gates green: type-check, 64/64 tests, lint, build.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>
himerus pushed a commit that referenced this pull request Apr 18, 2026
…ites against ancestor changes

Codex round-2 finding R2-3 (HIGH): the round-1 fix for finding #6 added
a resolvedBaseDirCache keyed by the raw baseDir string. path.resolve('.')
reads process.cwd() at call time, but cache hits skipped re-resolution.
A long-lived process calling appendAuditRecord('.', ...) before and
after process.chdir() would append to the first cwd's audit log even
after the chdir — audit events routed to the wrong hash chain.

- Remove resolvedBaseDirCache entirely. path.resolve + fs.realpath are
  cheap; audit append is not a hot path. writeQueues (the actual
  correctness fix from round 1) stays, keyed by the resolved path.
- Regression test: chdir between two appendAuditRecord('.') calls and
  assert each record lands in the correct directory.

Codex round-2 finding R2-4 (MEDIUM): the round-1 symlink refusal fix
validated paths but copyFile/unlink dereferenced strings later. A
concurrent attacker with write access inside the install root could
swap an ancestor directory for a symlink between validation and write.
COPYFILE_EXCL anchored only the leaf.

- Snapshot the ancestor chain with realpath + lstat mtime after
  assertSafeDestination.
- Re-verify immediately before every unlink and before the terminal
  write; any ancestor change raises UnsafeInstallPathError with
  kind: 'ancestor-changed'.
- Replace copyFile with openSync(O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW)
  + fs.write. O_EXCL races safely; O_NOFOLLOW refuses any symlink that
  sneaks in at the leaf.
- Deterministic tests for both the ancestor-change detection and the
  O_NOFOLLOW leaf refusal. The end-to-end race (attacker swaps during
  live install) is skipped with a documented reason: single-process
  vitest cannot deterministically drive such a race without real
  multi-process timing coordination.

Residual risk: sub-millisecond window between ancestor re-verify and
the open syscall. Documented in the copy.ts header comment.

All quality gates green: 68/68 tests pass (1 intentional skip),
type-check clean, lint clean, build clean.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>
himerus pushed a commit that referenced this pull request Apr 18, 2026
…correct git config read

Codex finding #5 (HIGH): rea init followed destination symlinks via
copyFile + chmod, so a malicious symlink at e.g. .claude/hooks/foo.sh
could redirect a subsequent --force install to an arbitrary path and
chmod its target 0o755.

- Resolve install root once with realpath; assert every destination
  resolves inside it.
- lstat every destination before writing. Any symlink raises
  UnsafeInstallPathError naming the offending path and its link target.
- Use COPYFILE_EXCL on fresh creates. Intentional overwrites unlink
  first to defeat symlink-swap TOCTOU between lstat and copyFile.

Codex finding #6 (MEDIUM): two callers in the same process calling
appendAuditRecord with different-looking paths to the same directory
used different writeQueues keys, breaking the per-process hash-chain
serialization the file header promised.

- Normalize baseDir with path.resolve + best-effort realpath at function
  entry; cache resolved keys at module scope.

Codex finding #8 (MEDIUM): fs.rename on Windows throws EEXIST when the
destination exists. rea init could not update an existing
.claude/settings.json on Windows.

- Try rename; on EEXIST/EPERM, unlink dest and retry. No runtime dep
  added.

Codex finding #9 (MEDIUM): the hooksPath regex matched any
hooksPath = X line anywhere in .git/config regardless of section, so a
[worktree] or [alias] block with the key redirected the installer.

- Shell out to git config --get core.hooksPath via execFile. Fall back
  to .git/hooks when unset or errored.

Adds symlink-refusal tests, concurrent-append serialization tests,
Windows rename-retry simulation, and section-aware hooksPath tests.
All quality gates green: type-check, 64/64 tests, lint, build.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>
himerus pushed a commit that referenced this pull request Apr 18, 2026
…ites against ancestor changes

Codex round-2 finding R2-3 (HIGH): the round-1 fix for finding #6 added
a resolvedBaseDirCache keyed by the raw baseDir string. path.resolve('.')
reads process.cwd() at call time, but cache hits skipped re-resolution.
A long-lived process calling appendAuditRecord('.', ...) before and
after process.chdir() would append to the first cwd's audit log even
after the chdir — audit events routed to the wrong hash chain.

- Remove resolvedBaseDirCache entirely. path.resolve + fs.realpath are
  cheap; audit append is not a hot path. writeQueues (the actual
  correctness fix from round 1) stays, keyed by the resolved path.
- Regression test: chdir between two appendAuditRecord('.') calls and
  assert each record lands in the correct directory.

Codex round-2 finding R2-4 (MEDIUM): the round-1 symlink refusal fix
validated paths but copyFile/unlink dereferenced strings later. A
concurrent attacker with write access inside the install root could
swap an ancestor directory for a symlink between validation and write.
COPYFILE_EXCL anchored only the leaf.

- Snapshot the ancestor chain with realpath + lstat mtime after
  assertSafeDestination.
- Re-verify immediately before every unlink and before the terminal
  write; any ancestor change raises UnsafeInstallPathError with
  kind: 'ancestor-changed'.
- Replace copyFile with openSync(O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW)
  + fs.write. O_EXCL races safely; O_NOFOLLOW refuses any symlink that
  sneaks in at the leaf.
- Deterministic tests for both the ancestor-change detection and the
  O_NOFOLLOW leaf refusal. The end-to-end race (attacker swaps during
  live install) is skipped with a documented reason: single-process
  vitest cannot deterministically drive such a race without real
  multi-process timing coordination.

Residual risk: sub-millisecond window between ancestor re-verify and
the open syscall. Documented in the copy.ts header comment.

All quality gates green: 68/68 tests pass (1 intentional skip),
type-check clean, lint clean, build clean.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>
himerus added a commit that referenced this pull request Apr 18, 2026
#14)

* feat(audit): add metadata field and public @bookedsolid/rea/audit helper

Attach optional metadata to the AuditRecord schema and emit caller-supplied
keys from ctx.metadata through the audit middleware (skipping the reserved
autonomy_level key kept for internal bookkeeping).

Add src/audit/append.ts as a standalone helper that reads the tail of
.rea/audit.jsonl for prev_hash, computes the SHA-256 hash, and appends
atomically with fsync. Exported as @bookedsolid/rea/audit so the
codex-adversarial agent and downstream consumers (Helix helix.plan /
helix.apply, future plugins) can emit structured events through the same
hash chain.

Add src/audit/codex-event.ts as the single source of truth for the
codex.review event shape, shared between the TypeScript helper and the
push-review-gate shell hook.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(hooks): enforce Codex adversarial review on protected-path pushes

Extend push-review-gate.sh to block git push when the diff touches any of
src/gateway/middleware/, hooks/, src/policy/, or .github/workflows/ unless
.rea/audit.jsonl contains a codex.review entry for the current HEAD. The
grep pattern matches the constants in src/audit/codex-event.ts — keep both
in lockstep if either changes.

Document the audit-append responsibility in agents/codex-adversarial.md
with a concrete example using the public @bookedsolid/rea/audit helper.

Deliberate non-action on commit-review-gate: commit-side enforcement would
double friction without adding safety, since nothing lands remote without
passing the push gate. The rationale is captured in the push-gate header
so a future reader does not 'fix' the missing commit-side check.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(gateway): MCP server, downstream pool, registry loader, smoke tests

Implement the rea serve gateway on top of @modelcontextprotocol/sdk 1.29:

- src/registry/{types,loader}.ts — zod-validated RegistrySchema with the
  same TTL + mtime-invalidation cache pattern as src/policy/loader.ts.
  Server names constrained to lowercase-kebab.
- src/gateway/downstream.ts — per-server DownstreamConnection wrapping a
  Client + StdioClientTransport pair. One reconnect on transport error,
  then mark unhealthy and let the circuit-breaker middleware take over.
- src/gateway/downstream-pool.ts — Map<serverName, DownstreamConnection>
  with <serverName>__<toolName> prefix routing. Split on first __ so
  downstream tools that themselves contain __ still work.
- src/gateway/server.ts — upstream Server bound to the full 10-layer
  middleware chain: audit → kill-switch → tier → policy → blocked-paths →
  rate-limit → circuit-breaker → injection → redact → result-size-cap →
  terminal. Zero-server mode boots cleanly with an empty catalog.
- src/gateway/session.ts — per-process UUID session_id stable for the
  lifetime of rea serve.
- server.test.ts — smoke tests via InMemoryTransport covering zero-server
  listTools, zero-server callTool denied, HALT denial, and tier
  classification.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(cli): rewrite `rea serve` as real MCP gateway with graceful shutdown

- `src/cli/serve.ts` loads `.rea/policy.yaml` + `.rea/registry.yaml`, creates
  the gateway, and connects StdioServerTransport. SIGTERM / SIGINT drain
  in-flight work and close the downstream pool before exit.
- `src/cli/index.ts` adds `--force` and `--accept-dropped-fields` flags on
  `rea init` (consumed by the upcoming install pipeline).

Zero-server registries boot cleanly and advertise an empty tool catalog so
first-run does not crash.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(policy): layered profile schema and five shipped profiles

- `src/policy/profiles.ts` introduces a zod-strict `ProfileSchema` with all
  fields optional, the `HARD_DEFAULTS` layer, and `mergeProfiles` /
  `loadProfile` helpers. Merge order is `hardDefaults ← profile ←
  reagentTranslation ← wizardAnswers` so each later layer can only narrow the
  preceding one (autonomy ceilings always clamp).
- Five profiles under `profiles/`: `minimal`, `bst-internal` (what this repo
  dogfoods), `open-source`, `client-engagement`, and `lit-wc`. Each is a
  literal fragment — no `extends` chains — so the materialized
  `.rea/policy.yaml` on disk is the full source of truth for what the
  middleware enforces.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(cli): install pipeline — copy, settings merge, commit-msg, claude-md, reagent

Makes `rea init` a real installer instead of a stub. New modules under
`src/cli/install/`:

- `copy.ts` — copies `hooks/**`, `commands/**`, `agents/**` into `.claude/`,
  chmods hooks `0o755`, conflict policy per flag (`--force` overwrites,
  `--yes` skips existing, otherwise interactive prompt).
- `settings-merge.ts` — pure merge into `.claude/settings.json`; never
  silently overwrites consumer hooks; warns only when chaining onto a
  pre-existing matcher (novel-matcher additions on a fresh install produce
  exactly one informational notice per matcher, not per hook).
- `commit-msg.ts` — belt-and-suspenders install of `.git/hooks/commit-msg`
  (and `.husky/commit-msg` when husky is present); respects
  `core.hooksPath`.
- `claude-md.ts` — managed fragment inside `CLAUDE.md` delimited by
  `<!-- rea:managed:start v=1 -->` / `<!-- rea:managed:end -->`; content
  outside the markers is never touched.
- `reagent.ts` — field-for-field translator with explicit copy / drop /
  ignore lists. Drop-list fields refuse translation without
  `--accept-dropped-fields` to prevent silent security downgrades; autonomy
  is clamped to the profile ceiling.

Each module ships with vitest coverage (`copy.test.ts`,
`settings-merge.test.ts`, `reagent.test.ts`).

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(cli): wire init, expand doctor to 9 checks, changeset for 0.2.0

- `src/cli/init.ts` is rewritten to drive the full install pipeline: load
  profile, optionally translate an existing reagent policy, merge wizard
  answers, materialize `.rea/policy.yaml` as a literal, copy artifacts via
  the new install modules, merge settings atomically, install the
  commit-msg hook, and update the CLAUDE.md managed fragment.
- `src/cli/doctor.ts` grows to 9 checks (`.rea` dir, policy parses,
  registry parses, agents count, hook executability, settings matchers
  present, commit-msg hook installed, codex-adversarial agent + command,
  registry parse roundtrip). Exit code reflects the worst check.
- `package.json` adds the `./audit` subpath export (public API for the
  adversarial-review helper) and includes `.husky/` in `files[]` so the
  husky source ships to consumers.
- `.changeset/0.2.0-mvp.md` — minor bump documenting Tracks 1/2/3 and the
  explicit deferrals to the full 0.2.0 cycle.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* docs(codex): use colon-form slash commands (/codex:review, /codex:adversarial-review)

The Codex plugin exposes commands as /codex:review and /codex:adversarial-review.
Our docs were using space-form which would break invocation.

Note: THREAT_MODEL.md has one remaining occurrence of the old form but is in
blocked_paths and requires a direct maintainer edit.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* fix(hooks): close three push-review-gate bypasses surfaced by codex

- Replace +++ patch-header scrape with git diff --name-status so file
  DELETIONS under protected paths also require Codex review (#1).
- Parse push refspecs correctly: split on ':', take destination, strip
  'refs/heads/' / 'refs/for/', reject bare 'HEAD' as target. Prior regex
  let 'git push origin HEAD:main' collapse diff to empty (#2).
- Replace two-grep audit scan with jq -e structural predicate enforcing
  tool_name == "codex.review" AND metadata.head_sha == $sha AND
  metadata.verdict not in {blocking, error}. Prior greps accepted any
  audit line with matching substrings inside arbitrary metadata (#3).
- Fail-closed on every parse error. jq still guarded at hook entry.

Updates codex-event.ts docstring to describe the jq predicate instead
of the old substring match (which is now actively misleading).

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* fix(gateway): restrict downstream child env and reset reconnect per episode

Codex finding #4 (HIGH): every MCP child spawned from .rea/registry.yaml
inherited the operator's full process environment — OPENAI_API_KEY,
GITHUB_TOKEN, customer secrets, everything. Registry is an attacker-writable
surface in shared / CI contexts.

- Default to a hardcoded allowlist of neutral env vars (PATH, HOME,
  LANG, NODE_*, TMPDIR, etc.).
- New optional RegistryServer.env_passthrough: string[] opts specific
  additional names into the forwarded set. Names matching
  /(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)/i are refused at schema-parse
  time — the explicit server.env is the escape hatch for operator-typed
  secrets.
- Merge order: allowlist then passthrough then explicit. Undefined host
  vars are skipped (no "undefined" string serialization).

Codex finding #7 (MEDIUM): reconnectAttempted never reset after success,
so one reconnect was one-per-object-lifetime, not one-per-failure-episode
as documented.

- Reset reconnectAttempted on successful reconnect+retry.
- 30s flap-guard: refuse to reconnect a second time within that window,
  mark unhealthy so circuit breaker takes over.
- JSDoc updated to match actual semantics.

THREAT_MODEL.md update (env-inheritance policy documentation) owed — file
is in blocked_paths and needs a direct maintainer edit.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* fix(install): reject symlink destinations, portable atomic settings, correct git config read

Codex finding #5 (HIGH): rea init followed destination symlinks via
copyFile + chmod, so a malicious symlink at e.g. .claude/hooks/foo.sh
could redirect a subsequent --force install to an arbitrary path and
chmod its target 0o755.

- Resolve install root once with realpath; assert every destination
  resolves inside it.
- lstat every destination before writing. Any symlink raises
  UnsafeInstallPathError naming the offending path and its link target.
- Use COPYFILE_EXCL on fresh creates. Intentional overwrites unlink
  first to defeat symlink-swap TOCTOU between lstat and copyFile.

Codex finding #6 (MEDIUM): two callers in the same process calling
appendAuditRecord with different-looking paths to the same directory
used different writeQueues keys, breaking the per-process hash-chain
serialization the file header promised.

- Normalize baseDir with path.resolve + best-effort realpath at function
  entry; cache resolved keys at module scope.

Codex finding #8 (MEDIUM): fs.rename on Windows throws EEXIST when the
destination exists. rea init could not update an existing
.claude/settings.json on Windows.

- Try rename; on EEXIST/EPERM, unlink dest and retry. No runtime dep
  added.

Codex finding #9 (MEDIUM): the hooksPath regex matched any
hooksPath = X line anywhere in .git/config regardless of section, so a
[worktree] or [alias] block with the key redirected the installer.

- Shell out to git config --get core.hooksPath via execFile. Fall back
  to .git/hooks when unset or errored.

Adds symlink-refusal tests, concurrent-append serialization tests,
Windows rename-retry simulation, and section-aware hooksPath tests.
All quality gates green: type-check, 64/64 tests, lint, build.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* fix(hooks): use pre-push stdin contract and allowlist verdict predicate

Codex round-2 finding R2-1 (HIGH): the round-1 refspec parser extracted
only the dst side and then diffed "$MERGE_BASE"...HEAD. A user on
branch foo pushing "git push origin hotfix:main" had the gate review
foo's commits against main, not hotfix's — protected-path changes on
hotfix evaded the gate entirely.

- Read git's real pre-push stdin contract: lines of
  <local_ref> <local_sha> <remote_ref> <remote_sha>
- Use local_sha as the source commit for the diff.
- Use remote_sha as the merge base when the remote already has the ref;
  fall back to merge-base with target / main for new branches.
- Argv parser kept as fallback for manual testing; it also now resolves
  src^{commit} for src:dst, not HEAD.
- Multi-refspec pushes iterate all refspecs and pick the one with the
  largest diff so a mixed push cannot hide large commits behind a
  trivial refspec.
- All-zero local_sha (branch delete) refspecs are tracked separately;
  a delete-only push fails closed with an explicit block message.
- macOS bash 3.2 compatible (no namerefs).

Codex round-2 finding R2-2 (HIGH): the jq predicate used a blocklist
(`.metadata.verdict != "blocking" and != "error"`). Missing verdict
yielded jq null, which compares != to any string and passes — a forged
record with just head_sha set satisfied the gate.

- Flip to allowlist: `.metadata.verdict == "pass" or == "concerns"`.
- null / missing / unknown verdicts all correctly fail.

shellcheck clean, syntax-checked, parse tests passing.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* fix(audit,install): remove cwd-aware baseDir cache; anchor install writes against ancestor changes

Codex round-2 finding R2-3 (HIGH): the round-1 fix for finding #6 added
a resolvedBaseDirCache keyed by the raw baseDir string. path.resolve('.')
reads process.cwd() at call time, but cache hits skipped re-resolution.
A long-lived process calling appendAuditRecord('.', ...) before and
after process.chdir() would append to the first cwd's audit log even
after the chdir — audit events routed to the wrong hash chain.

- Remove resolvedBaseDirCache entirely. path.resolve + fs.realpath are
  cheap; audit append is not a hot path. writeQueues (the actual
  correctness fix from round 1) stays, keyed by the resolved path.
- Regression test: chdir between two appendAuditRecord('.') calls and
  assert each record lands in the correct directory.

Codex round-2 finding R2-4 (MEDIUM): the round-1 symlink refusal fix
validated paths but copyFile/unlink dereferenced strings later. A
concurrent attacker with write access inside the install root could
swap an ancestor directory for a symlink between validation and write.
COPYFILE_EXCL anchored only the leaf.

- Snapshot the ancestor chain with realpath + lstat mtime after
  assertSafeDestination.
- Re-verify immediately before every unlink and before the terminal
  write; any ancestor change raises UnsafeInstallPathError with
  kind: 'ancestor-changed'.
- Replace copyFile with openSync(O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW)
  + fs.write. O_EXCL races safely; O_NOFOLLOW refuses any symlink that
  sneaks in at the leaf.
- Deterministic tests for both the ancestor-change detection and the
  O_NOFOLLOW leaf refusal. The end-to-end race (attacker swaps during
  live install) is skipped with a documented reason: single-process
  vitest cannot deterministically drive such a race without real
  multi-process timing coordination.

Residual risk: sub-millisecond window between ancestor re-verify and
the open syscall. Documented in the copy.ts header comment.

All quality gates green: 68/68 tests pass (1 intentional skip),
type-check clean, lint clean, build clean.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* fix(install): reject ancestor symlinks and escapes in snapshotAncestors

Codex round-3 finding R3-1 (HIGH): the round-2 snapshotAncestors
recorded ancestor realpaths but did not assert they resolved under the
install root. An attacker swapping .claude/hooks for a symlink to
/tmp/decoy before the snapshot had that state accepted as baseline;
verifyAncestorsUnchanged then passed, and writeFileExclusiveNoFollow
wrote the file to /tmp/decoy. O_NOFOLLOW protects only the leaf, not
ancestor components — a confirmed install-root escape primitive.

- snapshotAncestors now lstats each ancestor and refuses symlinks with
  UnsafeInstallPathError { kind: 'symlink' }.
- After realpath resolution, every ancestor must equal resolvedRoot or
  begin with resolvedRoot + path.sep. Otherwise
  UnsafeInstallPathError { kind: 'escape' }.
- Walk must terminate at resolvedRoot; hitting filesystem root without
  matching raises escape (belt-and-suspenders against upstream bugs).
- Two deterministic regression tests: ancestor-symlink-inside-root and
  ancestor-escape-outside-root. Both verify the decoy target stays
  untouched.

All quality gates green: 70/70 tests pass (1 intentional skip),
type-check clean, lint clean, build clean.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* fix(hooks): fail-closed on unresolvable remote refs and parse --delete correctly

Codex round-3 finding R3-2 (HIGH): the round-2 merge-base fallback
`git merge-base X Y 2>/dev/null || echo $remote_sha` wrote the literal
SHA string to $mb when Y was not in the local object DB (the common
stale-checkout case). Downstream `rev-list` and `git diff` failed on
the invalid ref, errors swallowed by `|| echo` defaults, DIFF_FULL
collapsed to empty, and the gate exited 0 with no review — full
bypass for any push whose remote tip the local repo hadn't seen.

- Probe `git cat-file -e "${remote_sha}^{commit}"` before merge-base
  when remote_sha != ZERO_SHA. Missing object → exit 2 with fetch
  remediation message.
- Capture merge-base, rev-list --count, and git diff exit statuses
  explicitly. Non-zero → exit 2. Empty output only reaches the
  legitimate no-op push path when git exited 0.

Codex round-3 finding R3-3 (MEDIUM): resolve_argv_refspecs treated
`-*` tokens as flags to skip unconditionally, so
`git push --delete origin doomed` skipped --delete, consumed origin
as remote, and processed doomed as a normal push. HAS_DELETE never
fired; a destructive deletion was reviewed as an ordinary commit.

- Explicit --delete / -d / --delete=VALUE cases set delete_mode=1.
- Subsequent bare refspecs tagged with __REA_DELETE__ sentinel in the
  same refspec array (no second array; macOS bash 3.2 compat).
- Emission loop strips the sentinel and emits
  ZERO|ZERO|(delete)|refs/heads/<dst> matching the git pre-push stdin
  contract. Existing HAS_DELETE block fail-closes on delete-only
  pushes — no downstream change.

shellcheck clean. All edge cases verified (normal push, src:dst,
upstream inference, legacy :doomed syntax, HEAD-target block).
All quality gates green: 70/71 tests pass (1 intentional skip),
type-check clean, lint clean, build clean.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(hooks): land G11.1 audited escape hatch for push-review-gate

Pulled G11.1 from the 0.3.0 resilience plan into 0.2.0 after round-4
Codex review hit an account rate limit. Without an escape hatch, a
Codex outage blocks every push that touches a protected path — turning
an availability failure of an external service into a hard-stop on
local development. The escape hatch preserves the audit contract while
allowing a push to proceed when the reviewer is unavailable.

Contract: set REA_SKIP_CODEX_REVIEW to a non-empty reason string.
Empty / unset = no bypass, gate enforces as before. The reason is
written verbatim into the audit record so every skip leaves a
durable, hash-chained explanation that `git blame`, auditors, and
future reviewers can find.

Implementation details
- New block runs inside the protected-path branch, before the existing
  Codex-audit grep. When REA_SKIP_CODEX_REVIEW is non-empty:
  1. Emit a loud stderr banner so the skip is impossible to miss in
     terminal output.
  2. Compute files_changed count from DIFF_FULL for the audit record.
  3. Resolve actor from `git config user.email || user.name`. Fail
     closed (exit 2) if neither is set — an unattributable skip is
     worse than a blocked push.
  4. Require `dist/audit/append.js` to exist. Fail closed if missing
     — we never want to "skip the audit skip record."
  5. Invoke the existing append helper via `node --input-type=module`
     with tool_name "codex.review.skipped" (note the distinct name).
  6. Exit 0 only on successful audit append; any non-zero node exit
     fails closed.

- tool_name is deliberately "codex.review.skipped", NOT "codex.review".
  The push-review-gate grep / jq predicate for codex.review records
  must never match a skip record, or the gate would become a no-op
  for any future push on the same HEAD. Two distinct event names, one
  shared hash chain.

- jq is used to serialize the audit-record fields, so reasons
  containing quotes, newlines, or shell metacharacters cannot break
  out of the JSON context. Fail closed if jq is missing or serialize
  returns non-zero.

- Metadata recorded: head_sha, reason (verbatim REA_SKIP_CODEX_REVIEW
  value), actor, verdict "skipped", files_changed (integer count of
  changed files in the protected paths diff).

Tests (__tests__/hooks/push-review-gate-escape-hatch.test.ts, 8 cases)
- Fail-closed when dist/audit/append.js is missing
- Fail-closed when no git user.email or user.name is configured
- Happy path: exit 0, banner present, audit record has every expected
  field with correct values
- Reason=="1" literal when caller sets REA_SKIP_CODEX_REVIEW=1 — no
  implicit "default" reason; whatever the caller types is what gets
  logged, including short values
- Skip record does NOT satisfy the existing codex.review jq predicate
  — regression guard against collapsing the two event names
- Sanity: gate still blocks (exit 2) when env var is unset
- Sanity: gate still blocks when env var is empty string
- Sanity: dist/audit/append.js present in-repo

shellcheck clean. 78 tests pass (13 files, 1 pre-existing skip).
Lint, type-check, build all clean.

First use is this push itself: Codex is rate-limited until
2026-04-25, the round-3 fixes (R3-1, R3-2, R3-3) were applied per
spec but not adversarially verified, and the escape hatch is the
mechanism that audits its own introduction.

Follow-ups still owed (0.3.0):
- G11.2 pluggable Claude-self reviewer (fallback review path)
- G11.3 startup probe for Codex availability
- G11.5 telemetry on skip frequency
- THREAT_MODEL.md: document the escape hatch as an audited gate
  weakening (requires direct maintainer edit — blocked_paths)
- .claude/hooks/push-review-gate.sh dogfood mirror resync

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* docs(changeset): add G11.1 escape hatch to 0.2.0 notes

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(reviewers): land AdversarialReviewer interface + Codex adapter stub

G11.2 step 1 of 3 — introduces the pluggable adversarial-reviewer
contract the push gate will eventually dispatch through.

- src/gateway/reviewers/types.ts: ReviewVerdict / ReviewFinding /
  ReviewResult / ReviewRequest / AdversarialReviewer shared shapes
- src/gateway/reviewers/codex.ts: CodexReviewer adapter. isAvailable()
  probes `codex --version` with a 2s timeout; review() throws by design
  because the real path is the codex-adversarial agent, not a TS call
- Unit tests cover exec success/ENOENT/timeout/non-zero, version
  caching, and the documented review() throw

No behavior change — nothing wires these in yet. G11.2 steps 2 and 3
add ClaudeSelfReviewer and the selector; G11.3/G11.4 adopt them.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(reviewers): add ClaudeSelfReviewer fallback

G11.2 step 2 of 3 — the real runtime fallback when Codex is unreachable.
Not a cross-model check, so every result is flagged degraded=true so the
audit log is honest about what actually ran.

- src/gateway/reviewers/claude-self.ts: one-shot Opus call in a fresh
  context with a review-only system prompt. Parses STRICT JSON matching
  ReviewResult; verdict=error on parse failure, APIError (429/5xx), or
  network error. Caps diff at 200KB and notes truncation in the summary
- Pin reviewer_version to claude-opus-4-7 so audit entries stay
  reproducible across future model bumps
- Always pins degraded=true even if the model tries to overwrite it
- Adds @anthropic-ai/sdk@^0.90.0 (verified via npm view). Only new
  dependency this task touches
- 16 new unit tests exercising isAvailable, success path, malformed
  findings drop, error paths (missing key, unparseable, bad verdict,
  APIError, generic Error), and truncation

Not yet wired into the selector or push gate — that lands in step 3.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(reviewers): add selector + policy/registry schema hooks

G11.2 step 3 of 3 — ties the interface, CodexReviewer, and
ClaudeSelfReviewer together behind a single selectReviewer() entry
point with audit-friendly (degraded, reason) signals. No caller wired
yet — push-review-gate integration is G11.3/G11.4 work per the spec.

- src/gateway/reviewers/select.ts: precedence is env REA_REVIEWER >
  registry.reviewer > policy.review.codex_required=false > default
  (Codex first, fall back to claude-self degraded=true) > throw
  NoReviewerAvailableError pointing at the G11.1 escape hatch
- Policy schema: new optional review.codex_required boolean; strict on
  unknown nested fields so typos fail loudly
- Registry schema: new optional top-level reviewer enum ('codex' |
  'claude-self'); unknown values rejected at parse time
- 16 new selector tests cover the full precedence table, the unknown-
  env-var rejection, the NoReviewerAvailableError path, and the
  policy-first no-Codex case (first-class, NOT degraded). Policy and
  registry loader tests gain a block for the new fields — all
  backwards-compatible

Total delta: 78/1 skipped -> 122/1 skipped. Lint + type-check + build
all green. @anthropic-ai/sdk@^0.90.0 is the only new dep.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(hooks): honor review.codex_required in push-review-gate (G11.4)

When .rea/policy.yaml sets review.codex_required: false, the protected-path
Codex adversarial-review gate is skipped entirely. The REA_SKIP_CODEX_REVIEW
escape hatch also becomes a no-op (skipping a review that isn't required is
not meaningful).

Adds src/scripts/read-policy-field.ts — a tiny standalone script that
exposes a single scalar policy field to shell hooks without dragging in the
full CLI surface. Exit codes distinguish missing (1) from malformed (2) so
callers can pick different fail modes.

Fail-closed semantics: if the helper can't parse the policy, the gate
treats codex_required as true (safer default) and logs a warning.

7 new integration tests exercise the no-codex path alongside the existing 8
escape-hatch tests, including malformed-policy and missing-policy regressions.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(doctor): conditional Codex checks under review.codex_required (G11.4)

When policy.review.codex_required is false, the two Codex-specific doctor
checks (codex-adversarial agent, /codex-review command) are replaced by a
single info line explaining why they were skipped. In the default and
explicit-true cases, the original behavior is preserved.

The curated-agents roster still expects codex-adversarial.md so flipping
codex_required back to true does not require a re-install.

Extracts `collectChecks(baseDir)` as a testable seam and adds a `info`
status kind for purely advisory lines that never contribute to exit code.

4 new unit tests cover both modes plus the absent-field regression.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(profiles): add bst-internal-no-codex and open-source-no-codex (G11.4)

Two new profile variants that carry every setting from their parent
(bst-internal / open-source) but are designed to default
review.codex_required: false at init time. The profile YAMLs themselves
don't emit the review block — that's written by the init flow based on
the profile name — but the leading comment documents the coupling.

Each file explains when the variant is appropriate and how to re-enable
Codex later (edit .rea/policy.yaml, flip codex_required to true).

Profile registry discovery is file-based (loadProfile checks for
profiles/<name>.yaml), so simply adding these files makes them
available; the allowlist in src/cli/init.ts is updated in the
accompanying init change.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(init): add --codex / --no-codex flags and wizard prompt (G11.4)

rea init now writes an explicit review.codex_required field into every
.rea/policy.yaml it creates. The value is resolved in this order:

  1. Explicit --codex / --no-codex flag (commander's boolean-with-negation
     pair) wins unconditionally.
  2. Otherwise derive from the chosen profile name — profiles ending in
     `-no-codex` default to false, everything else defaults to true.
  3. Interactive mode prompts for a final confirmation, seeded with the
     flag/profile default.

Adds `bst-internal-no-codex` and `open-source-no-codex` to the profile
allowlist (the YAMLs were added in the previous commit).

When the resolved value is false, the CLI prints a durable notice after
install pointing at the exact knob (`review.codex_required: true`) the
operator would flip to re-enable Codex later. A TODO comment in the same
block flags the coupling with a future G6-style Codex install assist.

7 new non-interactive init tests cover the flag combinations and confirm
the written policy parses via the strict loader (catching any key typo
in the emitted YAML).

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* docs: document G11.4 + G11.2 in 0.2.0 changeset and CLAUDE.md

Changeset picks up the actual surface that landed for G11.4 (push gate,
doctor, init flow, two new profile variants, 18 new tests) and records
G11.2 which was missing from the original draft.

CLAUDE.md profile listing now enumerates the -no-codex variants and
explains what they actually change at init time. Non-Negotiable Rules
section is untouched.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* fix(kill-switch): enforce single-shot HALT read with fail-closed errno handling (G4)

Close the TOCTOU gap between the middleware's HALT check and the downstream
terminal. The previous implementation called `stat` → `lstat` → `open` as a
three-syscall sequence, creating a window in which HALT state could change
between the decision and the read.

The rewrite issues exactly ONE syscall per invocation on the HALT file:
`fs.open(path, O_RDONLY)`. The decision is derived entirely from the open
outcome:

  * ENOENT          → HALT absent → proceed with the chain.
  * open succeeds   → HALT present → deny. A best-effort read populates the
                      reason string (capped at 1024 bytes); the read does
                      NOT influence the decision.
  * any other errno → unknown state → deny (fail-closed).

Semantic guarantee codified in the module-level doc block: HALT is evaluated
exactly once per invocation, at chain entry. A call that passes that check
runs to completion; a call that fails it is denied. Creating .rea/HALT
mid-flight does NOT cancel in-flight invocations — it blocks subsequent
invocations only. This matches standard kill-switch semantics (SIGTERM after
acceptance: the process continues).

The decision is recorded on `ctx.metadata.halt_decision` (absent | present |
unknown) and `ctx.metadata.halt_at_invocation` (ISO-8601 timestamp when
present, null otherwise). The audit middleware already forwards arbitrary
ctx.metadata keys into the hash-chained record, so every audit row now
carries the HALT decision that governed it.

THREAT_MODEL.md §5.7 needs a corresponding update to replace the "theoretical
TOCTOU on shared filesystems" residual risk with the explicit semantic
guarantee. THREAT_MODEL.md is in blocked_paths, so the proposed paragraph
is drafted to /tmp/halt-semantic-update.md for the maintainer to apply.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* test(kill-switch): cover TOCTOU, concurrency, and errno fail-closed paths (G4)

Six new tests exercise the single-shot HALT semantic from every angle:

  1. HALT created between chain start and terminal — the test's "next()"
     writes .rea/HALT mid-flight, yields a tick, and asserts the invocation
     still completes. Proves the middleware never re-checks.

  2. HALT removed mid-invocation — HALT present at entry → denied. Removing
     HALT after the middleware returns does NOT rescue the call; the
     terminal never runs.

  3. Per-invocation decisions, never cached — invocation 1 sees HALT
     (denied), HALT is removed, invocation 2 sees it absent (allowed).
     Two separate decisions.

  4. ENOENT regression — HALT absent → next() runs, status stays Allowed.

  5. Non-ENOENT errno → fail-closed — HALT exists with mode 0o000. On a
     non-root user the open fails with EACCES → decision 'unknown' →
     denial. On root, open succeeds → decision 'present' → still denied.
     Terminal never runs in either case.

  6. Concurrency matrix — 10 invocations across a HALT toggle. First batch
     of 5 runs with HALT absent (all allowed); HALT is then written; second
     batch of 5 (all denied). Each invocation's decision reflects the state
     at ITS own chain entry, not a shared snapshot.

The existing "HALT is a directory" test updated to assert
platform-invariant denial (Linux: open on a dir succeeds → 'present';
macOS: open returns EISDIR → 'unknown'; both deny). Existing
"caps HALT read size" test updated to also assert halt_decision.

Test count delta: +6 (140 → 146 pass, 1 skip unchanged).

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(redact-safe): add wrapRegex with worker-based timeout bound (G3)

Adds `src/gateway/redact-safe/match-timeout.ts` — a synchronous `SafeRegex`
wrapper that bounds every regex `test`/`replace` to a configurable wall-clock
budget (default 100ms).

Implementation: Option A — worker thread per exec.

  - No native dependency (vs `re2`, which would add a build step and a
    second regex dialect).
  - Hard timeout: on expiry the parent calls `worker.terminate()`, which
    reliably kills a catastrophic backtracker.
  - Overhead ~1ms per call. Acceptable for gateway payloads today; worker
    pooling is a future-proofing option for 0.3.0 that would not change
    the public `SafeRegex` surface.

Synchronization: the parent blocks on `Atomics.wait` over a
SharedArrayBuffer while the worker computes. The worker writes its reply
to a MessageChannel port (transferred via `transferList`), then stores `1`
into the SAB and calls `Atomics.notify`. The parent wakes, drains the
reply port via `receiveMessageOnPort`, and terminates the worker. This
keeps `.test()` / `.replace()` synchronous so they remain a drop-in
replacement for `RegExp.prototype.test` / `.replace` inside the existing
middleware tight loops.

On timeout: `.test()` returns `{matched: false, timedOut: true}` and
`.replace()` returns `{output: input (unchanged), timedOut: true}`. An
optional `onTimeout` callback fires exactly once and its errors are
swallowed so a bad logger cannot break middleware. The caller (redact /
injection middleware — added in the next commit) is responsible for
emitting the audit event with size+pattern-id only, never the input text.

Tests cover: benign match/replace, catastrophic `(a+)+$` pattern against
`"a".repeat(25) + "X"` timing out within 2× the budget, replace returning
input unchanged on timeout, `onTimeout` fire-exactly-once, callback error
swallowing, default 100ms timeout, and `.pattern` passthrough.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(redact,injection): route default and user patterns through wrapRegex (G3)

Every regex the redact and injection middleware layers run against
untrusted MCP payloads now goes through the `SafeRegex` wrapper from G3.
Per-call timeouts mean a catastrophic backtracker can no longer hang the
gateway — the worker is terminated, the offending value is replaced with
a sentinel, and an audit event is emitted.

Changes:

- `src/gateway/middleware/redact.ts`:
  - `SECRET_PATTERNS` is now exported (required by the CI lint:regex
    check added in the next commit).
  - New `createRedactMiddleware({ matchTimeoutMs?, userPatterns? })`
    factory. Defaults preserve current behavior (100ms budget). Users
    from policy-loaded patterns are a first-class input.
  - `redactSecrets` takes compiled patterns + optional `onTimeout`
    callback. On timeout the entire field is replaced with the sentinel
    `[REDACTED: pattern timeout]` — the scanner never lets an un-scanned
    value escape. Scanning short-circuits on the offending pattern.
  - Timeout audit events are pushed into `ctx.metadata` under the key
    `redact.regex_timeout` as an array of
    `{event, pattern_source, pattern_id, input_bytes, timeout_ms}`.
    The input text is NEVER written — only its UTF-8 byte length.
  - The exported `redactMiddleware` constant is preserved for
    back-compat; `createRedactMiddleware()` is the new canonical form.

- `src/gateway/middleware/injection.ts`:
  - `INJECTION_PHRASES` now exported (for lint:regex). Added exported
    `INJECTION_BASE64_PATTERN` + `INJECTION_BASE64_SHAPE` constants — the
    two regexes this middleware runs. Both pass through `SafeRegex` now.
  - `scanForInjection` takes compiled SafeRegex bundle; patterns are
    built once per invocation via `compileInjectionPatterns`.
  - Timeout events land on `ctx.metadata` under
    `injection.regex_timeout`. Same size-only contract as redact.
  - Literal phrase matches continue to use `String.prototype.includes`
    (no regex, no ReDoS surface).

- `src/gateway/redact-safe/match-timeout.ts`:
  - Added `matchAll` op to `SafeRegex` — bounded match enumeration,
    needed so the injection middleware can extract base64 tokens without
    falling back to unbounded `String.prototype.match`. The worker forces
    the global flag so matchAll is meaningful regardless of how the
    pattern was specified.
  - Removed the unused async runner + `wrapRegexAsync` export; the sync
    surface is sufficient and matches how middleware actually calls.

Tests:

- `src/gateway/middleware/redact.test.ts` (new, 7 tests):
  redaction + sentinel + audit-metadata shape + no-input-leakage +
  invocation-continues-after-timeout + nested-object preservation.

- `scanForInjection` keeps its existing literal-phrase behavior; the
  base64 branch now uses `SafeRegex.matchAll`.

Performance note: the middleware chain still walks every string in the
result and runs N patterns × 1 worker-spawn per string. This is the
defense-in-depth cost the threat model already accepts. Worker pooling
is a 0.3.0 optimization that would not change the public surface.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(ci,policy): add lint:regex, load-time safe-regex check on user patterns (G3)

Completes the G3 defense-in-depth story. Two new enforcement points land
around the runtime timeout from the first two commits:

1. Build-time static lint. `scripts/lint-safe-regex.mjs` imports the
   compiled `SECRET_PATTERNS` and the two injection-scan constants from
   `dist/`, passes each through `safe-regex`, and exits non-zero on any
   flagged offender. Wired into `pnpm lint` as `lint:regex` and chained
   in BEFORE eslint so a bad regex short-circuits the pipeline fast.
   Running it caught one offender already — the existing "Private Key"
   pattern with nested `\s+` inside optional alternation. Tightened to
   a single-space form that matches the canonical PEM armor header
   (`-----BEGIN [TYPE ]PRIVATE KEY-----`). Non-standard whitespace in
   PEMs is not in our threat model.

2. Load-time safe-regex check on user-supplied patterns.
   `src/policy/types.ts` gains a `RedactPolicy` interface with
   `match_timeout_ms?: number` and `patterns?: UserRedactPattern[]`.
   `src/policy/loader.ts` validates each pattern via `safe-regex` at
   load time — a flagged pattern rejects the entire policy load with an
   error that names the offender. The zod schema stays strict so typos
   fail loudly. Malformed-regex-source also fails load.

Gateway wiring:

- `src/gateway/server.ts` compiles user patterns via `wrapRegex` at
  gateway-create time and passes the configured `matchTimeoutMs` to
  both `createRedactMiddleware` and `createInjectionMiddleware`. User
  patterns are appended after defaults, preserving precedence.

Tests (7 new in `src/policy/loader.test.ts`):

- accepts `redact.match_timeout_ms` + `redact.patterns` round-trip
- back-compat: `redact` undefined when not set
- rejects `(a+)+$` (safe-regex flagged)
- rejects malformed regex source (`(`)
- rejects unknown fields at the `redact.` level (strict)
- rejects unknown fields inside a `patterns` entry (strict)
- accepts a bounded user pattern end-to-end

Dev dependencies: `safe-regex@^2.1.1` + `@types/safe-regex@^1.1.6`
(verified existence + license via `npm view`).

Changeset `.changeset/0.2.0-mvp.md` gains a `## ReDoS safety (G3)`
section and G3 is removed from the deferred list.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(observability): land CodexProbe — availability polling and state (G11.3)

CodexProbe polls `codex --version` and a best-effort catalog subcommand
to expose whether the Codex CLI is reachable right now. The probe is
intentionally decoupled from the reviewer selector — it reports state
only, it never gates a review. Consumers (`rea serve` startup, `rea
doctor`) read the state and decide what to do.

Key behaviors:
- Never throws from getState(); startup never fail-closes on a probe miss.
- setInterval is .unref()'d so polling does not pin the event loop.
- onStateChange listeners fire on transitions, not on every tick.
- Concurrent probe() callers share a single in-flight exec.
- Degraded-skip path for Codex builds that don't recognize `catalog --json`,
  documented inline so a false unauthenticated flag can't creep in.

18 unit tests cover exit codes, timeouts, ENOENT, version parsing,
lifecycle, listener semantics, and the concurrency guarantee.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(serve,doctor): wire CodexProbe lifecycle and doctor check (G11.3)

On `rea serve` startup, run an initial probe when
policy.review.codex_required is not explicitly false. A failed probe
emits a stderr warn only; serving continues. The probe runs periodically
via start() and is stopped on SIGTERM/SIGINT.

`rea doctor` now runs a one-shot probe (when Codex is required) and
adds two rows: `codex.cli_responsive` (pass/warn) and
`codex.last_probe_at` (info). Probe failure does NOT fail the doctor —
it surfaces as a warn consistent with the existing Codex-optional
checks. Kept collectChecks() accepting an optional probe state so
existing unit tests (which don't run a probe) still pass.

4 new doctor tests cover the pass/warn branches, no-codex isolation,
and the pure checksFromProbeState helper.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(observability): land codex-telemetry — append-only metrics.jsonl (G11.5)

Observational telemetry for adversarial-review invocations. Each record
captures invocation_type, estimated token counts (chars/4), duration,
exit code, and whether stderr looked rate-limited. Appended to
`<baseDir>/.rea/metrics.jsonl` as JSONL, fsync'd after each write.

Explicit non-goals documented in the module header:
- NOT the audit log — audit is hash-chained and authoritative; telemetry
  is free-form operator numbers.
- NEVER stores input_text / output_text. The strings are consumed once
  for token estimation and then discarded. A test asserts absence of
  marker strings in the written file to enforce the contract.

Fail-soft writes — any I/O error surfaces as a single stderr warning and
resolves without throwing. Telemetry must never interfere with a review.

`summarizeTelemetry` buckets records by local-tz day, most-recent first,
and handles missing file / malformed lines / out-of-window records
cleanly. 15 unit tests cover the shape, payload-absence invariant,
rate-limit regex with 4 real-world stderr examples, day bucketing, and
fail-soft behavior.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(reviewers,doctor): instrument ClaudeSelfReviewer and add --metrics flag (G11.5)

ClaudeSelfReviewer.review() now writes a single telemetry row per SDK
call via an internal emitTelemetry helper that contains both sync
throws and async rejections from a misbehaving injected telemetry fn.
Three paths are instrumented — success, API error, unparseable output
— with exit_code = 0 on success and 1 on any error. The 'no API key'
short-circuit is deliberately NOT instrumented; there is no SDK call
to measure.

CodexReviewer.review() is left uninstrumented. It throws today (real
path goes through the codex-adversarial agent); a TODO comment
references the 0.3.0 work where Codex runs from TS and the same
instrumentation will apply.

rea doctor --metrics prints a compact 7-day telemetry summary after
the existing checks. The flag never contributes to the exit code —
purely observational.

Test hygiene: ClaudeSelfReviewer test suite now redirects
process.cwd() to a tmpdir in beforeAll so the default telemetry path
(when baseDir/recordTelemetryFn are not injected) doesn't scribble
into the repo's own .rea/metrics.jsonl. .rea/metrics.jsonl added to
.gitignore as belt-and-suspenders for consumers.

Changeset updated with G11.3 + G11.5 sections.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* chore(eslint): ignore .claude/worktrees/ to prevent sibling-agent bleed

Parallel agent worktrees live under .claude/worktrees/. Without this
exclusion, eslint walks into their src/ and flags all of the transient
in-flight work — including any other agent's in-progress branch — as
errors in this checkout.

No behavior change in normal development.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* feat(install,cli,hooks): G12 upgrade + install hardening for 0.2.0-mvp

G12 (install manifest + rea upgrade) lands together with a broad
hardening pass driven by a local Opus code-reviewer adversarial read
of the full surface.

Install manifest + upgrade
- install-manifest.json with per-file sha256 + source classification
- rea upgrade --dry-run | -y | --force with bootstrap mode
- synthetic entries for CLAUDE.md managed fragment and settings.json
- drift classification: new/unmodified/drifted/removed-upstream

Hardening (addresses B1-B7 from local review)
- fs-safe.ts: resolveContained + atomicReplaceFile with
  O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW, three-file Windows replace
- zod path-traversal refinements run at parse time (control chars,
  absolute POSIX/Windows, drive letters, UNC, ..)
- TOCTOU defenses: snapshotAncestors + verifyAncestorsUnchanged
- upgrade.ts: all fs mutations routed through safe helpers,
  SHA recomputed from installed bytes, diff size cap 256KB
- .husky/pre-push: here-doc loop fixes subshell scope bug, anchored
  protected-path regex, POSIX-portable awk for HALT reason,
  Codex audit grep matches tool_name AND head_sha
- postinstall.mjs: fileURLToPath for Windows portability,
  package-manager-agnostic upgrade recommendation
- shared START_MARKER/END_MARKER/extractFragment between install
  and upgrade to prevent marker drift

Tests
- fs-safe.test.ts (16): resolveContained, atomicReplaceFile,
  safeDeleteFile, safeReadFile including symlink refusal
- manifest-schema.test.ts (19): strict parse, path rejection for
  absolute/UNC/traversal/control-chars, synthetic entries
- Net +35 tests; 247 total passing

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* chore(policy): set review.codex_required=false for this repo

Codex is rate-limited in our environment. Local Opus code-reviewer
substitutes for the adversarial-review leg of Plan → Build → Review
per CLAUDE.md. The push-gate already honors this via G11.4, so with
the flag set no env-var bypass is required on each push.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* fix(ci,hooks,tests): unblock Lint/Test CI on 0.2.0-mvp PR

- ci.yml Lint job: build before lint so lint:regex can inspect dist/
- ci.yml Test job: build before test so no-codex hook integration tests
  resolve dist/scripts and dist/audit symlinks into the scratch repo
- push-review-gate.sh: use [.] instead of \. in the protected-path ERE
  so GNU awk does not dirty stderr with escape-sequence warnings that
  made the no-codex tests brittle. Sync .claude/hooks/ copy.
- redact.test.ts: bump worker-regex timeout from 30ms to 250ms. Under
  GitHub Actions worker-thread startup load, 30ms was below the noise
  floor for default patterns and they spuriously timed out on benign
  input. 250ms keeps per-test duration sub-second while clearing CI.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* fix(husky): honor review.codex_required in terminal pre-push hook (G11.4 parity)

`.claude/hooks/push-review-gate.sh` (Claude-Code PreToolUse path) already
short-circuits the protected-path Codex audit requirement when the policy
sets `review.codex_required: false`. The terminal pre-push hook
(`.husky/pre-push`) was missed during G11.4 and still demanded the audit
entry for every protected-path diff, breaking the first-class no-Codex
mode for anyone pushing from the terminal.

Mirror the Claude-Code hook's policy read: invoke
`dist/scripts/read-policy-field.js review.codex_required` once; if the
field resolves to `false`, skip the audit requirement on this push. Every
other path (HALT, protected-path regex, audit-log grep, REA_SKIP
env-var escape hatch, fail-closed missing helper) is unchanged.

Fail-closed: if the helper is missing (unbuilt rea) or errors, treat the
field as true — safer default. Operator can `pnpm build` or set the
escape-hatch env var for a one-off bypass.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

* fix(tests): escape-hatch makeScratchRepo sets baseline identity unconditionally

CI runners have no global git user.email / user.name, so the previous
conditional-identity logic caused `git commit` to abort with "Author
identity unknown" before the hook under test could ever run.

makeScratchRepo now:
  1. Sets a baseline identity before the initial commits so the commits
     always succeed.
  2. Applies the caller's requested identity state AFTER the commits:
       - null      → unset the config (fail-closed test path)
       - string    → override with that value
       - undefined → leave the baseline in place

This preserves the original test intent — each test still exercises the
hook with its intended identity state — while making the suite robust
against CI environments that lack a global git identity.

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>

---------

Signed-off-by: Jake Strawn <bandy.strawn@clarityhouse.press>
Co-authored-by: Jake Strawn <bandy.strawn@clarityhouse.press>
@dependabot dependabot Bot changed the title chore(deps-dev): Bump typescript from 5.9.3 to 6.0.3 chore(deps-dev): bump typescript from 5.9.3 to 6.0.3 Apr 18, 2026
@dependabot dependabot Bot force-pushed the dependabot/npm_and_yarn/typescript-6.0.3 branch 3 times, most recently from ec7f951 to 1af2c24 Compare April 18, 2026 22:38
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](microsoft/TypeScript@v5.9.3...v6.0.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot force-pushed the dependabot/npm_and_yarn/typescript-6.0.3 branch from 1af2c24 to b7d533e Compare April 18, 2026 23:34
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.

0 participants