Integrate the initial First Draft CLI - #5
Conversation
Create a source-executed ESM command shell for the unscoped firstdraft package with deterministic help, version, exit status, and non-echoing usage errors. Keep the runtime dependency-free and free of installation hooks, telemetry, update checks, or implicit network access. Allowlist and inspect the exact tarball, then install and execute it offline as part of the checks. Pin development and release work to Node 24 while proving the Node 22.0 runtime floor on Linux and Windows. Scope test discovery to the repository test tree and use the working npm shell shim on the affected Windows floor runtime.
Agents need a deterministic, private starting point before network behavior arrives. Add plan init to create the exact empty sketch/0.19 document and UUIDv7 Project state beneath a nested ignored .firstdraft directory. Validate keys and names at the schema and I-JSON boundary before invoking randomness or the filesystem. Refuse every existing .firstdraft path and use exclusive writes so initialization never overwrites existing data. Extend the package allowlist and tests for the new runtime files, and exercise the Node 22 compatibility job across Linux, Windows, and macOS.
Send the exact local Plan representation with a create-or-match precondition so agents cannot silently overwrite a newer remote head. Pin the accepted API origin and opaque ETag in private local state, validate response identity before updating it, and report ambiguous transport outcomes without inventing recovery data.
Foundation Plan authors need stable identities before adding independently mutable subjects, but the Skill should not invent or persist them itself. Expose the existing UUIDv7 generator as a pure leaf command with no project or network access. Document the continuity rules and exercise both direct and packed-package paths at the minimum runtime.
Technical reviewA note on scope first. The body positions this as an integration of #1 through #4, so a merge-shaped review would be the natural approach. Those four opened before this watcher's cutoff and were never reviewed here, so nothing in the diff has been read before. This is a full review of the source rather than a check that four reviewed slices survived integration. The branch is also a fast-forward line of four commits rather than a merge, so there is no resolution to audit. What the PR is really asking for is a first read of the whole CLI. Verification reproduced
The conditional push is the load-bearing part, and the safety argument holdsEverything else in this PR is scaffolding around one operation: replacing a whole Foundation Plan on the server without ever silently overwriting somebody else's work. The mechanism is a precondition on every request, chosen by whether local state already knows an ETag: ...(state.foundation_plan_etag
? { "If-Match": state.foundation_plan_etag }
: { "If-None-Match": "*" })I traced the failure paths to see whether any of them can lose a Plan, since that is the property worth having. None can:
So the design's real strength is that the precondition makes every failure mode safe, including the ones the client cannot detect. The recovery path exists to keep the user unstuck, not to prevent data loss, because the conditional header already prevents that. Two details I want to call out as good rather than merely correct. foundationPlan.source_sha256 === sourceSha256That turns "the server said 200" into "the server stored exactly the bytes I sent", which is a meaningfully stronger claim. The supporting evidence is unusually well chosenThree things stood out as the kind of check that catches real regressions rather than restating the implementation. The UUIDv7 test uses the RFC's own vector.
Errors never echo user input. Tests push The CI matrix is also right in a way that is easy to get wrong. It runs the One design observation for agent callers, not a defectThis is a suggestion rather than something wrong, and it only matters because the intended caller is an agent rather than a person. Five distinct push failures collapse to exit code 1: local read, network, protocol, server rejection, and the state-write failure. Exit 2 is reserved for usage and configuration. The situations call for different responses, though. A network failure is safe to retry. A server rejection needs the Plan fixed. A state-write failure specifically must not be retried until state is repaired, which is exactly what its message says. The stderr format also varies by case. The state-write failure emits JSON with a machine-readable Either distinct exit codes or an I should note one hypothesis I checked and discarded. The push tests exercise Small notes
|
Lesson: the lost update, and the header that prevents itHere is a bug that no amount of careful Ruby will fix. Two people open the same record. Both edit. Both save. The second save wins completely, and the first person's work is gone with no error, no warning, and nothing in the log to suggest anything happened. Bob never saw Alice's version. His browser sent the state of the world as of 10:01, and the server took it. This is called the lost update problem, and it is not a race condition in the usual sense. There is no tight timing window. Bob had three full minutes to cause it. The CLI in this PR pushes an entire Foundation Plan document to the server in one request, so it has this problem in its purest form. Two agents working on the same project would overwrite each other silently. What follows is how it avoids that. Version numbers, and why HTTP already has oneThe instinct from Rails is HTTP has the same idea built in, and you have probably seen half of it. When a server sends a response it can include an You most likely know this from caching. Rails' The write side uses the same tag and is far less widely used. The client sends the version it is trying to replace: The server compares. If the resource is still at Note what that buys you: the check and the write happen in one request, on the server, where nothing can slip between them. A client that fetched first and then wrote would have a window between the two. This has no window. The trick for creating something that does not exist yet
The header for that reads oddly the first time:
So the CLI picks its precondition based on whether it has seen an ETag before: ...(state.foundation_plan_etag
? { "If-Match": state.foundation_plan_etag }
: { "If-None-Match": "*" })Two lines, and every write is now conditional. The part that took me a while to appreciateStoring the ETag locally after a successful push introduces an obvious worry. What if the push succeeds and then writing the local file fails? The disk is full, or the directory went read-only. The server has your new Plan, and your machine has no record of it. The tempting reaction is that this needs careful handling to avoid corruption. It does not, and the reason is worth sitting with. Your local state still holds the old ETag, or no ETag at all. So your next push sends a precondition that is now stale, and the server answers That changes what the error handling is for. It is not preventing data loss, because the header already did. It is getting the user unstuck, which is why the CLI prints the ETag it received and tells you not to push again until you have repaired local state: {
"error": "local_state_not_saved",
"detail": "The Plan was accepted, but its ETag could not be saved. Do not push again until local state is repaired.",
"recovery_state": { "foundation_plan_etag": "\"a1b2c3\"", "...": "..." }
}The same reasoning covers a network failure mid-request. You genuinely cannot know whether the server got it. You do not need to know, because whichever happened, your next conditional push either works or gets refused. It never destroys anything. That is the general shape worth taking away. Design so that "I do not know what happened" and "nothing bad can happen" are compatible. A system that requires the client to know the truth will eventually be wrong about it. Where this shows up in your Rails workAny form that edits a record somebody else might also be editing has this problem, and If you are building a JSON API, the HTTP version is worth knowing because it is standard and clients already understand it. Return an The general rule: a blind overwrite is only safe when you are the only writer. As soon as that stops being true, and it usually stops being true earlier than expected, you want the write to carry a claim about what it expects to be replacing. |
|
Thanks — Skills #6 makes this a concrete consumer contract rather than hypothetical polish. I agree the Skill should not have to distinguish recovery branches by matching English prose.\n\nI do not think this should amend the integration PR. The current behavior remains fail-closed and preserves conditional-write safety; changing the output contract while integrating the four existing commits would obscure that review. The first post-integration CLI slice should make plan push failures uniformly machine-readable, followed by the corresponding Skill update.\n\nA bounded contract:\n\n- Keep the current exit-status classes: 0 for success, 1 for operational or remote failure, and 2 for usage or configuration.\n- Emit exactly one JSON object to stderr for every handled plan push failure.\n- Give that object a stable top-level error discriminator, with a small vocabulary such as invalid_arguments, invalid_configuration, local_input_unreadable, request_outcome_unknown, server_rejected, and the existing local_state_not_saved.\n- Use request_outcome_unknown for both transport ambiguity and a success-shaped response that could not be verified. Its recovery rule is stop and reconcile; do not infer acceptance or retry.\n- Have server_rejected include the HTTP status and a validated server problem or diagnostic payload when safe. Keep recovery_state only on local_state_not_saved.\n- Never echo argv, local Plan bytes, raw network errors, or unvalidated response bodies.\n- Exercise every branch through the full CLI runner and the packed artifact.\n\nThat lets Skills #6 key both damaged-local-files and ambiguous-outcome behavior entirely on stable codes. I am leaving #5 unchanged and unmerged. |
Summary
firstdraftcommand shell and audited package boundarysketch/0.19Foundation Plan drafts with client-minted Project identityIntegration boundary
This PR preserves the four logically discrete commits previously reviewed in the component stack. It introduces no behavior beyond those slices and adds no authentication, pull, reconciliation, publication, or Compilation command.
The component PRs remain open as the slice-level review history:
They will remain open until this integration PR has completed hosted review and CI.
Verification
npm run check