Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 104 additions & 7 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,91 @@ the accepted source SHA-256 from the saved ETag, hashes the current local bytes,

### Materialize a direct Compilation

With `--output`, the CLI requires an explicit absent destination beneath an existing real directory. It validates
that destination before pushing the Plan, checks it again after analysis, and never overwrites, merges into, or
repairs an existing path.
With `--output`, the CLI accepts either an explicit absent destination beneath an existing real directory or a path
that resolves to the physical current directory. It validates either destination before pushing the Plan. An absent
destination is checked again after analysis; root adoption instead holds its owned lock and performs the exact
pre-move identity recheck described below. Other existing destinations remain invalid, so
`--output ./application` retains its absent-directory contract.

`--output .` is the noninteractive root-adoption mode. `./`, an absolute spelling of the current directory, and
another spelling that resolves to that same physical directory select the same mode. It works at any real current
directory that meets the preconditions below and does not recognize Drawing Board or another repository layout
specially. This first root-adoption contract supports POSIX filesystems; Windows retains absent-directory output
and refuses root adoption as `root_platform_unsupported`. Before starting Compilation, the CLI requires:

- the current directory to be a real, writable, non-filesystem-root directory;
- no existing top-level path whose portable, case-insensitive name is `design` or the reserved
`.firstdraft-root-output` transaction path;
- every top-level entry other than `.git` to be a regular file or real directory on the current directory's
filesystem. Interior symlinks, dependency trees, sockets, and nested repositories move opaquely with their
top-level directory; the CLI neither follows nor repairs them; and
- when the current directory is the root of a Git worktree, a clean tracked worktree and index with no unmerged
entries, sparse checkout, or in-progress merge, rebase, cherry-pick, or revert. Untracked and ignored design
material may remain present. Submodules and tracked `.gitmodules` files are refused in this first root-adoption
contract rather than moved with broken Git wiring. A directory nested inside a higher Git worktree is refused
rather than treated as non-Git. A valid top-level `.git` file for a linked worktree is retained like a `.git`
directory.

Git-backed root adoption invokes the installed Git executable explicitly. Read-only discovery uses
`git --no-optional-locks` with stable NUL-delimited porcelain so it does not refresh the index. Before remote work,
the CLI verifies in a temporary preview that every currently ignored entry remains ignored after its path and
applicable worktree `.gitignore` files move beneath `design`; repository-local and configured global exclusions are
both honored. A refusal is `invalid_output_path` with a machine-readable `reason` and happens before Plan push.

The CLI creates `.firstdraft-root-output` with exclusive creation during the pre-push output check and holds it
through analysis, Compilation, and materialization. That directory is both the single-writer lock and the owned
transaction journal, so a concurrent root adoption is refused before either command sends a request. Immediately
after acquiring it, the CLI captures every other top-level entry's exact name, entry type, device, and inode. After
staging the artifact and any replacement Git index, it rechecks that set immediately before moving anything. Size,
modification time, and contents are deliberately not part of this identity: interior changes are not recursively
inventoried, and a top-level directory moves intact at the transaction boundary. A replaced, added, or removed
top-level entry stops materialization. The reserved-path precondition ignores only the transaction directory
created and still held by this invocation.

`compilation download --output .` acquires the same lock before its first status request and holds it through
artifact download and materialization. Either command removes its own transaction directory on every ordinary exit
before the journal records an irreversible move or index installation. Its signal handlers do the same when Node
dispatches the signal before that boundary. A journal whose phase records no irreversible operation is likewise
safe to remove; the manual reconciliation rule below applies only after `root_rollback_incomplete`.

The complete generated artifact is written and verified inside that in-root transaction directory before any
existing path moves. Staging inside the destination makes every later rename same-filesystem even when the current
directory itself is a container mount point. The artifact may not own a top-level path whose portable,
case-insensitive name is `design` or `.firstdraft-root-output`; artifact validation already excludes `.git` at any
depth.

The transaction creates `./design` with mode `0755` on POSIX, moves every preexisting non-Git top-level entry under
it, keeps an existing top-level `.git` file or directory at the root, and installs the artifact's top-level entries
at the root. Immediately before each artifact entry is installed, its root destination must still be absent; an
unexpected entry stops the transaction and is never overwritten. If the root contains no entry other than `.git`,
it does not retain an empty `design` directory. A
nested mount that cannot travel with its top-level directory may make its rename fail; that is a transactional
failure, not permission to copy or traverse the mount.

For a Git root, the CLI first prepares a replacement index that stages each formerly tracked path at
`design/<old-path>` and stages every exact generated artifact path at the root. This handles overlapping names such
as `README.md` and `.gitignore` without leaving the old design blob indexed at a generated path. Previously
untracked and ignored paths are never added to the index. Preparing that index writes the generated blobs into the
Git object database; a rollback may therefore leave unreachable blobs for ordinary Git garbage collection, while
`HEAD`, refs, configuration, and history remain unchanged. After the worktree renames finish, the CLI installs the
prepared index through Git's actual index lock path and atomic lock-file commit protocol, including in a linked
worktree whose index is outside the adopted root. The transaction journal retains whether an index existed plus an
exact private copy, mode, and digest of its prior bytes until final verification succeeds. The preflighted ignore
protection is rechecked after the move. The caller should inspect and commit this staged root-adoption change before
using destructive worktree or index restoration commands. A non-Git root remains non-Git and is not initialized.

The journal is a versioned private JSON record plus owned staging files. It records the physical root and original
top-level identity set, the transaction phase, completed design and artifact renames, and, for Git, the resolved
index path and original and prepared index digests. Each irreversible phase is recorded before the next one starts.
On any failure after a move, index installation, or post-install verification, the CLI first restores the exact
prior index through the same Git lock boundary, then reverses artifact and design renames in journal order. A fully
successful rollback removes only the owned transaction. If rollback itself cannot finish,
`materialization_failed` reports `reason: "root_rollback_incomplete"` and includes
`recovery_path: ".firstdraft-root-output"`; it leaves the journal and owned copies in place rather than guessing.
Do not delete that directory or run Git restoration commands. Inspect the versioned journal, restore the listed
index and paths to its recorded original identities, verify that snapshot, and only then remove the transaction
directory. Another root adoption reports `root_busy` until that state is reconciled; a foreign preexisting directory
with the same reserved name reports `root_reserved_path`.

After valid analysis, the CLI requests one Compilation for that exact reviewed Head and never starts GitHub
Publication. It validates that the `202` response identifies the same Project, graph version, Head, Analysis,
Expand All @@ -166,10 +248,15 @@ and materialization failures retain the last validated Compilation projection so
without starting duplicate work. Follow the [direct Compilation recovery procedure](errors.md#direct-compilation-recovery).

Success writes one JSON object to stdout containing the validated Project, Compilation, and absolute output path.
The installed directory contains exactly the artifact files and modes; the CLI does not add a Git repository, run a
formatter, or repair generated source. When the output is nested inside another Git worktree, initialize the
application as its own repository before running generated checks that inspect Git; otherwise Git resolves to the
parent worktree. Progress on stderr reports analysis and Compilation only.
Root adoption additionally reports `root_adoption.design_path` (or `null` when no design directory was needed), its
top-level moved-entry count, whether a Git repository was preserved, and whether its index was replaced.
After final verification succeeds, the CLI removes its owned `.firstdraft-root-output` transaction directory.
An absent output directory contains exactly the artifact files and modes. Root adoption additionally contains the
preserved `design` directory and an existing root `.git`, when present; every artifact-owned path remains exact. The
CLI does not add a Git repository, run a formatter, or repair generated source. When an absent output is nested
inside another Git worktree, initialize the application as its own repository before running generated checks that
inspect Git; otherwise Git resolves to the parent worktree. Progress on stderr reports analysis and Compilation
only.

### Publish through GitHub

Expand Down Expand Up @@ -232,6 +319,16 @@ Materialize an already successful Compilation into an absent path:
firstdraft compilation download 01900000-0000-7000-8000-000000000001 --output ../movie-catalog
```

The same command accepts `--output .` and applies the root-adoption transaction above. This is the recovery path
when a retained direct Compilation succeeded but an earlier root materialization failed _and fully rolled back_; it
never starts replacement work. An incomplete rollback leaves `.firstdraft-root-output` and requires journal
reconciliation before this command can run again.

Successful root adoption is intentionally one-way. The original `.firstdraft` authoring state moves under
`design/.firstdraft`; run later Plan commands from `design`, not from the generated application root. Compiling a
later Plan revision does not overwrite an already adopted root: choose a new absent output and deliberately
reconcile it with application work.

The command validates the UUID and output path before network access, makes one status `GET`, requires `succeeded`,
and makes one artifact `GET`. It never starts work or polls. Historical artifact validation uses the retained
`compilation.head_source_sha256`, not the current local Plan or ETag, to pin the artifact's exact
Expand Down
20 changes: 17 additions & 3 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ materialization failure envelopes include that last validated projection as `cur
- after `artifact_unavailable`, wait if appropriate and use
`firstdraft compilation download <current.compilation.id> --output <new-absent-path>`; and
- after `materialization_failed`, repair the destination condition, then use the same lower-level download command
with a new absent path.
with a new absent path. A root-output attempt whose transaction fully rolled back may instead retry that retained
download with `--output .`; `reason: "root_rollback_incomplete"` requires reconciliation of the retained
`.firstdraft-root-output` journal before another root attempt. Only that reason requires manual reconciliation:
do not delete its journal or run Git restoration commands; read its versioned record, restore the exact listed
index and path identities, verify its original snapshot, and then remove the transaction directory. A journal
whose phase records no irreversible operation is safe to remove. After `.firstdraft` has moved successfully under
`design`, run retained status and later Plan commands from `design`, not from the generated application root.

After `compilation_wait_timed_out`, retained work may still continue. Use
`firstdraft compilation status <current.compilation.id>` for one read-only status check; do not rerun
Expand Down Expand Up @@ -118,5 +124,13 @@ stopped without following the replacement.
| `compilation status --wait` | `compilation_changed`, `compilation_wait_timed_out` | 1 | Retained identity/provenance changed or the wait ended. |
| `compilation download` | `compilation_not_succeeded` | 1 | Status was not `succeeded`; no artifact request was made. |
| Download commands | `artifact_unavailable`, `invalid_artifact` | 1 | Artifact transport or integrity validation failed; direct Compile post-start errors include `current`. |
| Download commands | `invalid_output_path` | 2 | The destination was not an absent path beneath an existing real directory. |
| Download commands | `materialization_failed` | 1 | The output raced or installation failed; direct Compile post-start errors include `current`. |
| Download commands | `invalid_output_path` | 2 | The absent destination or root-adoption preconditions failed; `reason` identifies the stable refusal. |
| Download commands | `materialization_failed` | 1 | The output changed or its transaction failed; `reason` identifies incomplete rollback when applicable. |

Root-output `invalid_output_path.reason` values are `destination_exists`, `root_not_real`, `root_not_writable`,
`root_platform_unsupported`, `root_reserved_path`, `root_entry_unsupported`, `root_enclosing_worktree`,
`root_git_unavailable`, `root_git_unsupported`, `root_git_dirty`, `root_ignore_not_preserved`, and `root_busy`. Git
refusing discovery, including a `safe.directory` refusal, is `root_git_unavailable`; a discovered but unsupported
Git shape such as a submodule is `root_git_unsupported`. Root-output `materialization_failed.reason` values are
`output_changed`, `root_artifact_collision`, `root_ignore_changed`, `root_transaction_failed`, and
`root_rollback_incomplete`.
1 change: 1 addition & 0 deletions scripts/check-pack.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ if (result.status !== 0) {
"src/file-system.js",
"src/plan-compile-progress.js",
"src/plan-state.js",
"src/root-output.js",
"src/uuid-v7.js",
"src/version.js",
]);
Expand Down
Loading