Quirlv0.5.1
ArchitectureDecisions

Managed project cloning

Canonical Quirl project documentation synced from docs/decisions/2026-09-06_192326220_managed-project-cloning.md.

Accepted · 2026-09-06

Status: Accepted (interactive Unix and explicit CLI scope). Date: 2026-09-06 (derived from the commit that introduced this entry; the source record carried no date).

Decision

Extend Quirl's existing Projects feature with quirl projects clone <repository> [--root path] and quirl projects root [--root path]. Use the GHQ-compatible <root>/<host>/<repository-path> layout, retaining nested namespaces. Each checkout is an independent Git repository. Resolve the root from an explicit option, GHQ_ROOT, Git's ghq.root configuration, then ~/Projects, in that order. With multiple ghq.root values, reuse a matching checkout in any configured root; otherwise create the checkout under the first root. quirl projects root prints that first root. No GHQ executable or short command alias is required.

In rich interactive Normal mode, recognize only a standalone, literal git clone URL without options, redirections, a destination, or additional commands. Before execution, offer the concrete managed destination. The original command remains the default; changing its destination requires an explicit selection. Users can choose a managed location once, opt in to using managed locations for future eligible clones, or dismiss future suggestions. Store this preference separately from executable Lua configuration. quirl projects policy [ask|managed|off] prints the saved setting or changes it. Missing state defaults to ask. Choosing the original command, managing once, or disabling suggestions saves off; cancelling leaves ask. managed requires explicit opt-in. The preference lives in $XDG_STATE_HOME/quirl/clone-policy.json, falling back to ~/.local/state/quirl/clone-policy.json. Scripts and explicit destinations preserve ordinary Git semantics.

The CLI owns orchestration, preference persistence, and the existing project index. Git remains responsible for cloning and authentication, using the normal foreground execution path. Successful clones and validated existing checkouts become immediately available to the project picker. Opening a project is a separate explicit Alt-Q u action that preserves unfinished input; a subprocess cannot change its parent's directory.

Failure model and invariants

Remote text, environment, Git configuration, persisted preferences, filesystem entries, and subprocess results are untrusted inputs. A destination can already exist or change between inspection and cloning. Git can fail after creating partial data. Index publication and preference persistence can fail independently of a successful clone. SIGINT or SIGTERM may arrive during a metadata probe or while the chooser owns terminal input. Cancellation or terminal failure must retain ordinary foreground child ownership and restore the editor.

  • Parse supported HTTP(S), SSH, and scp-style remotes into a validated host and complete repository path. Strip only the conventional final .git suffix. Nondefault ports use a host__port_NNN suffix; input host underscores are rejected so they cannot collide with this suffix. SSH aliases retain their supplied identity rather than consulting SSH configuration. Reject traversal, empty components, controls, percent encodings, query strings, IPv6 literals, embedded HTTP credentials, unsupported transports and local paths before any filesystem mutation. Remote text is never shell syntax and is never evaluated a second time.
  • Resolve roots with bounded configuration reads. Canonicalize the selected existing root, or its nearest existing ancestor when creating a new root, as the user's trusted location. Reject symlinks in managed descendants. Git's pathname-based subprocess boundary assumes trusted ancestors remain unchanged while Git runs; it cannot prevent arbitrary same-user ancestor replacement. A configured root is a parent directory, never a request to combine independent projects into one Git repository.
  • Never overwrite an existing destination. Reuse a checkout only after verifying its Git marker and matching origin identity. A conflicting checkout, directory, or file returns an actionable error. Reuse never pulls, resets, or checks out a branch. Reserve a new final directory with atomic creation before starting Git. Creation races remain errors; no error path recursively removes data.
  • Run Git through the existing supervised process boundary. Preserve its exit status and authentication interaction. Report partial clone data without silently retrying or deleting it.
  • A scoped signal guard owns SIGINT/SIGTERM cancellation across metadata probes and the chooser. Handlers only record cancellation; the supervised execution boundary terminates and reaps probe children. The chooser checks the same token every bounded 16 ms turn. A cancelled probe cannot fall back to executing the original clone, and cancellation cannot accept a managed destination.
  • Publish cancellation as visible status 130 and retain the last user command's status across prompt turns. Before executing user source, the CLI explicitly seeds the native executor with the bounded shell status. Internal metadata probes never publish their status into the interactive shell's $?.
  • Publish a project only after validating a successful checkout. A cache failure must not turn a completed clone into an invitation to overwrite or reclone it.
  • The recognizer excludes expansions, aliases, wrappers, compound commands, options, redirections, and explicit destinations. Explicit executable paths and forced external commands such as /usr/bin/git clone and ^git clone also execute unchanged. Only bare git and canonical literal quirl projects commands enter the corresponding interactive workflows. Only interactive rich Normal-mode execution offers suggestions.
  • Persist only a strict, versioned preference of at most 4 KiB, with bounded reads, private atomic replacement, and conflict detection against concurrent updates. Invalid state cannot enable automatic managed cloning. Cancelling an offer must not opt in or execute a different command.

Resource sketch

Remote and root strings are at most 4,096 bytes. Each remote component is at most 255 bytes, with at most 32 remote components and 32 absolute-root components. Git root configuration admits at most 16 values. Canonical roots are revalidated against the same byte and depth limits; the combined destination is at most 4,096 bytes. All configuration and checkout probes for one plan share one absolute 2-second deadline. Reservation revalidation and post-clone verification each have a separate 2-second budget. Each probe captures at most 16 KiB per output stream. Preference state is at most 4 KiB. The chooser admits at most one input event per 16 ms turn and observes cancellation each turn. No repository scan or remote network lookup runs per keystroke. The existing project cache owns bounded publication and background discovery. Interactive Git execution has the same foreground lifecycle and cancellation behavior as other terminal programs, without an overall clone wall deadline.

Evidence required

Unit tests cover supported remote transports and nested namespaces, invalid and oversized paths, root precedence, existing matching and conflicting checkouts, symlink boundaries, malformed preferences, and recognition exclusions. Process fixtures prove clone failure and cancellation leave existing data intact. An isolated signal fixture sends SIGINT only after a metadata child starts, then requires a cancelled result, child cleanup, and no clone execution. Chooser tests require already-cancelled input to acquire no terminal and never accept a destination. Interactive journeys prove the default preserves the original command, opting in changes only eligible clones, successful cloning reaches the picker immediately, and opening restores unfinished shell input correctly. Catalog tests keep the CLI, help, completion, and generated command reference aligned.

On this page