Skip to content

hypignore usage policy: .hypignore capture-seam drop (LLP 0049/0052/0053)#211

Merged
philcunliffe merged 18 commits into
masterfrom
integration/hypignore-usage-policy
Jun 30, 2026
Merged

hypignore usage policy: .hypignore capture-seam drop (LLP 0049/0052/0053)#211
philcunliffe merged 18 commits into
masterfrom
integration/hypignore-usage-policy

Conversation

@philcunliffe

@philcunliffe philcunliffe commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Implements the .hypignore folder-scoped usage policy — LLP 0049 (spec), designed in LLP 0052, planned in LLP 0053.

What

A .hypignore file (gitignore-style, ancestor-walked) maps a directory subtree to a usage class. V1 ships one class — ignore (never recorded). Enforced at the capture seam in the client adapters (LLP 0050), so the live LLM call is untouched and only persistence is suppressed.

  • T1 src/core/usage-policy/ — shared cwd-agnostic matcher (format.js parser + privacy fail-safe, matcher.js ancestor-walk + per-cwd cache).
  • T2/T3 claude + codex adapter drops — live projector returns [] for an ignored cwd; backfill skips ignored sessions (the only 4 sites that resolve a cwd, per LLP 0050).
  • T4 hyp ignore / hyp unignore / hyp ignore --check CLI verbs.
  • T5 hermetic smoke hypignore_capture_drop — proves the drop end-to-end.

No cache schema, export driver, or gateway change (capture-seam only). local-only + session opt-out are deferred (LLP 0051).

🤖 Generated by neutral (design→plan→implement), held for human review + merge.

Change-Set: hypignore-usage-policy

philcunliffe and others added 13 commits June 29, 2026 19:04
Technical design for the .hypignore folder-scoped usage policy: a shared
cwd-agnostic matcher in src/core/usage-policy/ (ancestor walk + per-cwd cache +
file-format fail-safe), capture-seam enforcement at the four claude/codex
adapter drop-sites (live projector returns [], backfill skips), and the
hyp ignore/unignore/--check CLI verbs.

@ref LLP 0049 [implements] — covers the hypignore-usage-policy spec
@ref LLP 0050 [constrained-by] — adapter-enforced, shared matcher in core
@ref LLP 0051 [constrained-by] — local-only/session opt-out deferred, format stays forward-compatible

Generated-by: neutral

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Five independently-mergeable tasks: T1 shared core matcher (src/core/usage-policy/),
then T2 claude + T3 codex adapter capture-seam drops + T4 hyp ignore/unignore/--check
CLI in parallel, then T5 hermetic smoke. Capture-seam only (LLP 0050) — no cache schema,
export driver, or gateway change.

Related: LLP 0052 (design), LLP 0049 (spec), LLP 0050 (enforcement point)
Generated-by: neutral

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…49/0050)

Add `src/core/usage-policy/`, the single shared, cwd-agnostic matcher for the
`.hypignore` folder-scoped usage policy. V1 enforces only the capture seam; this
task lands the core module the Claude/Codex adapters (T2/T3) and the CLI (T4)
will import.

- `format.js` `parseHypignore`: strip `#`/blank lines, first token is the class;
  empty/comment-only => `ignore`; unknown/unimplemented token => `ignore` + a
  `warn` string (the privacy fail-safe). `@ref LLP 0049#file-format`/`#fail-safe`.
- `matcher.js` `createUsagePolicyResolver({readFileSync,existsSync})`:
  gitignore-style ancestor walk from a cwd to the nearest `.hypignore`, per-cwd
  memo cache, fs injected. `resolve(cwd)` -> `{class,governedBy,declared}`,
  `isIgnored(cwd)`. `@ref LLP 0050`/`LLP 0049#scope`.
- `index.js` barrel + `types.d.ts` (`UsageClass`, `ParseResult`, `ResolveResult`,
  `UsagePolicyResolver`).
- Tests `test/core/usage-policy.test.js`: empty=>ignore, unknown=>ignore+warn,
  nearest-ancestor wins, no file=>full, cache stable + reads once.

Task-Id: T1

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the shared usage-policy resolver (LLP 0050 / src/core/usage-policy)
into both Claude capture-seam drop-sites so an exchange whose resolved
cwd is governed by an ancestor `.hypignore` is never written to the
cache, for live capture and backfill alike (LLP 0049 R1/R2):

- projector.js: createClaudeExchangeProjector holds one resolver per
  projector. Once the exchange cwd is resolved from the session-context
  record, an ignored cwd returns no rows BEFORE building any, so the
  ai-gateway source write guard persists nothing. Returning `undefined`
  is the projector's existing no-rows signal (a literal `[]` is an
  invalid projection that would log a spurious warning); the live LLM
  call already streamed, so it is untouched.
- backfill.js: createClaudeBackfillProvider holds one resolver per run
  and skips ignored sessions before projecting/writing, keyed on the
  same cwd the row would carry (record cwd, else first transcript line).
- Both drop-sites emit a structured usage_policy_drop event.
- Resolver is injectable on both factories for hermetic tests.

New test/plugins/claude-usage-policy-drop.test.js drives the real
matcher (injected fs) through the gateway dispatcher and the backfill
provider: ignored cwd => no rows / session skipped; clean cwd and
no-cwd unaffected.

@ref LLP 0050 [implements]
@ref LLP 0049#requirements [constrained-by]

Task-Id: T2
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Symmetric to the @hypaware/claude T2 drop: enforce the `.hypignore`
folder-scoped `ignore` usage policy at the Codex capture seam, the only
place that resolves a `cwd`, so an ignored exchange never reaches the cache.

- `codex/src/exchange-projector.js`: `createCodexExchangeProjector` holds one
  shared `createUsagePolicyResolver()` (per listener; injectable for tests).
  Once the exchange `cwd` is resolved, an ancestor `.hypignore` of class
  `ignore` returns no projection, so the gateway source's
  `messageRows.length > 0` write guard persists nothing. The response has
  already streamed, so the live LLM call is untouched (LLP 0049 R1/R2). Emits
  a structured `usage_policy_drop` log (hashed cwd, governing path).
- `codex/src/backfill.js`: `createCodexBackfillProvider` holds one resolver and
  skips a session whose recorded `cwd` is ignored before projecting/yielding,
  so `hyp backfill` never re-imports sessions ignored live (LLP 0049 R1). Adds
  `sessions_ignored` to the scan-complete telemetry.

The projector returns `undefined` (its established skip signal — the dispatcher
maps it to an empty rows array, the "return []" of LLP 0050 §Live); returning a
literal `[]` would trip the gateway's invalid-projection warning.

No cache schema, export driver, gateway, or settlement change — capture-seam
only, per LLP 0050. Tests mirror T2: ignored cwd -> no rows/skip, clean cwd
unaffected, drop telemetry emitted; the real shared matcher is exercised via
injected fs.

@ref LLP 0050 [implements]

Task-Id: T3

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#cli)

Implement the usage-policy CLI verbs on top of the shared core matcher
(T1). These are imperative, cwd-relative, filesystem-mutating commands, so
they register as plain `ctx.commands` CommandRegistrations alongside the
existing `ignore`/`attach`/`detach` commands — not LLP 0034 verbs (whose
`VerbOperationContext` has no `cwd` and whose tools are remotely
invokable). LLP 0009 itself draws this line: "imperative/interactive
commands stay `ctx.commands.register`".

- `hyp ignore [path]` writes a self-documenting `.hypignore` (comment
  header + `ignore` token) at the git repo root, else the cwd; an explicit
  `path` overrides. Idempotent (R5): a path already governed by an
  ancestor `.hypignore` is a no-op success, never a redundant nested file.
- `hyp unignore [path]` removes the nearest governing `.hypignore`.
  Idempotent: a no-op when nothing governs.
- `hyp ignore --check [path]` reports ignored?/governing file/residual
  already-cached row count; prospective-only, never purges
  (LLP 0049 #prospective-only). The residual count pushes a superset LIKE
  filter into `ai_gateway_messages` then refines with an exact subtree
  match in JS (squirreling LIKE treats `_`/`%` as wildcards), and degrades
  to `unknown` when the dataset is unavailable.

Repo-root resolution is a new `findRepoRoot` in the core usage-policy
module: a dependency-free, fs-injectable `.git` ancestor walk mirroring the
adapters' `git rev-parse --show-toplevel`, kept in core so the CLI need not
spawn git and stays hermetically testable.

Tests: idempotency for ignore/unignore, repo-root vs cwd vs explicit-path
placement, `--check` output (ignored/clean/--json/graceful-unknown), a real
residual count proving the LIKE-superset + exact-refine, and direct
findRepoRoot unit tests.

Task-Id: T4
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…P 0053 T5)

Hermetic smoke that drives one Claude exchange from a `.hypignore`'d cwd and
one from a clean cwd through the daemon (ai-gateway + claude), then asserts:

- only the clean session's rows land in `ai_gateway_messages` (no ignored cwd,
  no ignored session id), proving R1;
- the gateway returned 200 for the ignored exchange and its `aigw.exchange`
  log recorded `rows_written = 0`, proving R2 (live call untouched);
- the claude projector emitted a `usage_policy_drop` event naming the governing
  `.hypignore`.

Each phase runs under a stable `smoke_name`/`smoke_step` root span so a failure
names the broken step. Registered in the README smoke battery (the flow index).

@ref LLP 0049#requirements [tests]
@ref LLP 0050 [tests]
@ref LLP 0053#tasks

Task-Id: T5

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A relative `path` argument to `hyp ignore`/`unignore`/`ignore --check`
resolved against the Node process cwd instead of the command-context
cwd, so injected/remote/test dispatch could write, remove, or check the
wrong tree — a privacy-relevant misfire for this control. Resolve against
`ctx.cwd ?? process.cwd()` like the sibling verbs (plugin scaffold/init)
already do. No-path and absolute-path behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Dual-agent review — request_changes

  • Verdict: request_changes
  • Risk class: medium
  • Auto-merge advisory: 👎 thumbs down — verdict is request_changes; needs human-gated follow-up
  • Reviewed head: 7a201bf · fix pushed: 77bfab9

Advisory only: no merge was attempted.

What changed during review

One actionable major was fixed and pushed (77bfab9): relative path args to hyp ignore/unignore/--check now resolve against the command-context cwd, not the Node process cwd (matching the sibling plugin verbs). Full CI mirror (build:types + typecheck + lint + test, 1521 pass) green. The remaining major below is spec-sanctioned and left for a human decision — not silently patched.

Headline finding (open, human decision)

Process-lifetime negative (full) policy cache makes hyp ignore silently ineffective on a running daemon. A cwd already resolved+cached as full by the live projector keeps recording after the user writes a .hypignore / runs hyp ignore, until the daemon restarts — a silent leak window against R1. Faithfully implemented per LLP 0052 #matcher (cache is process-lifetime), so it is a documented design gap, not a code deviation. Decide: accept-as-documented (+ surface a restart hint to the user) or harden (bounded TTL / mtime re-stat that still satisfies R6). Raised independently by both reviewers.

Risk capstone

Cross-reference: reviewer findings vs blast radius

Finding Reviewer(s) Severity On hot path? Status
Process-lifetime full cache → hyp ignore ineffective on running daemon Claude + Codex major yes (live resolver) OPEN — spec-sanctioned (LLP 0052); human decision
Relative path arg resolved vs process.cwd not ctx.cwd Codex major no (CLI only) FIXED + pushed (77bfab9)
Claude backfill cwd derived from windowed (post-time-window) entries Codex major (med conf) backfill only OPEN-note — likely unreachable ("cwd rides every line"); drop mirrors row stamp; below Claude ≥80 bar
Fail-safe warn produced then discarded Claude + Codex minor no note
Em dashes vs AGENTS.md absolute no-em-dash rule Claude minor no note
Codex review

Fix Validations

No explicit bug-fix claim to validate. This is feature work, so findings below are new issues in the changed behavior.

Findings

4) Concurrency, Ordering & State Safety

  • Severity: major
  • Confidence: high
  • Evidence: src/core/usage-policy/matcher.js:42, src/core/usage-policy/matcher.js:50, src/core/usage-policy/matcher.js:51, hypaware-core/plugins-workspace/claude/src/projector.js:99, hypaware-core/plugins-workspace/codex/src/exchange-projector.js:45, src/core/cli/core_commands.js:3753, src/core/cli/core_commands.js:3797
  • Why it matters: A live daemon caches the policy result per cwd for the projector lifetime, so hyp ignore or hyp unignore can silently fail to affect future captures for a cwd already resolved before the file change.
  • Suggested fix: Add invalidation for mutable policy files, for example mtime/TTL checks, avoid caching full, or signal the daemon from the CLI; add a regression that resolves a cwd, creates/removes .hypignore, then resolves the same cwd again.

1) Behavioral Correctness

  • Severity: major
  • Confidence: high
  • Evidence: src/core/cli/core_commands.js:3737, src/core/cli/core_commands.js:3790, src/core/cli/core_commands.js:3826, src/core/cli/dispatch.js:310, src/core/cli/dispatch.js:322
  • Why it matters: Relative hyp ignore [path], hyp unignore [path], and hyp ignore --check [path] arguments resolve against the Node process cwd instead of the command context cwd, so injected/remote/test dispatch can write, remove, or check the wrong tree.
  • Suggested fix: Resolve as path.resolve(ctx.cwd ?? process.cwd(), parsed.path ?? '.'), or equivalent, in all three command paths.

1) Behavioral Correctness

  • Severity: major
  • Confidence: medium
  • Evidence: hypaware-core/plugins-workspace/claude/src/backfill.js:176, hypaware-core/plugins-workspace/claude/src/backfill.js:185, hypaware-core/plugins-workspace/claude/src/backfill.js:344, hypaware-core/plugins-workspace/claude/src/backfill.js:379
  • Why it matters: Claude backfill derives the policy cwd from windowed entries, so a --since/--until window can miss a cwd present elsewhere in the loaded session and import rows from an ignored session.
  • Suggested fix: Compute the policy cwd from record?.cwd ?? sessionEntries.find(...)?.cwd before applying the time window, and add a test where the cwd-bearing transcript line is outside the import window.

11) Debuggability & Operability

  • Severity: minor
  • Confidence: high
  • Evidence: src/core/usage-policy/format.js:39, src/core/usage-policy/matcher.js:66, src/core/usage-policy/matcher.js:67
  • Why it matters: Unknown or reserved .hypignore tokens are clamped to ignore, but the emitted warn is dropped by the resolver, making fail-safe drops hard to diagnose.
  • Suggested fix: Carry warn through ResolveResult or log once per governing file at the adapter/CLI boundary.

No Finding

  1. Contract & Interface Fidelity; 3) Change Impact / Blast Radius; 5) Error Handling & Resilience; 6) Security Surface; 7) Resource Lifecycle & Cleanup; 8) Release Safety; 9) Test Evidence Quality; 10) Architectural Consistency.

Evidence Bundle

  • Changed hot paths: src/core/usage-policy/matcher.js, src/core/usage-policy/format.js, src/core/cli/core_commands.js, Claude/Codex live projectors, Claude/Codex backfills, hypignore_capture_drop smoke.
  • Impacted callers: hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:503, hypaware-core/plugins-workspace/ai-gateway/src/source.js:117, src/core/cli/dispatch.js:322.
  • Impacted tests: test/core/usage-policy.test.js:132, test/core/ignore-command.test.js:110, test/core/ignore-command.test.js:165, test/plugins/claude-usage-policy-drop.test.js:32, test/plugins/claude-usage-policy-drop.test.js:103, test/plugins/codex-backfill.test.js:341, test/plugins/codex-exchange-projector.test.js:36.
  • Unresolved uncertainty: Whether process-lifetime policy caching is intentional despite the CLI contract; whether historical Claude transcripts can have cwd only outside a filtered backfill window; tests were not run.
Claude review

Claude review

Process-lifetime negative (full) policy cache makes hyp ignore silently ineffective on a running daemon

  • Severity: major
  • Confidence: 92
  • Evidence: src/core/usage-policy/matcher.js:48-55 (caches every result incl. {class:'full'} for the resolver/daemon lifetime); src/core/cli/core_commands.js:3764 (hyp ignore prints only wrote <file>)
  • Why it matters: A user who has already been working in a folder (its cwd resolved+cached as full by the live projector's long-lived resolver) then runs hyp ignore keeps being recorded for every later exchange in that folder until the daemon restarts — the exact "I flagged it, now stop" intent fails silently, the leak this control exists to prevent. It is faithfully implemented per LLP 0052 #matcher ("Cache is process-lifetime ... --check constructs a fresh resolver"), so it is a design gap rather than a code deviation, but R1 ("an exchange whose resolved cwd has an ancestor .hypignore MUST NOT be written") is violated for the window between file-write and daemon-restart, and nothing tells the user. This is the one judgment call a human should adjudicate (accept-as-documented vs harden).
  • Suggested fix: At minimum, have hyp ignore/hyp unignore print + document that a running daemon must be restarted for the change to affect already-active folders. For a real fix, bound the negative-result cache (short TTL or re-stat the nearest ancestor dirs on lookup) so a newly-written .hypignore is honored without restart while still satisfying R6 (note: simply not caching full reintroduces the per-exchange walk R6 forbids). Either path is a change to Accepted LLP 0052 and should be deliberated, not silently patched.

Fail-safe warn for unimplemented/unknown class tokens is produced then discarded

  • Severity: minor
  • Confidence: 85
  • Evidence: src/core/usage-policy/matcher.js:66-67 (walk() keeps class/declared, drops parsed.warn; ResolveResult has no warn field); src/core/usage-policy/format.js:36-40 (warn produced); adapter drop-logs log governed_by but not declared
  • Why it matters: R3 says an unimplemented class MUST resolve to ignore (satisfied — privacy-safe) and SHOULD warn; LLP 0052 #telemetry asks to warn "with the declared token and the governing path". As written a .hypignore containing local-only or a typo is clamped to ignore with no observable signal, so operators can't distinguish a misdeclared policy from an intentional ignore.
  • Suggested fix: Carry warn/declared through ResolveResult and emit a warn-level event on fail-safe drops in both adapters including the declared token.

Em dashes (U+2014) in new runtime string + comments violate the head AGENTS.md absolute rule

  • Severity: minor
  • Confidence: 95
  • Evidence: src/core/cli/core_commands.js:3680 (em dash baked into the HYPIGNORE_TEMPLATE # header written to every .hypignore), plus em dashes in new comments/JSDoc/@ref glosses at core_commands.js:3725,3739,3749,3775,3817,3855; usage-policy/repo_root.js:10,18,19; codex/src/exchange-projector.js:81
  • Why it matters: The PR head's AGENTS.md bans U+2014 "anywhere: code, comments, JSDoc, strings, or docs"; the template em dash is functionally harmless (it sits in a # comment line the matcher never tokenizes) but is still a written-to-disk violation, and two of the offenders are @ref LLP 0049#cli glosses.
  • Suggested fix: Replace each with -, a colon, or a sentence split.

(Cross-reviewer, FIXED) Relative path arg resolved against process cwd, not ctx.cwd

  • Severity: major
  • Confidence: 90
  • Evidence: src/core/cli/core_commands.js:3737,3790,3826 (was path.resolve(parsed.path ?? ctx.cwd))
  • Why it matters: Raised by Codex (cat 1). A relative path argument to hyp ignore/unignore/--check resolved against the Node process cwd instead of the command-context cwd, diverging from the sibling verbs (core_commands.js:1610,1708) and letting injected/remote/test dispatch write/remove/check the wrong tree — a privacy-relevant misfire.
  • Suggested fix: APPLIED — now path.resolve(ctx.cwd ?? process.cwd(), parsed.path ?? '.') at all three sites; no-path/absolute behavior unchanged, full CI mirror green, pushed as 77bfab9.

Notes verified clean (no finding)

  • R1/R4: all four drop sites (claude live projector.js:173, claude backfill.js:185, codex exchange-projector.js:83, codex backfill.js:196) use the single shared createUsagePolicyResolver and check the EXACT cwd each site stamps on the row (projector.js:298; exchange-projector.js:130/866-869; backfill.js:379; backfill.js:555). No leak.
  • R2: live drop returns undefined BEFORE building rows, after the response has streamed; dispatcher (message_projector.js:512-520) treats undefined as "no rows" without logging aigw.projector_invalid_output; the proxied response is never touched.
  • R3 fail-safe: format.js clamps unknown/local-only/empty to ignore; matcher safeRead clamps unreadable .hypignore to ignore. Always suppress-more.
  • #scope: ancestor walk to fs root, nearest wins, no-file => full, root terminates (no infinite loop), symlink handling is lexical and consistent with how cwd is stamped.
  • Contracts/imports/tests: dispatcher always passes {log} to project(); all CLI imports/ctx fields present; coverage exercises fail-safe, nearest-ancestor, all four drop sites (ignored-vs-clean), R5 idempotency, and --check LIKE-superset/exact-refine; smoke flow is filename-registered and asserts only-clean-row-lands + a usage_policy_drop event.

Reviewed from a clean detached worktree at the PR head. Reports: /Users/phil/workspace/hypaware/.git/worktrees/tmp.dBlx0CjJrd/dual-review/pr-211

@philcunliffe philcunliffe added the neutral:stuck neutral attempted this but cannot complete it autonomously — needs a human label Jun 30, 2026
@philcunliffe

Copy link
Copy Markdown
Contributor Author

🤖 neutral: built, green, reviewed — held on one design decision

The loop drove this end-to-end from request LLP 0049: design 0052 → plan 0053 → 5 implemented+verified tasks → green PR → dual-review. lint + typecheck + test green at head 77bfab9, mergeable, never merged.

Fixed during review (77bfab9, CI-green): a MAJOR — relative path args to hyp ignore/unignore/--check resolved against process.cwd() instead of ctx.cwd, diverging from the sibling verbs and mis-targeting under injected/remote dispatch.

Held for you — the lone request_changes driver (a design call, not a code bug)

The shared matcher's process-lifetime cache (src/core/usage-policy/matcher.js, faithful to LLP 0052 #matcher) means a cwd first resolved as full keeps being recorded after you later write a .hypignore / run hyp ignore, until the daemon restarts — a silent leak window against R1. Both reviewers raised it independently.

It's a documented design choice, so resolving it is yours:

  • (a) amend 0052 #matcher to a bounded staleness check — short TTL, or an mtime re-stat of the governing path — that still satisfies R6 (no unbounded hot-path fs work) — and update matcher.js; 0052 is neutral-minted and unmerged, so this is a clean follow-up commit on this branch; or
  • (b) accept it for V1 and print a "restart the daemon to apply" hint in hyp ignore output.

Not "don't cache full" — that reintroduces the per-exchange walk R6 forbids.

Minor (non-blocking, left as notes)

  • R3 SHOULD-warn: the fail-safe warn from format.js is dropped by matcher.js (a clamped local-only/typo is indistinguishable from an intended ignore in logs).
  • Em-dashes in the .hypignore template string + some new comments violate AGENTS.md's no-em-dash rule.

Full findings + blast-radius: the dual-review comment above. The privacy core (all four capture-seam drop sites use the shared resolver on the exact stamped cwd; R2 live-call-untouched; fail-safe/scope) was verified clean.

…p (LLP 0052)

Resolves the lone request_changes from the PR #211 dual-review.

- matcher: the per-cwd cache gains a short TTL (CACHE_TTL_MS, 5s) so a
  .hypignore written or removed mid-run is honored within the window instead of
  only after a daemon restart (R1), while keeping the capture hot path bounded
  to one ancestor walk per cwd per window (R6). Clock and ttl are injectable.
  Updates LLP 0052 #matcher (why TTL, not process-lifetime) and notes the
  future CLI-to-daemon cache-invalidation signal that drives the window to zero.
- adapters: carry the fail-safe warn/declared through ResolveResult; the four
  capture-seam drop sites add `declared` and escalate a clamp to warn level so
  an operator can tell a typo'd/unimplemented policy from an intended ignore
  (R3 SHOULD-warn).
- em dashes: replace U+2014 in the .hypignore template string and the new
  comments/JSDoc/@ref glosses per AGENTS.md (README and LLP doc prose left).

Tests: TTL staleness (both directions) and a fail-safe warn-level drop. Full
suite (1524 pass), typecheck, lint, and hypignore_capture_drop smoke green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Decision resolved + review fixes landed (e40e545)

The lone request_changes driver (process-lifetime policy cache → silent leak window against R1) is resolved as option (a): harden with a bounded TTL. Pushed additively on top of 77bfab9; CI mirror green (1524 pass, typecheck, lint) plus the hypignore_capture_drop hermetic smoke.

What changed

  • Bounded-TTL policy cache (matcher.js). The per-cwd cache now carries a short TTL (CACHE_TTL_MS, 5s). A .hypignore written (or removed) on a running daemon is honored within the window instead of only after a restart (closes the R1 leak), while the capture hot path stays bounded to one ancestor walk per cwd per window (satisfies R6 — deliberately not "don't cache full", which reintroduces the per-exchange walk R6 forbids). Clock + TTL are injectable; two regression tests cover both staleness directions (ignore appearing, and unignore removing). LLP 0052 #matcher updated with the rationale.
    • Follow-up noted (not V1): hyp ignore/unignore should signal the running daemon to invalidate and prime the affected cwd's cache entry, collapsing apply latency from "within the TTL" to zero. Forward-note left in matcher.js and the hyp ignore handler.

Also folded in (the two minors from the review)

  • R3 SHOULD-warn: the fail-safe warn/declared now flow through ResolveResult; all four drop sites log declared and escalate a clamp to warn level, so a typo'd/unimplemented policy (e.g. local-only in V1) is distinguishable from an intended ignore in the logs. New test asserts the warn-level drop.
  • Em dashes: replaced U+2014 in the .hypignore template string and the new comments/JSDoc/@ref glosses per AGENTS.md. (README and the LLP doc prose still contain pre-existing em dashes — left out of scope; happy to sweep those separately if wanted.)

The Codex "claude backfill cwd from windowed entries" finding (major / med-confidence, judged likely unreachable) is untouched and still open as a note.

@philcunliffe philcunliffe removed the neutral:stuck neutral attempted this but cannot complete it autonomously — needs a human label Jun 30, 2026
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Dual-agent review — request_changes

  • Verdict: request_changes
  • Risk class: medium
  • Auto-merge advisory: 👎 thumbs down — verdict is request_changes; needs human-gated follow-up

Advisory only: no merge was attempted.

Context: re-review after the post-convergence head move — the #213 expandDetachClientNames conflict resolution in core_commands.js (verified: both sides preserved, each function defined once, expandDetachClientNames still wired into the detach path) and the bounded-TTL policy cache (verified correct and leak-safe). The lone major below is a latent contract hazard, not an active leak (claude/codex projector matchers are non-overlapping today, so exactly one projector matches any exchange).

Risk capstone

Cross-reference: reviewer findings vs high-risk surfaces

Source Finding (severity, evidence) Intersects
Codex major: drop undefined not terminal in dispatch (message_projector.js:500-524) Risks #1, Cross-package usage
Codex minor: drop logged as no_projector_match (message_projector.js:168-174) Risks #1
Codex minor: smoke cleanup not in try/finally (hypignore_capture_drop.js) Files (smoke)
Claude minor: drop reuses overloaded undefined signal (message_projector.js:168-174,500-524) Risks #1
Claude minor: fail-closed safeRead catch untested (matcher.js:115-121) Risks #2
Codex review

Fix Validations

No bug-fix validation claimed. This PR implements a new .hypignore usage policy feature.

Findings

2) Contract & Interface Fidelity

  • Severity: major
  • Confidence: high
  • Evidence: hypaware-core/plugins-workspace/claude/src/projector.js:175, hypaware-core/plugins-workspace/claude/src/projector.js:186, hypaware-core/plugins-workspace/codex/src/exchange-projector.js:86, hypaware-core/plugins-workspace/codex/src/exchange-projector.js:98, hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:503, hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:512
  • Why it matters: The adapters use undefined as an intentional privacy drop, but the gateway dispatcher treats undefined as “this projector declined, try the next matching projector,” so the suppression is not a terminal contract and can fall through if another matching projector is registered.
  • Suggested fix: Add an explicit terminal “suppressed/no rows” projector result or dispatcher sentinel for usage-policy drops, and have projectExchange return [] without continuing to lower-priority projectors.

11) Debuggability & Operability

  • Severity: minor
  • Confidence: high
  • Evidence: hypaware-core/plugins-workspace/claude/src/projector.js:178, hypaware-core/plugins-workspace/codex/src/exchange-projector.js:89, hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:167, hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:169
  • Why it matters: A normal .hypignore drop emits the intended usage_policy_drop event, then the gateway also warns aigw.message_projection_skipped with reason: no_projector_match, making successful privacy suppression look like projection failure.
  • Suggested fix: Once an intentional drop is represented distinctly, suppress the generic skipped warning for that path or log it at debug with a usage-policy reason.

7) Resource Lifecycle & Cleanup

  • Severity: minor
  • Confidence: medium
  • Evidence: hypaware-core/smoke/flows/hypignore_capture_drop.js:80, hypaware-core/smoke/flows/hypignore_capture_drop.js:178, hypaware-core/smoke/flows/hypignore_capture_drop.js:237
  • Why it matters: The smoke starts an HTTP upstream and daemon, but cleanup happens only after the drive step succeeds, so a failed assertion or request can leave the daemon/server running and observability unflushed.
  • Suggested fix: Track echo and handle in outer scope and use try/finally to stop the daemon, close the upstream, shut down observability, and restore mutated env values on every failure path.

No Finding

  1. Behavioral Correctness
  2. Change Impact / Blast Radius
  3. Concurrency, Ordering & State Safety
  4. Error Handling & Resilience
  5. Security Surface
  6. Release Safety
  7. Test Evidence Quality
  8. Architectural Consistency

Evidence Bundle

  • Changed hot paths: Claude live projector drop, Claude backfill skip, Codex live projector drop, Codex backfill skip, hyp ignore / hyp unignore, usage-policy matcher/parser, hypignore_capture_drop smoke.
  • Impacted callers: createAiGatewayMessageProjector().projectExchange dispatches projector results at hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:167; dispatcher calls projector.project(input, ctx) at hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:503.
  • Impacted tests: test/plugins/claude-usage-policy-drop.test.js, test/plugins/codex-exchange-projector.test.js, test/plugins/codex-backfill.test.js, test/core/usage-policy.test.js, test/core/ignore-command.test.js, hypaware-core/smoke/flows/hypignore_capture_drop.js.
  • Unresolved uncertainty: I did not run the suite. Current registered projectors appear to be Claude and Codex with non-overlapping matchers, so the fallthrough risk is mostly a contract hazard today, but the false gateway warning is current behavior.
Claude review

Claude review

Four parallel review subagents (guidance+merge-resolution, bug-scan+TTL-cache,
contract+callers, comments+tests) plus a firsthand read of the merge resolution
and the gateway projector dispatch. Highest-value checks:

  • Merge resolution in src/core/cli/core_commands.js: CORRECT. Each of
    expandDetachClientNames (3837), parseIgnoreArgs (3849), runIgnore
    (3876), runUnignore (3933), runIgnoreCheck (3982) defined exactly once;
    no duplicate/shadow, no leftover stub. Master Fix probe-less client attach/reverse asymmetry that orphans settings #213's expandDetachClientNames
    still wired into the detach path (called at 3475, LLP 0045); the branch's
    runIgnore/runUnignore registered as CLI commands (271/277). Both sides of
    the conflict preserved.
  • Bounded-TTL policy cache: correct and leak-safe. TTL enforced on every
    read (matcher.js strict expiresAt > now()); leak window bounded to 5s and
    never longer; key is path.resolve(cwd) (no cross-repo collision); resolve
    path is fully synchronous (no async stale-read window). Drop path is
    fail-CLOSED (unreadable .hypignore -> '' -> ignore) and precedes any row
    construction; the policy-checked cwd equals the cwd stamped on the row at all
    four sites.
  • Capture-seam coverage complete: only claude + codex projectors/backfills
    record; all four given the drop; no third recording path missing the seam.

.hypignore drop reuses the "no projection" undefined signal (overloaded)

  • Severity: minor
  • Confidence: 82
  • Evidence: hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:168-174, :500-524
  • Why it matters: A usage-policy drop returns undefined, which
    dispatchProjector treats identically to "this projector declined — try the
    next matching projector," and projectExchange then logs
    aigw.message_projection_skipped reason=no_projector_match. Today this is
    benign (claude/codex matchers are non-overlapping, so exactly one projector
    matches any exchange — no fallthrough leak), but (a) every successful privacy
    drop is logged as a projection failure, and (b) the suppression is not a
    terminal contract, so a future overlapping projector could record a dropped
    exchange.
  • Suggested fix: Represent an intentional drop as a distinct terminal
    sentinel (not bare undefined) so the dispatcher stops the loop and
    projectExchange logs a usage-policy reason instead of no_projector_match.

Fail-closed safeRead catch on the privacy path is untested

  • Severity: minor
  • Confidence: 80
  • Evidence: src/core/usage-policy/matcher.js:115-121
  • Why it matters: The catch that turns an unreadable-but-present
    .hypignore into ignore is the fail-closed direction of a privacy control,
    but only its consequence (parseHypignore('') => ignore) is tested — delete
    the try/catch and every existing test still passes while the resolver would
    now throw on an unreadable marker (and the exchange would record).
  • Suggested fix: Add a matcher test with existsSync: () => true, readFileSync: () => { throw new Error('EACCES') } asserting
    resolve(cwd).class === 'ignore'.

Reports: /Users/phil/workspace/hypaware/.git/worktrees/tmp.nRGFEWnTpo/dual-review/pr-211

philcunliffe and others added 2 commits June 30, 2026 12:16
Three review fixes on the .hypignore capture-seam drop (PR #211).

Finding 1: represent an intentional usage-policy drop as a distinct terminal
sentinel (USAGE_POLICY_DROP) instead of bare `undefined`. The gateway
dispatcher now stops the projector walk on the sentinel (a later overlapping
projector can never record a dropped exchange) and logs it as
`aigw.usage_policy_drop` (reason=usage_policy_drop), not the misleading
`no_projector_match` miss. Bare `undefined` still means "decline, try the next
projector"; normal projections are unchanged. The claude and codex projectors
return the sentinel at their drop sites.

Finding 2: add a matcher test proving a present-but-unreadable .hypignore fails
closed to `ignore` (the safeRead try/catch).

Finding 3: wrap the hypignore_capture_drop smoke's daemon/upstream/env teardown
in try/finally so a failed assertion never leaks a live daemon, echo server, or
HYP_HOME/HYP_CONFIG/HOME into the next smoke.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@philcunliffe philcunliffe marked this pull request as ready for review June 30, 2026 19:20
@philcunliffe

Copy link
Copy Markdown
Contributor Author

🤖 neutral: conflict resolved, re-reviewed, green — held for human merge gate

Picked back up after the human resolved the design decision (bounded-TTL) and removed neutral:stuck. Driven through the reconcilePR rungs to ready + held:

  • Merge conflict (master advanced): single content conflict in src/core/cli/core_commands.js — master's Fix probe-less client attach/reverse asymmetry that orphans settings #213 expandDetachClientNames vs the branch's runIgnore. Resolved keeping both (verified: each of expandDetachClientNames/parseIgnoreArgs/runIgnore/runUnignore/runIgnoreCheck defined once, detach path still wired, no leftover stub). Then merge-based the clean docs-only Design: bounded query execution — kernel fix for the /v1/query OOM (LLP 0054-0057) #215/LLP 0054-0057: Draft → Accepted (approve bounded query execution) #216 advances to stay current.
  • Re-review (dual-agent, round 1 → fix → round 2): the round-1 request_changes was driven by one Codex major that was a latent hazard, not an active defect (the 18 projectors' matchers are non-overlapping, so exactly one matches any exchange — verified). The bounded-TTL cache was independently verified leak-safe and fail-closed. Rather than hold a disputed finding, all three findings were fixed:
    1. Terminal drop contract — an intentional .hypignore drop now returns a distinct frozen sentinel USAGE_POLICY_DROP instead of bare undefined. dispatchProjector stops the walk on it (a later overlapping projector can never record a suppressed exchange), and projectExchange logs aigw.usage_policy_drop (info) instead of mislabeling the privacy drop as no_projector_match (warn). Mutation-verified terminal behavior; bare-undefined decline path proven unchanged.
    2. Fail-closed matcher test — unreadable-but-present .hypignoreignore, closing a gap where deleting the safeRead catch left all tests green.
    3. Smoke try/finally — daemon/upstream cleanup + env restore now always run.
  • Round-2 review (this head): the contract change re-reviewed clean — sentinel is reference-identity-safe, dispatcher intercepts before the isValidProjection fall-through, the only projectExchange consumer (source.js) sees the existing [] no-rows contract for a drop.

State: head 1acc523, MERGEABLE, CI green (lint + test + typecheck; full suite 1669 pass / 0 fail, build:types + comprehensive tsc + hypignore_capture_drop smoke all green), reviewed.

Out of scope (pre-existing, not fixed here): telemetry asymmetry between claude/codex backfill drop logs; unignore silently ignores --json; some parseIgnoreArgs usage-error branches untested. All cosmetic/non-leak — fine as follow-ups.

Merge is a human act — neutral never merges. Held for your call.

@philcunliffe philcunliffe merged commit 0969971 into master Jun 30, 2026
6 checks passed
@philcunliffe philcunliffe deleted the integration/hypignore-usage-policy branch June 30, 2026 20:35
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