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
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ canonical Skills and bundled CLI are packaged once for Claude Code and Codex. UI
app's own design and components. The public catalog and Drawing Board pins determine what an installed workspace
actually receives.

Source candidate `0.6.0` requires CLI `0.6.0`, Service API `0.6`, and Foundation Plan `0.22`. It preserves the planning
workspace under `.firstdraft/design/` and defaults to current-folder local output. Use explicit `--github` for server
Source candidate `0.7.0` requires CLI `0.7.0`, Service API `0.6`, and Foundation Plan `0.22`. New remote work uses production
at `https://firstdraft.com`; staging is explicit with `firstdraft --staging ...` and a separate staging token.
It preserves the planning workspace under `.firstdraft/design/` and defaults to current-folder local output.
Use explicit `--github` for server
Publication. Its identities belong in
[release compatibility](release/compatibility.json). The [public catalog](.claude-plugin/marketplace.json) owns the
installed version; source compatibility does not establish publication. Only `create-full-stack-app` is packaged;
Expand Down Expand Up @@ -173,8 +175,12 @@ Those actions follow the machine-owned [compatibility record](release/compatibil

## Credential boundary

The Skill expects FIRSTDRAFT_API_URL and FIRSTDRAFT_API_TOKEN to be supplied by its workspace. Its shared helper
prefers the project's `bin/firstdraft` credential wrapper, then the bundled CLI, then an installed CLI on PATH.
The workspace supplies `FIRSTDRAFT_API_TOKEN` for production at `https://firstdraft.com`. For staging, use
`firstdraft --staging ...` and `FIRSTDRAFT_STAGING_API_TOKEN` from `https://staging.firstdraft.com`; the CLI never
falls back to the production token for staging. Existing Plans keep their saved origin. `FIRSTDRAFT_API_URL` is an
advanced custom-server override; a conflicting URL cannot be combined with `--staging` or redirect an existing Project.
The shared helper prefers the project's `bin/firstdraft` credential wrapper, then the bundled CLI, then an
installed CLI on PATH.
It preserves the working directory and arguments. The adapter forwards ambient credentials; it does not read
ignored credential files or provide an authentication UI. Claude Code does not deliver plugin `userConfig` to
`bin/` executables; a secure bridge is tracked in [issue #27](https://github.com/firstdraft/skills/issues/27).
Expand Down
11 changes: 6 additions & 5 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ longer part of an ordinary release. Coordinate the service, CLI, and Skills thro

[`release/compatibility.json`](release/compatibility.json) owns the candidate version, compatible CLI/API/Plan
identities, and deterministic package SHA-256. The current source candidate is
`@firstdraft.com/claude-code@0.6.0` with CLI `0.6.0`, API `>= 0.6.0`, `< 0.7.0`, and Plan `sketch/0.22`.
`@firstdraft.com/claude-code@0.7.0` with CLI `0.7.0`, API `>= 0.6.0`, `< 0.7.0`, and Plan `sketch/0.22`.
The [marketplace manifest](.claude-plugin/marketplace.json) independently selects a published plugin version;
retain its selection until the intended new version is actually published. Source compatibility is not public
catalog selection. Query npm when releasing rather than treating a dated distribution snapshot as current.

Plan `0.22` adds optional `application.pwa` and replaces the sole accepted `0.21` input format. Omission and `true`
enable ordinary online installation metadata; `false` omits it. API, CLI, and plugin `0.6.0` record that input and
artifact compatibility break. Target `rails-sketch/2026-09`, local-output defaults, and `.firstdraft/design/` stay
the same. There is no retained-Project migration or compatibility bridge.
CLI and plugin `0.7.0` require `FIRSTDRAFT_STAGING_API_TOKEN` for the staging origin, including existing staging
Plans that previously used `FIRSTDRAFT_API_TOKEN`. The separate credential is a breaking CLI configuration change;
API `0.6` and Plan `0.22` are unchanged. New remote work defaults to production and root `--staging` selects staging.
Saved Project origins, target `rails-sketch/2026-09`, local-output defaults, and `.firstdraft/design/` stay the same.
There is no retained-Project migration or compatibility bridge.

Use an ordinary pre-1.0 minor bump for a breaking compatibility change and a patch bump for a compatible change.
Never reuse a published npm version, protected release tag, or marketplace version for different package bytes.
Expand Down
2 changes: 1 addition & 1 deletion packages/claude-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "firstdraft",
"displayName": "First Draft",
"version": "0.6.0",
"version": "0.7.0",
"description": "Author a Foundation Plan and compile a bounded Rails, iPhone, and Android application",
"author": {
"name": "First Draft",
Expand Down
2 changes: 1 addition & 1 deletion packages/claude-plugin/package.template.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firstdraft.com/claude-code",
"version": "0.6.0",
"version": "0.7.0",
"description": "First Draft Foundation Plan authoring and UI development for Claude Code and Codex",
"license": "MIT",
"type": "module",
Expand Down
6 changes: 3 additions & 3 deletions release/compatibility.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"format": "firstdraft.release-compatibility/1",
"component": "skills",
"version": "0.6.0",
"version": "0.7.0",
"plugin_source": {
"package": "@firstdraft.com/claude-code",
"tarball_sha256": "23ca0098605ed1a2981648961fe7c5355b5b6280f3f3db6f4b957872778a9b08"
"tarball_sha256": "7a445058a78abf401308a79a7cf4decb3a8462f483284b8bf74c5d11b0d265c7"
},
"requires": {
"api_contract": [
">= 0.6.0",
"< 0.7.0"
],
"cli": [
"= 0.6.0"
"= 0.7.0"
],
"foundation_plan_formats": [
"firstdraft.foundation-plan.sketch/0.22"
Expand Down
40 changes: 36 additions & 4 deletions script/check-claude-plugin-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const cleanEnvironment = {...process.env};
for (const name of [
"FIRSTDRAFT_API_URL",
"FIRSTDRAFT_API_TOKEN",
"FIRSTDRAFT_STAGING_API_TOKEN",
"CLAUDE_PLUGIN_OPTION_api_url",
"CLAUDE_PLUGIN_OPTION_api_token",
"CLAUDE_PLUGIN_OPTION_API_URL",
Expand Down Expand Up @@ -151,22 +152,23 @@ try {
);
}
const canaryToken = `fd_${"a".repeat(43)}`;
const stagingCanaryToken = `fd_${"s".repeat(43)}`;
const execution = run(
pluginExecutable(fakeInstallation),
["probe"],
["--staging", "probe"],
fakeInstallation,
{
...cleanEnvironment,
CLAUDE_PLUGIN_OPTION_API_URL: "https://wrong.example.com",
CLAUDE_PLUGIN_OPTION_API_TOKEN: `fd_${"b".repeat(43)}`,
FIRSTDRAFT_API_URL: "https://staging.firstdraft.com",
FIRSTDRAFT_API_TOKEN: canaryToken,
FIRSTDRAFT_STAGING_API_TOKEN: stagingCanaryToken,
},
);
assert.deepEqual(JSON.parse(execution.stdout), {
apiToken: canaryToken,
apiUrl: "https://staging.firstdraft.com",
arguments: ["probe"],
stagingApiToken: stagingCanaryToken,
arguments: ["--staging", "probe"],
lowercasePluginApiTokenPresent: false,
lowercasePluginApiUrlPresent: false,
uppercasePluginApiTokenPresent: true,
Expand Down Expand Up @@ -221,6 +223,35 @@ try {
);
assert.equal(version.stdout, `${cliPackageVersion}\n`);
assert.equal(version.stderr, "");

const project = path.join(actualInstallation, "environment-check");
mkdirSync(project);
run(
pluginExecutable(actualInstallation),
["plan", "init", "--name", "Environment check"],
project,
cleanEnvironment,
);
const conflictingEnvironment = spawnSync(
pluginExecutable(actualInstallation),
["--staging", "plan", "push"],
{
cwd: project,
encoding: "utf8",
env: {
...cleanEnvironment,
FIRSTDRAFT_API_URL: "http://127.0.0.1:1",
FIRSTDRAFT_API_TOKEN: canaryToken,
FIRSTDRAFT_STAGING_API_TOKEN: stagingCanaryToken,
},
},
);
assert.equal(conflictingEnvironment.status, 2);
assert.equal(conflictingEnvironment.stdout, "");
assert.equal(JSON.parse(conflictingEnvironment.stderr).error, "invalid_configuration");
for (const token of [canaryToken, stagingCanaryToken]) {
assert(!`${conflictingEnvironment.stdout}${conflictingEnvironment.stderr}`.includes(token));
}
}
} finally {
rmSync(temporaryDirectory, {recursive: true, force: true});
Expand All @@ -246,6 +277,7 @@ function createFakeCli(directory) {
`#!/usr/bin/env node
process.stdout.write(JSON.stringify({
apiToken: process.env.FIRSTDRAFT_API_TOKEN,
stagingApiToken: process.env.FIRSTDRAFT_STAGING_API_TOKEN,
apiUrl: process.env.FIRSTDRAFT_API_URL,
arguments: process.argv.slice(2),
lowercasePluginApiTokenPresent: Object.hasOwn(process.env, "CLAUDE_PLUGIN_OPTION_api_token"),
Expand Down
6 changes: 3 additions & 3 deletions script/cli-contract/config.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export const cliRevision = "38528f4402bc0a75ac855f0226569fbbc92488e3";
export const cliRevision = "fb45245c9030e1898f81fcafe108d877116bd752";
export const cliRuntimeSha256 =
"648cc0b32520d11c36d2c434fd078c56ca7e60e3e7770c7bd9349d7b331c128d";
"cb07b7e35938662383979dfd497d4d056ed2d1d2c2d871236454730cc5f9a8b2";
export const cliPackageName = "@firstdraft.com/cli";
export const cliPackageVersion = "0.6.0";
export const cliPackageVersion = "0.7.0";

export const safeGithubReasonCodes = Object.freeze([
"github.configuration_missing",
Expand Down
44 changes: 44 additions & 0 deletions script/cli-contract/local-commands.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@ import path from "node:path";
import Ajv2020 from "ajv/dist/2020.js";

import {
apiToken,
compilationTarget,
configuredApiUrl,
foundationPlanFormat,
storedApiUrl,
} from "./config.mjs";
import { acceptedPlanResponse } from "./fixtures.mjs";
import {
assertErrorEnvelope,
initializedProject,
invokeRunner,
pinRemoteState,
planPath,
sequenceFetch,
statePath,
} from "./harness.mjs";

const planSchema = JSON.parse(
Expand All @@ -28,6 +33,7 @@ const validatePlan = ajv.compile(planSchema);

export async function verifyLocalCommands(context) {
await verifyLocalFailureBoundaries(context);
await verifyStagingCredentials(context);

const rootHelp = await invokeRunner(
context.runCli,
Expand Down Expand Up @@ -140,6 +146,44 @@ export async function verifyLocalCommands(context) {
assertErrorEnvelope(protectedOutput, "invalid_output_path", { status: 2 });
}

async function verifyStagingCredentials(context) {
const cwd = await initializedProject(context, "staging-credentials");
const apiUrl = "https://staging.firstdraft.com";
const stagingApiToken = "canary-private-staging-token";
const missing = await invokeRunner(
context.runCli,
["--staging", "plan", "push"],
cwd,
{
apiUrl,
stagingApiToken: "",
fetchFunction: async () => assert.fail("production credentials must not reach staging"),
},
);
assertErrorEnvelope(missing, "authentication_required", {
privateValues: [apiToken, stagingApiToken],
});

const calls = [];
const result = await invokeRunner(
context.runCli,
["--staging", "plan", "push"],
cwd,
{
apiUrl,
stagingApiToken,
fetchFunction: sequenceFetch([acceptedPlanResponse(readFileSync(planPath(cwd)))], calls),
},
);
assert.equal(result.status, 0);
assert.equal(result.stderr, "");
assert(!result.stdout.includes(stagingApiToken));
assert.equal(calls.length, 1);
assert.equal(new URL(calls[0].input).origin, apiUrl);
assert.equal(calls[0].init.headers.Authorization, `Bearer ${stagingApiToken}`);
assert.equal(JSON.parse(readFileSync(statePath(cwd), "utf8")).api_url, apiUrl);
}

async function verifyLocalFailureBoundaries(context) {
const invalid = await invokeRunner(
context.runCli,
Expand Down
15 changes: 8 additions & 7 deletions skills/create-full-stack-app/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ explicit handoff. Follow [writing notes](references/modeling-guide.md#retain-imp

## Current boundary

Targets plugin 0.6.0, CLI 0.6.0, API 0.6, Plan 0.22; catalog selection is separate.
Targets plugin 0.7.0, CLI 0.7.0, API 0.6, Plan 0.22; catalog selection is separate.

- Bounded generation includes Web Accounts/Policies/Scaffolds, development data, and selected iPhone/Android
clients. [Appearance](references/foundation-plan-022.md#application-and-clients) controls theme, native colors,
Expand Down Expand Up @@ -82,7 +82,7 @@ firstdraft_cli --version
firstdraft_cli --help
```

Require the version probe to succeed with one exact `0.6.0` output line and no other output, and top-level help that
Require the version probe to succeed with one exact `0.7.0` output line and no other output, and top-level help that
lists `generate`, `plan`, and `compilation`. Contract tests own separate stdout and stderr assertions for leaf
commands; do not repeat them in a startup shell loop. The compatible CLI supplies these public commands:

Expand All @@ -94,11 +94,12 @@ There is no public `plan publish` or `plan subject-id`. Never replace the CLI au
If its version or help differs, report it and stop remote work instead of using HTTP directly; local Plan work
may continue. Verify the registry and catalog before recommending an installation or upgrade; a source candidate may be unreleased.

Treat `.firstdraft/state.json` as private CLI-owned concurrency state. Never print, paste, commit, or treat it as
Plan content. Let the user configure `FIRSTDRAFT_API_TOKEN` and any initial `FIRSTDRAFT_API_URL` outside the
conversation. Never request or expose a token. Follow a project wrapper's documented credential bootstrap without
reading or bypassing its ignored environment files. After the user confirms authentication is configured, resume
the already requested CLI operation without asking them to authorize it again.
New remote work uses `https://firstdraft.com`; for requested staging use `firstdraft_cli --staging plan push`.
Read [environment setup](references/diagnostics-and-recovery.md#local-state-and-credentials) before remote work.

`.firstdraft/state.json` is private CLI-owned state: never print, paste, commit, or treat it as Plan content.
Never request or expose tokens. Follow a wrapper's credential bootstrap without reading ignored environment files.
Once configured, resume the requested operation without fresh authorization.

## Initialize or resume the local Plan

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ JSON object. An unrecognized prefixed line, a progress line after the envelope,
interleaved output fail closed. Branch on the object's stable `error` and structured fields rather than the
human-readable `detail` or broad process exit status.

The source candidate uses `@firstdraft.com/cli@0.6.0`. Its exact reviewed revision and runtime digest are owned by
[the CLI contract configuration](https://github.com/firstdraft/skills/blob/claude-v0.6.0/script/cli-contract/config.mjs)
The source candidate uses `@firstdraft.com/cli@0.7.0`. Its exact reviewed revision and runtime digest are owned by
[the CLI contract configuration](https://github.com/firstdraft/skills/blob/claude-v0.7.0/script/cli-contract/config.mjs)
at this plugin's protected release tag. Check the command surface rather than assuming the version alone
establishes compatibility. These source checks do not prove plugin/catalog publication, service authentication,
staging compatibility, or a complete user journey.
Expand All @@ -33,7 +33,22 @@ CLI-owned Project, origin, and ETag state. Inspect the Plan; do not print, paste
agent-authored Plan content. Inspect only its `api_url` when a persistent read-only status failure requires checking
the pinned origin.

Let the user configure `FIRSTDRAFT_API_TOKEN` outside the conversation. Do not request its value, print it, place
`plan init` only creates local files; it does not choose a server. New remote work defaults to production at
`https://firstdraft.com`, using `FIRSTDRAFT_API_TOKEN`. For requested staging work, use the root flag before each
remote command (`firstdraft_cli --staging plan push`) and configure
`FIRSTDRAFT_STAGING_API_TOKEN` from `https://staging.firstdraft.com`. Token selection follows the saved origin,
including existing staging Plans resumed without the flag. The CLI never sends the production token to staging
as a fallback. Staging Plans made with an older CLI now need the separate staging token variable.

`FIRSTDRAFT_API_URL` selects an advanced custom server and uses `FIRSTDRAFT_API_TOKEN`, except that the exact
staging origin always requires `FIRSTDRAFT_STAGING_API_TOKEN`. Combining `--staging` with a different URL, or with
a non-staging saved origin, returns `invalid_configuration`. `plan push` and `plan compile` also reject a URL
override that differs from the saved origin; status and download commands keep using that origin and ignore the
URL override. None of these selections migrates a Project. Preserve the existing Plan and state. To start
independent work in another environment, use a separate folder with a new Plan; do not hand-edit private state or
automatically copy Project identity across environments.

Let the user configure credentials outside the conversation. Do not request either token's value, print it, place
it on a command line, or persist it in project files. When the user confirms authentication is configured, resume
the already requested operation without asking for fresh authorization.

Expand Down Expand Up @@ -133,7 +148,7 @@ local development is unsuitable, not a prerequisite.

### Direct local output

CLI 0.6.0 defaults to current-root adoption and archives the original workspace under `.firstdraft/design/`.
CLI 0.7.0 defaults to current-root adoption and archives the original workspace under `.firstdraft/design/`.
CLI 0.3.0 supports the same archive via explicit `--output .`; older 0.2.2 uses top-level `design/`. Preserve the actual
layout of an already materialized application; this change does not migrate it.

Expand Down Expand Up @@ -478,7 +493,7 @@ bytes and private state; create replacement work only within the user's authoriz
| `plan push`, `plan status`, `plan compile`, `compilation status`, `compilation download` | `authentication_required` | Configure the token outside the conversation. |
| `plan push`, `plan status`, `plan compile`, `compilation status`, `compilation download` | `local_input_unreadable` | Preserve unreadable local files; do not reconstruct private state. |
| `plan status`, `plan compile`, `compilation status`, `compilation download` | `project_not_pushed` | No accepted Project/origin is pinned locally. |
| `plan push`, `plan compile` | `invalid_configuration` | The API origin or saved Head state is incompatible. |
| `plan push`, `plan status`, `plan compile`, `compilation status`, `compilation download` | `invalid_configuration` | The API origin, `--staging` selection, or saved Head state is incompatible. |
| `plan push`, `plan compile` | `server_rejected` | Inspect the validated status, bounded response, and diagnostics. |
| `plan push`, `plan compile` | `local_state_not_saved` | The Plan was accepted but private ETag state was not saved. |
| `plan push`, `plan compile` | `request_outcome_unknown` | A mutation or its response was not fully verified. See phase rules below. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,13 @@ The bundled schema was copied byte-for-byte from
release or execution evidence.

The source candidate and pinned contract check use the exact reviewed CLI revision and runtime digest in
[the CLI contract configuration](https://github.com/firstdraft/skills/blob/claude-v0.6.0/script/cli-contract/config.mjs)
[the CLI contract configuration](https://github.com/firstdraft/skills/blob/claude-v0.7.0/script/cli-contract/config.mjs)
at this plugin's protected release tag, as contract provenance rather than release or execution evidence. The CLI
exposes `generate uuid`, `generate application-key`, `plan init`, `plan push`,
`plan status`, local `plan compile` (equivalent to `--output .`), explicit `plan compile --github`,
`plan compile --output`, `compilation status`, and
`compilation download`. It has no public `plan subject-id` or `plan publish`. The coordinated checkout declares the
`@firstdraft.com/cli@0.6.0` package. Direct output accepts the ordinary absent destination and, on POSIX,
`@firstdraft.com/cli@0.7.0` package. Direct output accepts the ordinary absent destination and, on POSIX,
current-root adoption by default or with `--output .`; the recovery reference owns its preconditions. Check commands
rather than inferring compatibility from a version number. These source checks do not prove plugin/catalog
publication, authentication, staging compatibility, or a complete user journey.
Expand Down
Loading
Loading