hypignore usage policy: .hypignore capture-seam drop (LLP 0049/0052/0053)#211
Conversation
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>
Dual-agent review —
|
| 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 ignoreorhyp unignorecan 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], andhyp 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
windowedentries, so a--since/--untilwindow 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(...)?.cwdbefore 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
.hypignoretokens are clamped toignore, but the emittedwarnis dropped by the resolver, making fail-safe drops hard to diagnose. - Suggested fix: Carry
warnthroughResolveResultor log once per governing file at the adapter/CLI boundary.
No Finding
- 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_dropsmoke. - 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 ignoreprints onlywrote <file>) - Why it matters: A user who has already been working in a folder (its cwd resolved+cached as
fullby the live projector's long-lived resolver) then runshyp ignorekeeps 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 ...--checkconstructs a fresh resolver"), so it is a design gap rather than a code deviation, but R1 ("an exchange whose resolved cwd has an ancestor.hypignoreMUST 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 unignoreprint + 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.hypignoreis honored without restart while still satisfying R6 (note: simply not cachingfullreintroduces 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()keepsclass/declared, dropsparsed.warn;ResolveResulthas nowarnfield); src/core/usage-policy/format.js:36-40 (warn produced); adapter drop-logs loggoverned_bybut notdeclared - 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.hypignorecontaininglocal-onlyor a typo is clamped toignorewith no observable signal, so operators can't distinguish a misdeclared policy from an intentional ignore. - Suggested fix: Carry
warn/declaredthroughResolveResultand 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#cliglosses. - 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
pathargument tohyp ignore/unignore/--checkresolved 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
createUsagePolicyResolverand 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
undefinedBEFORE building rows, after the response has streamed; dispatcher (message_projector.js:512-520) treatsundefinedas "no rows" without loggingaigw.projector_invalid_output; the proxied response is never touched. - R3 fail-safe: format.js clamps unknown/
local-only/empty toignore; matcher safeRead clamps unreadable.hypignoretoignore. 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--checkLIKE-superset/exact-refine; smoke flow is filename-registered and asserts only-clean-row-lands + ausage_policy_dropevent.
Reviewed from a clean detached worktree at the PR head. Reports: /Users/phil/workspace/hypaware/.git/worktrees/tmp.dBlx0CjJrd/dual-review/pr-211
🤖 neutral: built, green, reviewed — held on one design decisionThe 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 Fixed during review ( Held for you — the lone
|
…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>
Decision resolved + review fixes landed (
|
# Conflicts: # src/core/cli/core_commands.js
Dual-agent review —
|
| 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
undefinedas an intentional privacy drop, but the gateway dispatcher treatsundefinedas “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
projectExchangereturn[]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
.hypignoredrop emits the intendedusage_policy_dropevent, then the gateway also warnsaigw.message_projection_skippedwithreason: 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
echoandhandlein outer scope and usetry/finallyto stop the daemon, close the upstream, shut down observability, and restore mutated env values on every failure path.
No Finding
- Behavioral Correctness
- Change Impact / Blast Radius
- Concurrency, Ordering & State Safety
- Error Handling & Resilience
- Security Surface
- Release Safety
- Test Evidence Quality
- 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_dropsmoke. - Impacted callers:
createAiGatewayMessageProjector().projectExchangedispatches projector results at hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:167; dispatcher callsprojector.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'sexpandDetachClientNames
still wired into the detach path (called at 3475, LLP 0045); the branch's
runIgnore/runUnignoreregistered 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.jsstrictexpiresAt > now()); leak window bounded to 5s and
never longer; key ispath.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
dispatchProjectortreats identically to "this projector declined — try the
next matching projector," andprojectExchangethen 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 bareundefined) so the dispatcher stops the loop and
projectExchangelogs a usage-policy reason instead ofno_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
.hypignoreintoignoreis 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
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>
🤖 neutral: conflict resolved, re-reviewed, green — held for human merge gatePicked back up after the human resolved the design decision (bounded-TTL) and removed
State: head Out of scope (pre-existing, not fixed here): telemetry asymmetry between claude/codex backfill drop logs; Merge is a human act — neutral never merges. Held for your call. |
Implements the
.hypignorefolder-scoped usage policy — LLP 0049 (spec), designed in LLP 0052, planned in LLP 0053.What
A
.hypignorefile (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.src/core/usage-policy/— sharedcwd-agnostic matcher (format.jsparser + privacy fail-safe,matcher.jsancestor-walk + per-cwd cache).[]for an ignoredcwd; backfill skips ignored sessions (the only 4 sites that resolve acwd, per LLP 0050).hyp ignore/hyp unignore/hyp ignore --checkCLI verbs.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