Skip to content

Add portable record export across surfaces#122

Open
tony wants to merge 72 commits into
issue-80-deterministic-identitiesfrom
issue-81-export
Open

Add portable record export across surfaces#122
tony wants to merge 72 commits into
issue-80-deterministic-identitiesfrom
issue-81-export

Conversation

@tony

@tony tony commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

Add the initial portable export contract across CLI, TUI, and MCP using one deterministic, frontend-neutral renderer.

  • Add agentgrep export for stable NDJSON or Markdown, bodies on by default, standard output or explicit files, bounded selection, no-clobber output, and explicit --force.
  • Add HUD /export [PATH] and /export-thread [PATH] for one selected record or its observed thread, with private collision-free defaults and all identity, rendering, and disk work off the Textual message pump.
  • Add a read-only export_records MCP tool for one to 20 existing search refs, bodies opt-in, a 400 KiB inline artifact ceiling, bounded opaque refs, and no server-side filesystem authority.
  • Share one exact allowlist that excludes source paths, origins, titles, session identifiers, and arbitrary metadata while preserving canonical IDs and an artifact-level schema version.
  • Harden source-alias protection, atomic writes, phase-error privacy, legacy ref compatibility, duplicate selection handling, PATH_MAX ref round-trips, and docs/runtime schema parity.

Relationship

This PR targets the branch for #111 because deterministic record identities are its prerequisite. It is independent of the bookmark PR and can be retargeted to master after #111 lands.

Closes #81

Tested

  • Full repository gate: Ruff, formatting, ty, 2,891 passed, 3 skipped, 3 expected failures, and Sphinx.
  • Format × body × cardinality × selection permutations; CLI stdout/file/force/refusal; MCP size/ref/privacy boundaries; TUI races, reset, teardown, large payloads, and static non-blocking guards.
  • Live Textual pane: private and explicit Markdown writes, literal bracketed filenames, no-overwrite refusal, preserved results, private modes, unchanged source stores, responsive narrow layout, and clean exit.

@tony
tony force-pushed the issue-80-deterministic-identities branch from adcf5e0 to cb6377d Compare July 12, 2026 20:14
@tony
tony force-pushed the issue-81-export branch from 59d247d to 1be23b6 Compare July 12, 2026 20:15
@tony
tony force-pushed the issue-80-deterministic-identities branch 9 times, most recently from a68c683 to 5ffd718 Compare July 14, 2026 03:56
@tony
tony force-pushed the issue-81-export branch from 1be23b6 to 89c6db2 Compare July 14, 2026 04:28
@tony
tony force-pushed the issue-80-deterministic-identities branch from 5ffd718 to 4e49bbd Compare July 14, 2026 23:36
tony added 13 commits July 14, 2026 18:36
why: Export needs one frontend-neutral artifact contract and a file sink
that cannot overwrite source stores or leak source metadata.

what:
- Render allowlisted deterministic NDJSON and Markdown artifacts.
- Preserve observed thread fidelity while preparing identities once.
- Write private artifacts with atomic no-clobber and alias guards.
why: Closing the trusted private-directory descriptor before installation allowed a path replacement to redirect output and bypass the 0700 directory guarantee.

what:
- Share one descriptor-relative atomic installer across both writers.
- Keep the private descriptor through collision allocation and fsync.
- Regress a deterministic directory replacement at the trust boundary.
why: ExportArtifact is publicly constructible, so its type hints cannot protect writer boundaries from forged format or selection values.

what:
- Validate exact format and selection literals before path effects.
- Share the validator across explicit and private writers.
- Cover both writers with forged-artifact regressions.
why: Identity-prepared conversation grouping exists only to avoid duplicate export hashing and should not become unchecked public API.

what:
- Make the prepared grouping seam package-private.
- Keep the public compatibility wrapper and its one-shot behavior.
- Retain private-seam compatibility and no-rehash regressions.
why: Deterministic record rendering and safe persistence need a bounded,
non-interactive command that preserves search semantics without loading the
export stack on help paths.

what:
- Add typed export parsing, limits, output policy, and facade dispatch.
- Render matching records to complete stdout writes or protected files.
- Cover parser permutations, fixture-store execution, and path-free errors.
why: The export command added two intentional public facade names, but the exact ADR 0010 surface snapshot was not updated.

what:
- Add ExportArgs to the sorted facade snapshot.
- Add run_export_command to the sorted facade snapshot.
why: Force-overwrite protection covered only sources that produced selected records, so an unmatched or unselected discovered store could be replaced by a file export.

what:
- Reuse one backend selection for search and file-sink inventory discovery.
- Protect deduplicated paths across every agent and non-default inventory.
- Keep stdout search-only and sanitize protection discovery failures.
why: MCP clients need a privacy-bounded way to export selected search
results without server-local writes or a second ref-matching policy.

what:
- Share one batched, position-aware resolver with inspect_result.
- Add bounded NDJSON and Markdown export with body opt-in.
- Redact refs and paths from audit and failure boundaries.
why: Typed FastMCP responses duplicated large artifacts into text and
structured content, so the response limiter truncated valid 400 KiB exports.

what:
- Return one TextContent artifact with schema-validated metadata.
- Check the complete serialized envelope before middleware truncation.
- Cover exact and escape-expanded limits through a real MCP client.
why: Different token encodings and historical refs can resolve to the same
physical turn even when their raw strings differ.

what:
- Attach a path-hiding source key and scan ordinal to resolved records.
- Reject repeated physical selections after resolution.
- Preserve distinct positioned occurrences with explicit regression tests.
why: A resolver-wide discovery failure is an operation error, not evidence
that one requested source or record is absent.

what:
- Raise one typed path-free resolver failure for discovery errors.
- Map the failure to tool errors in inspect and export.
- Preserve successful unresolved responses for ordinary missing selections.
why: The request validator compares raw ref strings, while semantic
aliases are rejected only after resolver matching.

what:
- Describe the validator's pre-discovery string-equality scope.
why: Headless surfaces already support export. Selected records and
observed threads also need a bounded, non-blocking path from the
interactive explorer.

what:
- Add argument-aware record and observed-thread slash commands.
- Snapshot results in chunks and render/write in a gated worker.
- Cover sink, identity, safety, race, teardown, and watchdog cases.
tony added 28 commits July 14, 2026 18:36
why: Preference persistence compiled templates with an arbitrary sample title,
so grammar validity incorrectly depended on one rendered filename's byte size.

what:
- Validate the exact token grammar and literal filename structure directly.
- Keep the 180-byte bound on filenames rendered for the selected title.
- Cover long safe templates independently of compiled filename limits.
why: Directory completion delegated tilde expansion to process-global account
lookup instead of the home already owned by the TUI session.

what:
- Pass the session home from the export dialog through the picker worker.
- Resolve only current-user tilde syntax with the shared safe resolver.
- Cover session-home expansion and other-user rejection without expanduser.
why: Canceling a Textual worker cannot stop a thread already blocked in
scandir, so rapid edits could overlap filesystem enumerations.

what:
- Keep one active enumeration and coalesce newer input into one latest slot.
- Launch the queued scan only after the blocked filesystem call returns.
- Preserve debounce, generation, focus, and unmount invalidation contracts.
why: Opening an untrusted preference path with buffered I/O could block startup
on a FIFO and accepted non-regular file types before parsing.

what:
- Open preference files nonblocking without following the final symlink.
- Require a regular fstat result before one bounded read loop.
- Cover FIFO latency, path-free fallback, and the exact 16 KiB boundary.
why: Unicode format controls can reorder or hide characters in a reviewed
basename without appearing as ordinary control characters.

what:
- Reject category Cf in template literals and compiled filenames.
- Cover zero-width and bidi override controls with focused regressions.
why: A clean first-use session remembered an app-owned export path that the
review validator required to exist, so the default flow could not proceed.

what:
- Create only the computed private default through the descriptor-safe walker.
- Keep arbitrary missing and symlinked directories validation-only and rejected.
- Prove clean-home export, 0700 mode, and path safety in Pilot regressions.
why: Pressing y accepted the durable action while the disabled confirmation
list still visibly highlighted No.

what:
- Move the review highlight to Save before disabling confirmation.
- Pin the visible saving state in the existing single-write Pilot test.
why: The inline template error fell below a 30 by 10 tmux viewport, leaving a
focused editor with no visible explanation.

what:
- Make edit and review stages vertically scrollable only when space requires it.
- Scroll inline errors into view without adding pump-side work.
- Cover feedback and stage reachability at 40x12, 30x10, and 60x16.
why: The TUI guide still described e as an immediate private export and blurred
its remembered review state with one-shot slash commands.

what:
- Document the reviewed directory, template, exact filename, and No path.
- Keep explicit and private-default slash-command behavior distinct.
- Scope the documentation contract to the Export section.
why: A failed reviewed save restored its retained inline draft and also emitted
a duplicate toast carrying the same error.

what:
- Let a mounted saving dialog consume its worker failure inline.
- Preserve path-free error toasts for slash-command and no-dialog failures.
- Cover retained drafts, cancel keys, and the no-dialog writer route.
why: Absolute export directories inside the active session home exposed a machine-specific prefix in the edit and review stages and in remembered preferences.

what:
- Compact session-home directories to tilde drafts without resolving paths.
- Apply compaction to loaded and newly entered directory values.
- Cover defaults, legacy preferences, outside paths, and persistence.
why: Ctrl-C dismissed the export modal immediately while editing, making a familiar terminal clear-field gesture unnecessarily destructive.

what:
- Clear a focused non-empty directory or template before dismissal.
- Preserve focus after clearing and dismiss on a second Ctrl-C.
- Retain immediate review cancellation and saving protection.
why: Restoring an asynchronous export failure at compact terminal sizes let deferred focus scrolling move the sole inline feedback outside the viewport.

what:
- Restore retained editor focus before applying asynchronous feedback.
- Reveal the error through a named callback after refreshed layout.
- Cover retained focus and visibility at 30 by 10.
why: A post-refresh error reveal could outlive its edit state and scroll a dialog after rapid dismissal or after its feedback had been cleared.

what:
- Scope deferred reveals to a generated non-empty error request.
- Require the mounted active dialog to remain in its edit phase.
- Invalidate pending reveals on updates, review, dismissal, and unmount.
- Cover rapid Escape and cleared-error races deterministically.
why: Directory drafts could contain control, bidi-format, or surrogate code points that made reviewed destinations and completion rows ambiguous.

what:
- Share one reviewability rule across preference load, save, and submit.
- Omit unsafe existing directory names from bounded completion.
- Cover control, invisible, bidi, surrogate, config, and dialog cases.
why: Draft validation created the first-use export directory before the user accepted Save, so review, No, and cancel crossed an unexpected mutation boundary.

what:
- Validate a missing exact app default through a read-only no-symlink prefix walk.
- Create that directory securely only in the accepted export worker.
- Keep arbitrary missing and symlinked paths rejected.
- Document artifact and private-preference mutation boundaries.
why: The widgets package re-exported completion rows, popup chrome, and export workflow values that are internal module details rather than reusable widgets.

what:
- Keep internal candidate, popup, draft, and intent types out of package exports.
- Import the intent type directly from its defining module.
- Pin the narrower package boundary without making an API claim.
why: The export changelog used an ambiguous pronoun and implied that every input state handled the shortcut identically.

what:
- State directly that e remains ordinary text when an input is focused.
- Pin the exact focused-input wording in the export documentation contract.
why: An empty directory draft resolved to the process working directory, so review could display a blank value while Save targeted an implicit location.

what:
- Reject empty text in the shared directory validator while preserving literal whitespace-only Unix paths.
- Cover resolution, preference load and save, dialog review, and completion boundaries.
why: Lexical directory compaction runs on the Textual pump, where an unbounded path with many components could stall input handling before validation was offloaded.

what:
- Apply a documented 4,096-character ceiling to the live editor and shared validation and persistence boundaries.
- Reject oversized Enter snapshots and compaction inputs before path processing, with boundary regressions.
why: Export editing should follow the directional muscle memory of the rest of the TUI, while review should remain calm and legible in small terminals.

what:
- Add clamped Ctrl-H/J/K/L and completion-aware arrow traversal.
- Pin edit and review hints without hiding validation feedback.
- Shrink-wrap a No-first confirmation with a moving selection marker.
- Cover navigation, layout, saving, and pump roles with Pilot tests.
why: Modal export obscured the active explorer and made the reviewed save
flow hard to distinguish from the reader. A pane-owned state machine keeps
context visible and gives shortcut and slash-command entry one behavior.

what:
- Replace the selected-record modal with a fresh detail-pane export flow.
- Restore the exact search draft, selection, focus, zoom, and reader state.
- Keep validation and durable output off the Textual message pump.
- Document and test the reviewed /export contract and compact layout.
why: Leading-slash text that is not a registered command is a valid search.
Treating every slash-prefixed value as transient restored an older query after
the next successful command.

what:
- Remember the active input after dispatch classifies it as a literal query.
- Cover exact value and selection restoration after a leading-slash search.
why: A second Tab could leave the export state for the live search input,
allowing hidden searches whose results no longer matched the restored query.

what:
- Wrap Tab and Shift-Tab across export editors and the review choice.
- Preserve Tab acceptance for an open directory completion.
- Cover edit and compact-review focus containment with Pilot.
why: Textual mounts dynamically added children after the opening pump turn.
A same-turn priority key could query export controls before compose completed.

what:
- Track when the pane's composed controls are ready for routed actions.
- Ignore edit, cancel, and review actions during the deferred mount window.
- Reproduce the race without an intervening Pilot pause.
why: Textual invokes reactive watchers on the message pump. The new query-draft
watchers were bounded but lacked the runtime role required by ADR 0011.

what:
- Mark value and selection draft watchers as pump-only.
- Extend the static role guard to keep both entrypoints colored.
why: The export action should replace the detail reader on wide terminals,
not obscure the results that give the action its selection context.

what:
- Preserve the results column beside the export pane in wide layouts.
- Keep the focused full-body export presentation for stacked terminals.
- Cover responsive transitions without changing reader state.
why: Only the direct thread command uses an automatic canonical-ID name;
the reviewed record command previews and confirms a templated filename.

what:
- Attribute automatic private naming specifically to /export-thread.
- Pin the command distinction in the export documentation contract.
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