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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ carry no release status. The `v<version>` tag and npm show whether a version is
Entries start with the first version prepared after this file was added, and earlier versions have none. The
repository's [tags](https://github.com/firstdraft/cli/tags) identify the source of each published version.

## 0.8.2

Adds `firstdraft compilation cancel <compilation-id>`, so an agent can recover a Compilation that will not finish
without an operator calling the Service for it.

- A Project allows one active Compilation. While a stuck one stays queued or running, `plan push` and `plan compile`
fail with First Draft's `409 compilation_active` problem, whose `detail` names that Compilation. Check it with
`compilation status`, cancel it, then rerun `plan compile`.
- The command makes one `POST` to the Service's cancel route for the local Project and prints the cancelled
Compilation in the same shape as `compilation status`. Repeating it prints the same result. First Draft refuses to
cancel a succeeded or failed Compilation and leaves it unchanged.
- New `error` values: `compilation_cancel_rejected`, which carries the Service problem in `response`
(`compilation_not_cancellable`, `compilation_not_found`, or `project_not_found`), and
`compilation_cancel_unavailable`, after which cancelling again is safe. A response that violates the Compilation
contract reports `invalid_compilation_status`.
- `firstdraft --help` describes the `compilation` group as "Inspect, download, and cancel Compilations".

Existing callers need no change. The cancel route is part of the Service's API `0.7.x` contract, so the accepted
API-contract range is unchanged.

## 0.8.1

Adds `firstdraft login` and `firstdraft logout`, so a token no longer has to be copied from `/api-tokens` into the
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ requires `--github`.
- conditional whole-document push and conflict reporting;
- analysis polling and complete GapSet output;
- direct Compile-and-materialize and private publish orchestration;
- retained Compilation inspection and artifact download;
- retained Compilation inspection, cancellation, and artifact download;
- terminal output, exit status, and recovery contracts;
- the dependency-free npm package; and
- package provenance and publication.
Expand Down
50 changes: 42 additions & 8 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ group's `--help` for concise executable syntax. See [Errors and recovery](errors

The current `0.8.x` source line contains the auditable command shell, browser and device login, local Foundation Plan initialization, local
application-key and UUID generation, conditional whole-document push, whole-graph analysis status polling, direct
Compile-and-materialize and private publish orchestration, and retained-Compilation inspection. CLI `0.8.x`
requires the service's `0.7.x` API contract. The
Compile-and-materialize and private publish orchestration, and retained-Compilation inspection and cancellation.
CLI `0.8.x` requires the service's `0.7.x` API contract. The
[changelog](https://github.com/firstdraft/cli/blob/main/CHANGELOG.md) describes each version.

## Command map
Expand All @@ -24,6 +24,7 @@ requires the service's `0.7.x` API contract. The
| `firstdraft plan compile --github` | Yes | Push and analyze, then publish to private GitHub |
| `firstdraft compilation status` | Yes | Inspect a retained Compilation by ID |
| `firstdraft compilation download` | Yes | Verify and materialize a successful retained Compilation |
| `firstdraft compilation cancel` | Yes | Cancel a queued or running Compilation by ID |

## Select an environment and authenticate

Expand Down Expand Up @@ -113,8 +114,8 @@ the existing Project's private state to redirect it.
`FIRSTDRAFT_API_URL` remains available for an initial custom HTTPS origin or loopback HTTP development server, and
it also selects the origin for `login` and `logout`, for example `FIRSTDRAFT_API_URL=http://127.0.0.1:3000
firstdraft login`. `--staging` together with a different URL is an error; the equivalent normalized staging URL is
allowed. Later pushes and compilation reject an override that differs from the pin. Read-only status and retained
download commands use the pin and ignore `FIRSTDRAFT_API_URL` unless checking its conflict with an explicit
allowed. Later pushes and compilation reject an override that differs from the pin. Status, retained download, and
cancel commands use the pin and ignore `FIRSTDRAFT_API_URL` unless checking its conflict with an explicit
`--staging`.

Every remote command selects credentials from its effective origin. First it uses the environment: the exact
Expand Down Expand Up @@ -464,6 +465,38 @@ POSIX, directories use mode `0755` and files use artifact-declared `0644` or `07
contents, and digests without claiming POSIX mode bits. The declared and streamed artifact envelope is bounded at
128 MiB.

## Cancel a stuck Compilation

A Project allows one active Compilation. While it is queued or running, First Draft refuses to replace the Plan or
start another Compilation with `409 compilation_active`, so `plan push` and `plan compile` fail. If that Compilation
will not finish, for example because its worker process died, cancel it from the Project's directory:

```sh
firstdraft compilation cancel 01900000-0000-7000-8000-000000000001
```

The `compilation_active` problem names the active Compilation's ID in its `response.detail`, and a `plan compile`
failure after a validated start carries it as `current.compilation.id`. Read the Compilation with
`compilation status` first. Cancelling one that is still making progress throws away its work, and the next
`plan compile` starts over.

The command validates the UUID before network access and makes one `POST` to the origin pinned for the Project, with
that origin's credential. It sends no Plan bytes or precondition header and changes no local state. Success prints
the validated cancelled Compilation in the same shape as `compilation status`, with `compilation.status` set to
`cancelled`. First Draft clears the Project's active Compilation, so the next `plan push` or `plan compile` can
proceed. A worker that finishes later cannot attach an artifact to a cancelled Compilation; stopping that worker is
best effort.

Cancel is idempotent for queued, running, and cancelled Compilations, so repeating it prints the same cancelled
Compilation. First Draft never changes a succeeded or failed Compilation: it answers
`409 compilation_not_cancellable`, which the CLI reports as `compilation_cancel_rejected`. Because a repeat is
safe, an outcome the CLI cannot confirm is `compilation_cancel_unavailable` rather than `request_outcome_unknown`.
See [stuck Compilation recovery](errors.md#stuck-compilation-recovery).

If the Compilation was started by `plan compile --github`, Cancel also cancels the Project's Publication. The
Publication is a Project singleton in this release, so `plan compile --github` cannot start another one for that
Project.

## Service endpoints

The CLI calls these Service API routes. The `/v1` routes go to the origin pinned for the Project and send the
Expand All @@ -479,6 +512,7 @@ timeout. Each request takes its method and path from one
| `GET` | `/v1/projects/{project_id}/analysis` | Read the Head's current analysis |
| `POST` | `/v1/projects/{project_id}/compilations` | Start a Compilation of the Head |
| `GET` | `/v1/projects/{project_id}/compilations/{compilation_id}` | Read one retained Compilation |
| `POST` | `/v1/projects/{project_id}/compilations/{compilation_id}/cancel` | Cancel that Compilation |
| `GET` | `/v1/projects/{project_id}/compilations/{compilation_id}/artifact` | Download that Compilation's artifact |
| `PUT` | `/v1/projects/{project_id}/github-publication` | Start or rejoin the Publication |
| `GET` | `/v1/projects/{project_id}/github-publication` | Poll or reconcile the Publication |
Expand All @@ -489,12 +523,13 @@ timeout. Each request takes its method and path from one
`plan push` sends the Plan `PUT`, and `plan status` reads the analysis. `plan compile` does both. It then starts a
Compilation, polls it, and downloads its artifact. With `--github`, it starts and polls the Publication instead.
`compilation status` reads one retained Compilation. `compilation download` reads it and downloads its artifact.
`compilation cancel` cancels it.
`login` exchanges its authorization code at `/oauth/token`. With `--interactive`, it starts at
`/oauth/device_authorization` and then polls `/oauth/token`. `logout` calls `/oauth/revoke`. The browser, not the
CLI, opens `/oauth/authorize` and `/device`, so they have no rows.

The first push sends `If-None-Match: *`. Later pushes, the Compilation `POST`, and the Publication `PUT` send the
saved Head ETag in `If-Match`. The Service also has a Compilation cancel route, which the CLI does not call.
The first push sends `If-None-Match: *`. Later pushes, the Compilation start `POST`, and the Publication `PUT` send
the saved Head ETag in `If-Match`. The cancel `POST` sends no precondition header.

The `/v1` routes belong to the API-contract range that `release/compatibility.json` accepts. The CLI does not read the
Service's `FirstDraft-API-Contract` response header. Instead, the Service's release compatibility check compares the
Expand Down Expand Up @@ -550,8 +585,7 @@ Use this checklist when a change adds a command or subcommand. Each step names t
5. **Document it.**
- In this page, add a row to the [command map](#command-map) and a section for the command. Add a row to
[Service endpoints](#service-endpoints) for each new route, and name the command in the paragraph below the
table. If the command calls a route that section says the CLI does not call, such as Compilation cancel,
rewrite that sentence.
table.
- In [errors.md](errors.md#error-index), add an Error index row for each new `error` value. Add recovery guidance
when retrying the command is safe in a different way than for the existing commands.
- When the Skill will call the command, update the Skill's CLI references and contract checks in
Expand Down
36 changes: 33 additions & 3 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ remain human-readable text on standard error with exit 2. Unexpected programming
`invalid_configuration` stops before a request when `--staging` conflicts with `FIRSTDRAFT_API_URL` or a Project's
saved origin. Push and Compile also reject any URL override that differs from that pin. Unset the conflicting
override or use a separate initialized project directory for the other environment; do not redirect existing
private Project state. Status and retained download commands continue using their pin.
private Project state. Status, retained download, and cancel commands continue using their pin.

`authentication_required` means the selected environment's token is missing or rejected. Staging uses
`authentication_required` means the selected environment's token is missing or rejected with `401`. Staging uses
`FIRSTDRAFT_STAGING_API_TOKEN`, including old staging Projects with no flag. Production and custom origins use
`FIRSTDRAFT_API_TOKEN`. When that variable is unset, the token saved by `firstdraft login` for the exact origin is
used. The CLI never substitutes one environment's credential for another's. Run `firstdraft login` for the same
Expand Down Expand Up @@ -130,10 +130,37 @@ operation is safe to remove. After successful adoption, run retained status and

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
`plan compile`. `compilation_failed`, `compilation_cancelled`, and `compilation_changed` already carry the
`plan compile`. If it stays `queued` or `running`, follow [stuck Compilation recovery](#stuck-compilation-recovery).
`compilation_failed`, `compilation_cancelled`, and `compilation_changed` already carry the
validated `current` projection appropriate to their stopping boundary. Authentication recovery may refresh the
credential, but it must continue from the retained ID rather than starting another Compilation.

## Stuck Compilation recovery

A Project allows one active Compilation. While one is queued or running, `plan push` and `plan compile` report
`server_rejected` with First Draft's `409 compilation_active` problem in `response`, because the Plan cannot be
replaced. `plan compile` can also meet it at the Compilation start, as `compilation_start_rejected`. Either problem's
`response.detail` names the active Compilation's ID.

1. Read that Compilation once with `firstdraft compilation status <id>`. One that has reached `succeeded`, `failed`,
or `cancelled` no longer blocks; rerun `plan compile`.
2. If it is still `queued` or `running` well after a Compilation normally finishes, for example after a
`plan compile` wait timed out, cancel it with `firstdraft compilation cancel <id>`. Cancelling throws away any
work it is still doing.
3. After the cancel succeeds, rerun `plan compile`.

Cancel never changes a succeeded or failed Compilation, and repeating it is safe:

- `compilation_cancel_rejected` means First Draft refused and nothing was cancelled; `response.code` says why.
`409 compilation_not_cancellable` means the Compilation already succeeded or failed, so it no longer blocks; check
it with `compilation status`. `404 compilation_not_found` or `404 project_not_found` means First Draft found no
such Compilation for this Project and account. Check the ID, the directory, and that the environment and login
match the ones that started it.
- `compilation_cancel_unavailable` means a network failure, timeout, or server error left the outcome unconfirmed.
Rerun `compilation cancel`, or check the result with `compilation status`.
- `invalid_compilation_status` means the response violated the Compilation contract, so the cancellation may or may
not have happened. Check with `compilation status` rather than repeating the unchanged request.

## Publication recovery

The Publication is a Project singleton. If its initial conditional `PUT` is ambiguous, the CLI attempts one
Expand Down Expand Up @@ -178,6 +205,9 @@ stopped without following the replacement.
| `compilation status`, `compilation download` | `compilation_status_unavailable`, `invalid_compilation_status` | 1 | The retained status could not be read or violated its exact contract. |
| `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. |
| `compilation cancel` | `compilation_cancel_rejected` | 1 | Nothing was cancelled; `response.code` says why ([recovery](#stuck-compilation-recovery)). |
| `compilation cancel` | `compilation_cancel_unavailable` | 1 | The cancellation could not be confirmed; repeating it is safe. |
| `compilation cancel` | `invalid_compilation_status` | 1 | The response violated the Compilation contract; check status before repeating. |
| 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 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. |
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firstdraft.com/cli",
"version": "0.8.1",
"version": "0.8.2",
"description": "Command-line interface for First Draft",
"license": "MIT",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion release/compatibility.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"format": "firstdraft.release-compatibility/1",
"component": "cli",
"version": "0.8.1",
"version": "0.8.2",
"requires": {
"api_contract": [">= 0.7.0", "< 0.8.0"],
"foundation_plan_formats": ["firstdraft.foundation-plan.sketch/0.23"]
Expand Down
37 changes: 37 additions & 0 deletions scripts/smoke-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,16 @@ try {
"Invalid arguments. Run 'firstdraft compilation status --help' for usage.",
});

const invalidCancel = spawnPackedCli(
["compilation", "cancel", "not-a-uuid"],
installationDirectory,
);
assertHandledFailure(invalidCancel, 2, {
error: "invalid_arguments",
detail:
"Invalid arguments. Run 'firstdraft compilation cancel --help' for usage.",
});

const invalidLogin = spawnPackedCli(
["login", "--canary-secret-option"],
installationDirectory,
Expand Down Expand Up @@ -520,6 +530,14 @@ async function exercisePackedCompilation(projectDirectory) {
completed_at: "2026-07-30T12:00:02.000000Z",
},
};
const cancelled = {
project: compilation.project,
compilation: {
...compilation.compilation,
status: "cancelled",
artifact: null,
},
};
const analysis = {
project: { id: projectId, graph_version: 1 },
analysis: {
Expand Down Expand Up @@ -590,6 +608,7 @@ async function exercisePackedCompilation(projectDirectory) {
publication: 0,
compilationStarts: 0,
status: false,
cancel: false,
artifact: false,
};
const server = createServer(async (request, response) => {
Expand Down Expand Up @@ -652,6 +671,13 @@ async function exercisePackedCompilation(projectDirectory) {
respondJson(response, 200, compilation);
return;
}
if (request.method === "POST" && request.url === `${statusPath}/cancel`) {
assert.equal(request.headers["if-match"], undefined);
assert.equal(requestBody.byteLength, 0);
seen.cancel = true;
respondJson(response, 200, cancelled);
return;
}
if (request.method === "GET" && request.url === artifactPath) {
seen.artifact = true;
response.writeHead(200, {
Expand Down Expand Up @@ -738,6 +764,16 @@ First Draft: Application compiled.
stderr: "",
});

const cancel = await spawnPackedCliAsync(
["compilation", "cancel", compilationId],
projectDirectory,
);
assert.deepEqual(cancel, {
status: 0,
stdout: `${JSON.stringify(cancelled, null, 2)}\n`,
stderr: "",
});

const output = path.join(projectDirectory, "generated");
const downloaded = await spawnPackedCliAsync(
["compilation", "download", compilationId, "--output", output],
Expand Down Expand Up @@ -852,6 +888,7 @@ First Draft: Application compiled.
publication: 1,
compilationStarts: expectedCompilationStarts,
status: true,
cancel: true,
artifact: true,
});
} finally {
Expand Down
Loading
Loading