Skip to content

Pin the product Compile CLI contract - #18

Merged
raghubetina merged 2 commits into
mainfrom
codex/stream1-cli-contract
Aug 4, 2026
Merged

Pin the product Compile CLI contract#18
raghubetina merged 2 commits into
mainfrom
codex/stream1-cli-contract

Conversation

@raghubetina

@raghubetina raghubetina commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace the obsolete subject-id/Publish/local-Compile contract in the exact source-and-packed CLI checker with the repaired product Compile and retained Compilation surface
  • split that checker into focused modules while preserving adversarial artifact, transport, state, status, Publication, provenance, materialization, and leak coverage
  • bind standalone push/status diagnostics to the accepted graph generation and distinguish exact Head bytes from canonical Compiler-input bytes
  • update behavioral evals for malformed JSON, schema and semantic diagnostics, recurring issues, stale generations, ambiguous mutation phases, terminal Publication outcomes, retained download, and the Movie Catalog product journey
  • pin the controlled joined service evidence from firstdraft/firstdraft#305 with its strict fake-GitHub boundary and explicit nonclaims

This PR renews the deterministic checker and eval contracts, not the packaged SKILL.md. Skills #19 performs the shipped workflow rewrite and must land before any release.

Integration

Skills #17 and the complete CLI stack have landed. The exact CLI contract revision f55edffc… remains an ancestor of CLI main, so the original reviewed contract and digest pin remain unchanged and both hosted ancestry gates now pass.

Verification

  • PATH="/Users/sandbox2/.asdf/shims:$PATH" node script/check-cli-contract.mjs /Users/sandbox2/code/firstdraft/cli-skills-contract-f55-20260804
  • PATH="/Users/sandbox2/.asdf/shims:$PATH" sh script/check (40 tests)
  • hosted Node 22.0.0 and Node 24.18.0 checks passed
  • git diff --check

The 40-test count consolidates two prior Compilation/Publication guidance cases into one product-Compile and retained-Compilation contract. The joined harness remains controlled local evidence: no live GitHub or staging mutation, generated-app execution, deployment, or fresh-agent proof.

Replace the monolithic successor check with focused modules that exercise the zero-flag journey, retained artifact safety, and the packed executable.\n\nBind the eval corpus and evidence summary to generation-aware analysis, Publication recovery, corrected digest provenance, and the controlled local service rehearsal so contract drift fails CI.
@raghubetina

Copy link
Copy Markdown
Contributor Author

Review

The checker work is good and I verified it against the pinned CLI. One finding: the packaged Skill still instructs three commands the CLI stack removes, and the summary's first bullet reads as though that were fixed here.

Verification

Check Result
sh script/check 40 tests / 40 pass
node script/check-cli-contract.mjs <cli#19 worktree> exit 0
same checker against a non-CLI directory throws

The last row is the sanity check that matters: a checker that exits 0 silently could be doing nothing, and this one fails when pointed at the wrong tree. So "the exact checker already passed locally against the pinned worktree" reproduces.

The packaged Skill still names removed commands

This branch does not touch skills/. git diff --name-only refs/fdwatch/pr17s...refs/fdwatch/pr18s -- skills/ is empty, and the reference counts are identical to main.

What the shipped SKILL.md still tells an agent to do:

Line Instruction Status in the CLI stack
126 "Use firstdraft plan subject-id for each genuinely new subject" removed by cli#16
230 firstdraft plan publish removed by cli#18
348 firstdraft plan compile --output <approved-absent-path> mode removed by cli#18
233 "Do not pass flags, run plan compile first" flag guidance now inverted
411 "published only after plan publish returns its validated private URL" command gone

I confirmed the first is genuinely unavailable by running the new CLI:

$ firstdraft plan subject-id
Unknown command.
Run 'firstdraft plan --help' for usage.          rc=2

So once the CLI stack releases, an agent following the shipped instructions fails at three separate points, each with a bare "Unknown command" and no hint about what to use instead.

This matters more than a stale doc because of what skills#16 built. The Skill is installable as a marketplace plugin, so this is not internal notes; it is text that reaches other people's agents and gets executed. Instructions that name a command are part of the interface, and this half of the interface has not moved.

The wording invites misreading

replace the obsolete subject-id/Publish/local-Compile contract with the repaired product Compile and retained Compilation surface

Read cold, that says the obsolete references were replaced. What was replaced is script/check-cli-contract.mjs and the new script/cli-contract/ modules, meaning the checker's expectations of the CLI. The instructions the checker exists to protect are unchanged.

skills#17 says the renewal is deliberately later work: "This branch deliberately does not alter the packaged Skill yet ... before the workflow and evidence stack is renewed." So this is sequencing rather than an oversight, and I am not asking for the Skill rewrite here.

What is worth adding is the release constraint, stated plainly: the packaged Skill must not reach users between the CLI stack landing and the instructions being renewed. Right now that ordering lives only in the reader's head, and the two repositories release independently. A line in this PR's description, or better an entry in the README's boundary section, would make it something a person can check rather than remember.

The blocked gate is handled honestly

Hosted CI intentionally requires the pinned CLI commit to be an ancestor of CLI main, so that one ancestry gate is expected to remain blocked until the CLI stack lands.

Good. An ancestry gate is the right mechanism, since it prevents pinning a commit that could be force-pushed away or that never merges, and saying up front that it will be red avoids the alternative failure mode where somebody sees red CI and assumes the branch is broken.

Worth confirming the gate distinguishes "not yet an ancestor" from "not a valid commit," since those need different responses and both would fail an ancestry check.

The module split reads well

Breaking one checker into artifact-safety, compilations, local-commands, packed-executable, plan-journey, plan-status, publication-validation, plus config, fixtures, and harness, is a sensible decomposition. The names map to the surfaces being checked rather than to implementation layers, so a failure points at what it means.

The description's claim that adversarial artifact, transport, state, status, Publication, provenance, materialization, and leak coverage is preserved through the split is the sort of thing a diff cannot easily show. The sh script/check count going from 41 on skills#17 to 40 here is worth a sentence: probably a consolidated or relocated test, but a reader comparing the two numbers will wonder which one went.

Distinguishing the two digests

distinguish exact Head bytes from canonical Compiler-input bytes

This is the same conflation cli#19 fixed, and pinning it in the contract checker is the right place. It means the Skill's contract cannot silently drift back to requiring the two digests be equal, which is the bug the Movie Catalog journey surfaced.

@raghubetina

Copy link
Copy Markdown
Contributor Author

When your documentation is an API

Here is a situation that did not exist a few years ago, and it changes how you should think about docs.

This repository ships a Skill: a folder of Markdown that gets installed into someone's Claude Code and tells an agent how to use the First Draft CLI. Instructions like:

Use `firstdraft plan subject-id` for each genuinely new subject and write each
locally minted ID into the complete Plan.

Meanwhile, in a different repository, the CLI removed plan subject-id. I checked:

$ firstdraft plan subject-id
Unknown command.
Run 'firstdraft plan --help' for usage.

So the shipped instructions name a command that no longer exists. An agent following them runs it, gets "Unknown command," and has to work out what to do with that.

Why this is different from a stale README

You have shipped stale documentation before. Everybody has. Usually the consequence is that a human reads it, tries the thing, sees it fail, mutters, and finds the right answer another way. Annoying, low stakes, self-correcting.

An agent reading instructions is different in three ways.

It follows them literally. A human sees plan subject-id in a doc from six months ago and thinks "that's probably been renamed." An agent runs it.

It has no other source. The human opens the CLI's --help, or asks a colleague, or reads the source. An agent has the Skill it was given.

The failure is not obviously a documentation problem. "Unknown command" looks like a broken installation or a missing dependency. The agent may retry, or try to install something, or report a confusing failure to the user. It will not usually conclude "my instructions are out of date."

So instructions consumed by an agent behave less like documentation and more like a client library. And you already know the rule for client libraries: you cannot remove a command without coordinating with the things that call it.

The version-skew problem, again

If you have shipped an API, this shape is familiar:

  • The server removes an endpoint.
  • Clients that call it break.
  • So you deprecate, wait, then remove.

Same problem here, with prose in the client position:

Skill says CLI has
today plan subject-id plan subject-id
after the CLI stack lands plan subject-id nothing

The window between those rows is the problem, and it opens the moment the CLI is released.

Note what makes it worse than an ordinary API break: the two live in separate repositories with separate release cycles. Nothing mechanical connects them. In one repository somebody deletes a command, tests pass, ship. In the other, a Markdown file still names it, tests pass, ship. Both repositories are individually green and the combination is broken.

What this PR does about it, and what it does not

The PR updates the contract checker: a script that runs the real CLI and asserts it behaves as the Skill expects. That is genuinely valuable, because it is the mechanical link the two repositories were missing.

What it does not do is update the instructions. The skills/ directory is untouched, and the stale command names are still there at the same counts as before.

That is a deliberate sequencing choice; a sibling PR says the Skill renewal comes later. Worth understanding as a reader, though, because the summary's first bullet reads as if the references had been replaced, and what was replaced is the checker's expectations of the CLI.

The technique worth stealing

The interesting idea here is testing your documentation by executing it.

Most of us test code and proofread docs. When your docs contain commands, you can do better:

[
  [["--version"], /\A0\.1\.0-alpha\.2\n\z/],
  [%w[plan --help], /compile\s+Compile and publish the current Foundation Plan/],
  [%w[compilation status --help], /compilation status <compilation-id> \[--wait\]/],
]

Run each command, assert the output matches. Now a removed command fails a check rather than a user.

You can approximate this cheaply in a Rails project. Something as simple as extracting every fenced shell command from your README and asserting each one at least exists:

test "every documented rake task exists" do
  documented = File.read("README.md").scan(/^(?:bundle exec )?rake ([\w:]+)/).flatten
  available = `rake -T`.scan(/^rake ([\w:]+)/).flatten
  (documented - available).tap do |missing|
    assert_empty missing, "README names tasks that do not exist: #{missing.join(", ")}"
  end
end

Crude, and it would have caught this. The general principle: anything in your docs that a reader will execute should be executed by a test.

Ancestry as a coupling mechanism

One more thing in this PR worth knowing about, because it is a neat trick.

The Skill pins the CLI commit it was verified against. A digest of a commit hash alone would not be enough, because a commit can exist on a branch, get force-pushed away, and never reach main. Then your pin refers to code nobody can obtain.

So CI checks that the pinned commit is an ancestor of the CLI's main branch. Not just that it exists; that it actually landed.

git merge-base --is-ancestor <pinned> origin/main

And the PR says plainly that this gate is expected to fail until the CLI stack merges. That honesty matters: an expected-red gate that nobody documented becomes a red gate everybody ignores, and then it stops being a gate at all.

Worth remembering the distinction generally. "This commit exists" is weak. "This commit is reachable from main" is a real guarantee, and it is one line of git.

The takeaway

Ask who reads your documentation. If the answer includes a program, the documentation is an interface, and interfaces have compatibility rules:

  • removing something a document names is a breaking change
  • the document and the thing it describes need a mechanical link, not good intentions
  • commands in docs should be executed by tests
  • if they live in separate repositories, write down the release order, because nothing else will

@raghubetina

Copy link
Copy Markdown
Contributor Author

Follow-on #19 now records the fresh-model evidence that this PR intentionally did not establish. The no-fresh-agent-proof boundary in this PR remains historically correct for its own diff; #19 adds separately pinned Home Inventory opening-interview and Movie Catalog diagnostic-to-Compile evidence after this contract. This PR remains the prerequisite and is not obsolete.

@raghubetina

Copy link
Copy Markdown
Contributor Author

Follow-up from the later stack: #19 now rewrites the packaged SKILL.md, removes the stale plan subject-id, public plan publish, and plan compile --output guidance, and must land before any release. I clarified this PR description to scope its own change to the deterministic checker and eval contracts.

The 41→40 count is also now explained: two prior Compilation/Publication guidance cases were consolidated into one product-Compile and retained-Compilation contract. The ancestry check already distinguishes a valid nonancestor from an invalid revision, so no code change is needed there.

Preserve the reviewed external CLI contract while inheriting the landed interview protocol and evaluation foundation.
@raghubetina
raghubetina changed the base branch from codex/stream1-interview-foundation to main August 4, 2026 22:05
@raghubetina
raghubetina merged commit 5330e74 into main Aug 4, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant