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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ handled-error recovery in `docs/errors.md`, living release policy in `RELEASING.
in `docs/release-history.md`. When behavior changes, update its owning document in the same change.

- Verify with `npm run check`, which takes about 30 seconds. A fresh checkout needs `npm ci --ignore-scripts` first.
- To add a command, follow `docs/commands.md#add-a-command`. To change the version, follow the `npm version` step in
`RELEASING.md`.
- Sibling repositories: `firstdraft/firstdraft` (private) owns the Service API and Plan format; `firstdraft/skills`
owns the Skill and plugin packaging.
- Changes to the accepted API-contract range, accepted Plan formats, command names or flags, handled `error` values,
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Run firstdraft --help or a command group's --help for concise terminal syntax.
| bin/ | Published executable entrypoint |
| src/ | Commands, API client, local Plan state, and output contracts |
| test/ | Command, protocol, recovery, and package tests |
| scripts/ | Test runner and package allowlist/smoke checks |
| scripts/ | Test runner, package checks, and version sync |
| docs/ | Command, error, release-history, and maintainer documentation |

## Development
Expand Down
5 changes: 4 additions & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ not authorization or runtime proof. Its closed `firstdraft.release-compatibility

## Prepare before merge

1. Update `package.json`, `package-lock.json`, and `release/compatibility.json`, and align the Skills CLI requirement.
1. Set the version with `npm version <x.y.z> --no-git-tag-version --ignore-scripts=false`. npm updates `package.json`
and `package-lock.json`, and the package's `version` script copies the version into `release/compatibility.json`.
The last flag is required because `.npmrc` sets `ignore-scripts=true`, which also skips that script. If it was
omitted, run `node scripts/sync-version.js --apply`. Then align the Skills CLI requirement.
2. Update the command, error, and Skill guidance affected by the change. When onboarding changes, coordinate the
[local guide](https://gist.github.com/raghubetina/3d424a97a1eaa6de8c406e67f32a237e) publication from the Service's
`docs/guides/local-app.md` before the new CLI reaches `latest`. Preserve dated release evidence.
Expand Down
15 changes: 7 additions & 8 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ evidence for implemented behavior; if they contradict a document, surface the co
| Local app development | [Local guide](https://gist.github.com/raghubetina/3d424a97a1eaa6de8c406e67f32a237e) |
| Codespaces fallback | [Drawing Board guide](https://github.com/firstdraft/drawing-board#build-an-app-with-first-draft) |
| Installation or package contract | [Root README](../README.md) |
| Commands, API, or output | [Command reference](commands.md) |
| Commands, Service API, or output | [Command reference](commands.md); [add a command](commands.md#add-a-command) |
| Errors and recovery | [Errors and recovery](errors.md) |
| Versioning and publication | [Release policy and runbook](../RELEASING.md) |
| Dated release observations | [Release history](release-history.md) |
Expand All @@ -17,8 +17,8 @@ evidence for implemented behavior; if they contradict a document, surface the co
## Authority boundaries

- [README.md](../README.md) owns repository orientation, direct installation, package boundaries, and routes.
- [commands.md](commands.md) owns detailed command semantics. Built-in `--help`, runtime source, and tests own exact
executable syntax and behavior.
- [commands.md](commands.md) owns detailed command semantics, Service endpoints, and the add-a-command checklist.
Built-in `--help`, runtime source, and tests own exact executable syntax and behavior.
- [errors.md](errors.md) owns handled-error interpretation and recovery guidance.
- [RELEASING.md](../RELEASING.md) owns living release policy and the operator runbook.
- [release-history.md](release-history.md) preserves dated release observations. Recheck live tags, package versions,
Expand All @@ -31,11 +31,10 @@ evidence for implemented behavior; if they contradict a document, surface the co
Start here, then load the one owning document for the task. Follow a cross-link only when the task crosses an
authority boundary. Create another page only for a distinct audience, task, or authority.

The documentation tests keep `AGENTS.md` at or below 2 KiB, the root README at or below 6 KiB, and this map at or
below 4 KiB. They also require every public topic to remain reachable from this map or the root README and verify
repository-local links and fragments. Outside release history, they reject retired version identities and
release-status labels on the current version. The package check separately verifies that every relative link in the
packaged Markdown resolves inside that exact package.
The documentation tests cap `AGENTS.md` at 2 KiB, the root README at 6 KiB, and this map at 4 KiB. They also
require every public topic to be reachable from this map or the root README, and they check local links and
fragments. Outside release history, they reject retired version identities and release-status labels on the current
version. The package check verifies that relative links in packaged Markdown resolve inside the package.

## Work on the repository

Expand Down
96 changes: 94 additions & 2 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ or `superseded`. Every validated analysis status is a successful read with exit
`analysis.status` value and inspect `analysis.diagnostics` rather than treating a completed analysis with issues as
a transport failure.

The projection includes the exact Head digest, Analyzer and Compiler releases, selected target, and
`analysis.gap_set` plus `analysis.gap_set_sha256`. A `valid` run always returns the complete parsed canonical
The projection includes the exact [Head](#head-and-its-etag) digest, Analyzer and Compiler releases, selected target,
and `analysis.gap_set` plus `analysis.gap_set_sha256`. A `valid` run always returns the complete parsed canonical
`firstdraft.foundation-gaps/2` object, including every ordered gap record and an empty `gaps` array when nothing is
missing. Both GapSet fields are `null` for every other status. The CLI validates the GapSet's Head, Project,
generation, releases, target, canonical digest, and complete record shapes, then prints the records without
Expand Down Expand Up @@ -399,3 +399,95 @@ sibling directory, verifies the complete tree, and atomically renames it into th
POSIX, directories use mode `0755` and files use artifact-declared `0644` or `0755`; Windows verifies structure,
contents, and digests without claiming POSIX mode bits. The declared and streamed artifact envelope is bounded at
128 MiB.

## Service endpoints

The CLI calls these Service API routes at the origin pinned for the Project. Every request sends the selected token
as a Bearer credential, refuses redirects, and has a bounded timeout. Each request takes its method and path from one
`SERVICE_ROUTES` entry in `src/api-response.js`. `test/service-endpoints.test.js` fails when this table and
`SERVICE_ROUTES` differ, or when no file in `src/` uses a declared route.

| Method | Path | Purpose |
| ------ | ------------------------------------------------------------------ | ------------------------------------ |
| `PUT` | `/v1/projects/{project_id}/foundation-plan` | Create or replace the Head |
| `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 |
| `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 |

`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.

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.

These 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
declared ranges before a release. The Service documents the routes in its Foundation Plan machine reference,
`docs/architecture/reference/README.md` in the private `firstdraft/firstdraft` repository.

### Head and its ETag

The Head is the exact Foundation Plan bytes that First Draft holds for a Project. The latest accepted
`PUT /v1/projects/{project_id}/foundation-plan` sets it. The Service stores those bytes unchanged. A byte change that
keeps the Plan's meaning still makes a new Head, although the Project's `graph_version` stays the same.

A successful Plan `PUT` returns a strong `ETag` of the form `"sha256:<hex>"`, quotes included. `<hex>` is the
64-character lowercase SHA-256 of the Head bytes. The CLI saves the complete header value in `.firstdraft/state.json`
and replays it in `If-Match`. `plan compile` also extracts `<hex>` to check that the local Plan still matches the
Head before it starts a Compilation or Publication. A saved ETag in any other form stops that check with
`invalid_configuration`.

The same digest appears as `head_source_sha256` in analysis and Compilation responses. To tell whether the local Plan
is the Head, compare the SHA-256 of `.firstdraft/foundation-plan.json` with `analysis.head_source_sha256` from
`plan status`. The artifact download expects an ETag of the same form over the artifact bytes.

The Service reference asks clients to replay the Plan ETag without interpreting it. This CLI parses it anyway. A
Service ETag in another form would make every `plan compile` stop with `invalid_configuration`.

## Add a command

Use this checklist when a change adds a command or subcommand. Each step names the file to change.

1. **Implement it.** Put the command's logic in a module under `src/commands/`, usually `<group>-<name>.js`. Accept
`fetchFunction`, file system functions, clocks, and request signals as options, as the existing commands do, so
tests can replace them. Throw a named error class for each failure the command handles. Build each Service
request with `serviceEndpoint` and a route declared in `SERVICE_ROUTES` in `src/api-response.js`.
2. **Dispatch it in `src/cli.js`.**
- Add a `<GROUP>_<NAME>_HELP` string, and list the command in its group's help: `PLAN_HELP`,
`COMPILATION_HELP`, or `GENERATE_HELP`. A new group also needs a line in `ROOT_HELP` and a branch in `run`.
- Add the branch in `runPlan`, `runCompilation`, or `runGenerate`, and a `run<Group><Name>` function.
- Parse arguments with strict `parseArgs`. Invalid syntax writes `invalid_arguments` and exits 2.
- A command that calls the Service also accepts `--staging` and calls `authenticateApiCommand`. It maps each
error class to one `writeJson(stderr, …)` envelope and its exit status.
3. **Test it.**
- Add `test/<group>-<name>.test.js`. Cover the help text, invalid arguments, the success output, and every
handled `error` value.
- Update the exact group help in the tests: `HELP` in `test/cli.test.js`, `PLAN_HELP` in `test/plan-init.test.js`,
or `GENERATE_HELP` in `test/generate-uuid.test.js`. No test asserts the whole `compilation` group help.
- Append a command that calls the Service to `REMOTE_COMMANDS` in `test/api-environments.test.js`. Some tests
there select entries by index, so add it at the end. The command's first request must reach the pinned origin
with that origin's token, and one `401` must produce `authentication_required`.
- Add packed-package cases to `scripts/smoke-package.js`, including at least an invalid-arguments case.
4. **Package it.** Add each new `src/` file to the exact list in `scripts/check-pack.js`. `firstdraft/skills` keeps
a copy of that list as `packedFileAllowlist` in `script/cli-contract/config.mjs`. The copy must change when
Skills bundles this CLI version.
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.
- 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
`firstdraft/skills` with its CLI pin.
6. **Review it.** New command names, flags, `error` values, and exit statuses need the independent review named in
`AGENTS.md`.
7. **Version it.** Choose the next version with the [version policy](../RELEASING.md#version-and-compatibility-policy)
and apply it with the [version step](../RELEASING.md#prepare-before-merge). If the command needs a route or
response that the accepted API range lacks, the Service ships it first under a new API-contract version. Then
raise `requires.api_contract` in `release/compatibility.json`, and align the Skills CLI requirement. Because the
CLI does not read the contract header, an older Service rejects the new route as not found.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"pack:check": "node scripts/check-pack.js",
"pack:smoke": "node scripts/smoke-package.js",
"test": "node scripts/run-tests.js",
"typecheck": "tsc --project jsconfig.json"
"typecheck": "tsc --project jsconfig.json",
"version": "node scripts/sync-version.js --apply"
},
"devDependencies": {
"@eslint/js": "10.0.1",
Expand Down
19 changes: 11 additions & 8 deletions scripts/smoke-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@ import { createServer } from "node:http";
import { tmpdir } from "node:os";
import path from "node:path";

import { RAILS_TARGET_PROFILE } from "../src/compilation-artifact.js";

const npmCli = requiredEnvironmentVariable("npm_execpath");
const apiToken = `fd_${"a".repeat(43)}`;

/** @type {{name: string, version: string}} */
const packageMetadata = JSON.parse(readFileSync("package.json", "utf8"));
/** @type {[string]} */
const [planFormat] = JSON.parse(
readFileSync("release/compatibility.json", "utf8"),
).requires.foundation_plan_formats;
const temporaryDirectory = mkdtempSync(path.join(tmpdir(), "firstdraft-cli-"));
const installationDirectory = path.join(temporaryDirectory, "installation");
const packedExecutable = path.join(
Expand Down Expand Up @@ -149,8 +155,8 @@ try {
name: initializedPlan.application.name,
},
{
format: "firstdraft.foundation-plan.sketch/0.23",
target: { id: "rails", profile: "rails-sketch/2026-09-bookmark-assets" },
format: planFormat,
target: { id: "rails", profile: RAILS_TARGET_PROFILE },
key: "oscar_party",
name: "Oscar Party",
},
Expand Down Expand Up @@ -392,10 +398,7 @@ async function exercisePackedCompilation(projectDirectory) {
"foundation-plan-rails/application-2026-09-27-bookmark-assets";
const compilerRelease =
"foundation-plan-rails/compiler-application-2026-09-27-bookmark-assets";
const target = {
id: "rails",
profile: "rails-sketch/2026-09-bookmark-assets",
};
const target = { id: "rails", profile: RAILS_TARGET_PROFILE };
const gapSet = {
format: "firstdraft.foundation-gaps/2",
source: { sha256: headSha256 },
Expand Down Expand Up @@ -450,7 +453,7 @@ async function exercisePackedCompilation(projectDirectory) {
graph_version: 1,
head_source_sha256: headSha256,
foundation_plan: {
format: "firstdraft.foundation-plan.sketch/0.23",
format: planFormat,
sha256: foundationPlanSha256,
},
analysis: {
Expand Down Expand Up @@ -585,7 +588,7 @@ async function exercisePackedCompilation(projectDirectory) {
{
project: { id: projectId, graph_version: 1 },
foundation_plan: {
format: "firstdraft.foundation-plan.sketch/0.23",
format: planFormat,
source_sha256: headSha256,
},
diagnostics: [],
Expand Down
Loading
Loading