Push local Foundation Plans conditionally - #3
Conversation
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.
Review: technicalReviewed the single commit The live round tripServer: the #162 head,
The two failure rows are the ones worth having: a rejected Plan and a stale validator both leave Also verified
Design notes
Small
|
Lesson: what this PR actually didThe client half of the conditional PUT contract. The reusable ideas are validating a success response before believing it, and being honest when you genuinely cannot tell whether a write happened. The one-sentence version
Two repositories, one contractThe server side (firstdraft/firstdraft#162) added conditional replacement: an unconditional The client keeps three things in ...(state.foundation_plan_etag
? { "If-Match": state.foundation_plan_etag }
: { "If-None-Match": "*" })I checked the whole loop against a real server rather than a stub: init and push created the Project at graph version 1, editing the Plan and pushing again returned 200 with a rotated ETag at version 2, pushing the identical bytes again changed nothing, and a Plan the server rejects came back as a 422 pointing at Do not believe a 200The habit worth stealing is what happens between "the server said 200" and "update local state". A less careful client stores whatever ETag came back. This one refuses the response unless all of these hold:
That last check is the interesting one. The client hashes the file itself and compares against the server's claim. If those disagree, something in the middle changed the bytes, or the response describes a different document, and adopting that ETag would pin local state to a version the user never wrote. Comparing a value you computed against a value you were told is a cheap way to detect a whole class of "the network was not transparent" problems. The same skepticism applies to which server it is talking to. The failure you cannot resolveEvery network client has one genuinely ambiguous case: the request went out and no valid response came back. The write may or may not have happened. Most tools paper over this. This one says it plainly: Both halves matter. "May have been accepted" is the truth. "Local state was not changed" tells you what the tool did about it, which is nothing, because guessing either way would be worse: recording an ETag it never validated would corrupt the next push, and pretending failure would hide a change that landed. There is a rarer cousin handled the same way. If the server accepts the document and the local state write then fails, the push genuinely succeeded but the client cannot record it. Rather than swallow that, the error carries the state that should have been written so the user can save it by hand. The general rule: when you cannot determine what happened, say so and change nothing. An honest "I do not know" is more useful than a confident wrong answer, because the user can act on it. Why the client refuses weak validatorsETags come in strong and weak forms, and the client's pattern for a valid ETag has no That is not fussiness. The server compares That is the theme running through this command. Every check happens as early as possible and as close as possible to the thing being checked, so failures name their cause instead of surfacing three steps later as something unrelated. |
|
The first portable consumer scaffold is now firstdraft/skills#1. It uses the reviewed init/push capabilities, never installs or upgrades the CLI automatically, requires explicit network intent, and stops before creating authored subjects until the next CLI slice exposes subject-ID generation. |
|
The next stacked CLI slice is #4. It adds the pure |
|
Server capability has advanced without changing this CLI transport contract. firstdraft/firstdraft#190 adds atomic import and replacement for Entities, |
Summary
firstdraft plan pushfor argument-free, whole-document Foundation Plan submissionIf-None-Match: *on create and replay the complete server ETag withIf-Matchon replacementVerification
npm run checkon the pinned Node 24.18.0: typecheck, lint, format, 56 tests, package allowlist, and packed-package smoke testFIRSTDRAFT_API_URLalready present in the environmentfirstdraft/firstdraft#162: create at graph version 1, conditional replacement at graph version 2 with ETag rotation, and source-addressed 422 diagnostics with local state unchangedStack
firstdraft/firstdraft#162Current boundary
The server slice still accepts only the empty starter Plan shape. Authentication, pull/reconciliation, non-empty import, and publish remain later work. If a request is accepted but its response cannot be fully verified, local state stays unchanged and manual recovery may be necessary until a reconciliation endpoint exists.