Uh oh!
There was an error while loading. Please reload this page.
Local-only directory selection: enrollment picker + export-seam withholding (LLP 0069/0080/0081) - #270
Conversation
…vers 0069) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add src/core/usage-policy/local_only.js: localOnlyListPath(stateDir), readLocalOnlyDirs (missing => [], corrupt => throws LocalOnlyListUnreadableError with error_kind 'local_only_list_unreadable', the LLP 0080 fail-safe), and writeLocalOnlyDirs (normalize/dedupe/sort absolute paths, mkdir -p, atomic tmp+rename write). Exported from the usage-policy barrel. @ref LLP 0071 [implements] @ref LLP 0080#fail-safe [implements] Task-Id: T1 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
collectHypAwareStatus best-effort reads the machine-local local-only list and reports usagePolicy.localOnlyDirCount; a corrupt list degrades overall with a local_only_list_unreadable diagnostic instead of silently showing zero. Text renders "local-only: withholding N directories from forwarding (recorded locally)" when N > 0; JSON carries the count under usage_policy. Task-Id: T8
createUsagePolicyResolver accepts an optional localOnlyListPath: resolve(cwd) now merges the existing .hypignore ancestor walk with segment-aware equal-or-descendant membership in the machine-local local-only list, and returns the most-restrictive class (ignore > local-only > full). A list-governed result names the list file as governedBy/declared. The parsed list is memoized with the existing 5s TTL, independent of per-cwd caching, and a corrupt list throws the same LocalOnlyListUnreadableError the store uses rather than silently resolving to "no exclusions". format.js's IMPLEMENTED set gains 'local-only', so a committable .hypignore declaring it resolves to that class instead of clamping to ignore (LLP 0070 #resolver). Updates the two existing tests that exercised 'local-only' as a stand-in for an unimplemented token to use a still- unimplemented token instead, and adds coverage for the new merge behavior. Task-Id: T2 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds `src/core/commands/local_only.js`: `listCapturedDirectories` (LLP 0069 #enumerate) runs one best-effort `executeQuerySql` GROUP BY over `ai_gateway_messages` and collapses to one candidate per distinct `cwd`, most-recently-active first. `runLocalOnlyPicker` (LLP 0072) is the stdin+stderr TTY-gated checkbox multi-select (reusing the existing `src/core/cli/tui` `multiselect`, rendered on stderr) that pre-checks directories already on the machine-local list, preserves entries not offered this round, persists via `writeLocalOnlyDirs`, prints the never-silent "withholding N..." line, and emits `local_only.picker_result` telemetry. Cancellation and a failed/empty enumeration both proceed with zero *new* exclusions without touching whatever was already on disk. Task-Id: T5
runBrowserLogin now calls runLocalOnlyPicker once the login-minted gateway credential is seeded, strictly before enrollCentralSink provisions or re-seeds a central sink - covering both the fresh-enroll and re-login forks. --no-forward and query-only (no gateway) logins never reach it, and a non-cancellation picker error (e.g. a corrupt existing list) is caught and warned rather than failing enrollment. Task-Id: T7
`hyp ignore --local-only [path]` / `hyp unignore --local-only [path]` add and remove entries in the machine-local `local-only` list (LLP 0071) as an alternative to the committable `.hypignore` dotfile, giving a non-login authoring path for directories excluded from forwarding (LLP 0072 #cli). Both verbs reuse the shared usage-policy resolver and its equal-or-ancestor matching rather than duplicating path logic (R8): add is a no-op when the target is already governed (by an existing list entry or a stricter `.hypignore`), and remove drops every governing list entry. `hyp ignore --check` now constructs the resolver with the list wired in, so it reports `local-only` alongside `ignore`, names whichever source (dotfile or list file) governs, and computes the residual cached-row count for either class. Task-Id: T6
Withhold `local-only` rows at the shared export read (`storage.readRowsSince`)
while still advancing the per-(sink, partition) watermark across them, so a
directory the user marked local-only is recorded in the cache and stays locally
queryable but never forwarded — and its tail is durably passed rather than
re-scanned each tick.
- storage.js: `createQueryStorageService` gains an optional `usagePolicyResolver`;
`readRowsSince` yields `{ after, dropped: true }` (no payload) for a row whose
non-empty `cwd` resolves to class != `full`, passes cwd-less rows untouched,
and lets a corrupt-list throw fail the partition read (fail-safe). Emits the
`usage_policy.export_drop` aggregate with hashed cwds.
- activation.js: every kernel boot defaults the resolver from the machine-local
list at `localOnlyListPath(<stateDir>)`.
- kernel contract: `readRowsSince` element type is now a discriminated union
(row-carrying | drop-only).
- central sink: advance `lastAfter` across drops, count `droppedRowCount`, widen
the end-of-partition watermark gate to `shipped>0 || dropped>0`
(ship-first/advance-second preserved; a failed chunk still never checkpoints).
- openIncrementalRows: skip drops in `rows()`, advance `lastAfter` incl. trailing
drops, expose `droppedRowCount`, peek past leading drops for `empty`; s3 and
local-fs write the watermark on an empty-but-dropped tick.
Tests: cache-but-never-forward for a local-only cwd, drop-only tick checkpoints,
un-excluded dir not re-sent past a checkpoint, failed chunk never checkpoints,
cwd-less datasets unaffected, corrupt list fails the tick with the watermark
untouched.
Task-Id: T3
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>…0070/0081 T4) The Iceberg table-format sink read committed rows via `storage.readRows`, a plain full scan that never went through `storage.readRowsSince` — the shared, usage-policy-filtered export seam every other sink (central forward, blob/ local-fs incremental) already reads through. That made it the last export path off the filtered seam: a directory the user marked `local-only` could still leak into a committed Iceberg snapshot even though no other sink would ever forward it. `openRows` now reads through `readRowsSince` instead: `since` omitted (full scan) with `includeLegacy: true`, skipping drop-only entries and discarding the `after` continuation `readRowsSince` yields — Iceberg commits are snapshot/marker-based (`state.js` load/writeMarker), not cursor-based, so there is no watermark here to advance. Tests: a committed snapshot excludes rows from a directory on the machine-local `local-only` list; a partition with no list configured commits every row unaffected, exactly as before. Task-Id: T4 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…81 T9) Adds `hyp smoke local_only_export_withhold`, closing the local-only-dir- selection change set: seeds cache rows for two cwds into a fixture dataset, marks one directory local-only via the durable CLI (`hyp ignore --local-only`), then drives the REAL central forward sink through the REAL sink driver. Asserts end to end: the clean cwd's rows reach the fake central server; the excluded cwd's rows never do, but stay locally queryable via `executeQuerySql`; the sink watermark advances across the withheld rows (a second forced tick ships zero bytes and POSTs nothing new); `hyp status` (text and --json) reports the withholding count; and `usage_policy.export_ drop` fires exactly once, naming the dropped-row and distinct-cwd counts. Registers the new flow in the README release checklist. Task-Id: T9 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nly-dir-selection # Conflicts: # src/core/cli/remote_commands.js
philcunliffe
commented
Jul 7, 2026
🔍 neutral review (Codex + 4 Claude lenses) — request_changesRound 1, head Major — |
…'t bypass withholding + fix inline import type (PR #270 review round 1) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nly-dir-selection # Conflicts: # src/core/commands/clients.js
philcunliffe
commented
Jul 7, 2026
🔍 neutral review round 2 — approve · ready for mergeRound-1 findings fixed and verified on head
Also merged up to current master (which landed #256/#266/#271/#273): the only conflict was the Verdict: approve. Review rounds complete (N=2). Flipping to ready and holding for a human to merge — neutral does not merge. Automated neutral review. |
Uh oh!
There was an error while loading. Please reload this page.
Implements the local-only-dir-selection change set — LLP 0080 (design), LLP 0081 (plan); covers request LLP 0069 and decisions LLP 0070/0071/0072.
Lets a user mark directories as local-only — recorded locally but never forwarded to a central sink — via a login-time enrollment picker and a durable
hyp ignore --local-onlyCLI, enforced at the export seam (drop-but-advance) so excluded rows stay locally queryable while never leaving the machine. Closes a real format-iceberg leak where captured directories could be forwarded before the user could withhold them.Tasks (all merged, verified as real ancestors of the integration branch):
src/core/usage-policy/local_only.js)readRowsSince+ sink/incremental consumers)src/core/commands/local_only.js)hyp ignore/unignore --local-only,--check)enrollCentralSink)hyp statussurface (withholding count)local_only_export_withholdGenerated-by: neutral
Change-Set: local-only-dir-selection