Skip to content

feat: post-parse handoff to the build-schema agent skill (ADR-0010) - #185

Open
yuanwen-tian wants to merge 5 commits into
mainfrom
feat/schema-build-agent-handoff
Open

feat: post-parse handoff to the build-schema agent skill (ADR-0010)#185
yuanwen-tian wants to merge 5 commits into
mainfrom
feat/schema-build-agent-handoff

Conversation

@yuanwen-tian

@yuanwen-tian yuanwen-tian commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The problem

The ADE workflow is three steps, but ade only shipped two of them:

ade parse  →  [ you hand-write a JSON Schema ]  →  ade extract --schema
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
              entirely manual, unvalidated, and the hardest part

Authoring a schema by hand means guessing field names and descriptions, running an extraction, eyeballing the output, and repeating — with no measurement of whether it actually got better. Meanwhile the build-schema skill (the landingai-ade plugin, mirrored from schema-loop) already does this properly: it drives this very CLI through measured iteration — real extractions, reviewer-confirmed ground truth, per-field accuracy targets — and stops only when converged with numbers or blocked with evidence. It delivers the final schema at <workspace>/deliverable/schema.json.

The two just didn't know about each other. A user had to know the skill existed, install the plugin, and wire up the handoff themselves.

What this PR does

Closes the loop, in both directions, so the middle step is offered rather than assumed.

ade parse -d invoice.pdf
   │
   ├─ prints the summary + next: hints                    ← unchanged
   │
   └─ "What next?"                        ← NEW (interactive terminals only)
        1) Build an extraction schema with an AI agent
        2) Open the viewer — grounded pages and markdown
        3) Exit                                           ← the default (bare Enter)
         │
         ├─ 3) Exit → nothing changes, exit 0, and both commands are restated:
         │
         │        Whenever you want them:
         │          ade schema build 849a8c60  build a schema with an AI agent
         │          ade view 849a8c60 --open   open the viewer
         │
         ├─ 2) Viewer → ade view <id> --open   (grounded pages + markdown)
         │
         └─ 1) Build
              ├─ ade asks: what do you want to extract?   (free text)
              ├─ ade asks: existing schema? golden set?   (both optional, Enter skips)
              ├─ ade picks/asks which agent CLI, remembers the choice
              │
              ├─►  claude / codex / your agent launches HERE, in your terminal,
              │    seeded with the parse job item id, store dir, workspace, and intent.
              │    The skill negotiates accuracy targets WITH YOU, iterates with real
              │    extractions, and opens its review viewer for ground-truth verdicts.
              │    ade waits. You finish the session normally.
              │
              ├─ ade finds <workspace>/deliverable/schema.json and names it
              └─ "Run that extraction now?"  → ade extract <id> --schema <that file>
                                                (a free cache hit — the skill's last
                                                 iteration already ran it)

The three choices are the two things a human actually does with a fresh parse, plus the way out. Exit is last and the default, so bare Enter behaves exactly as before this PR — but it no longer teaches nothing: exiting (and aborting with Esc, and finding no agent installed) hands back both commands with a runnable short id, because by then the summary's next: line has scrolled behind a menu.

Everything above is also reachable directly as ade schema build JOB_ITEM_ID, so a parse from last week gets the same flow.

How you actually use it now

Before this PR:

ade parse -d invoice.pdf                 # → job item id
$EDITOR schema.json                      # ...guess, and hope
ade extract 849a8c60 --schema schema.json

After — the guided path. Just parse, and say yes:

ade parse -d invoice.pdf
# → "What next?"  → pick 1 (build a schema)
# → describe what you want in plain language
# → your agent session runs the schema loop, you review, it converges
# → "Run that extraction now?" → y

After — the deliberate path. Same thing, on any stored parse, with inputs supplied up front:

ade schema build 849a8c60 \
  --intent "invoice number, total, and the line-item table" \
  --existing-schema ./v1.json \
  --ground-truth ./golden.csv

Nothing changes if you already have a schema. ade extract --schema is untouched, and the offer never appears in scripts, pipes, --json, CI, or inside an agent session.

Command usage

Command / flag What it does
ade schema build JOB_ITEM_ID Build a schema for a completed parse item (accepts an unambiguous id prefix). Interactive terminals only.
--intent TEXT What to extract, in plain language. Prompted when omitted; required with --json.
--existing-schema PATH A schema to improve. The skill runs it unchanged first, as the regression floor.
--ground-truth PATH Golden ground truth seeding the skill's eval set (JSON keyed by document filename, or flat CSV). Without it the eval set is bootstrapped through review.
--workspace PATH Where the skill works. Default <ade-home>/schema-runs/<source>-<date>/; pointing at an existing one resumes that run.
--agent NAME Which agent CLI to launch (claude, codex, or a config-declared name). Defaults to the configured or only available one.
--json One stable payload on stdout. Never prompts, never auto-runs the extract. Still needs a terminal (the agent session inherits it).
ade parse … Unchanged, plus the three-way "What next?" offer on interactive terminals and ade schema build <ref> in the next: hints.
ade view <id> --open Unchanged — reachable as choice 2 of the offer.
ade extract <id> --schema <deliverable/schema.json> Unchanged — this is what the handoff runs for you at the end.

What it costs

ade schema build makes no API call itself, but the session it launches does — and per iteration, not once. The skill validates every schema version with a real ade extract run, so a converging loop costs roughly one extraction per iteration per document. The extraction offered at the end is normally a free cache hit, since the loop's last iteration already ran it.

That was previously only discoverable by reading the skill or the invoice. It is now stated on every surface a user or agent meets before spending: ade schema build --help, the published result note in ade help --json, the workflow topic, and the launch notice printed immediately before the agent session starts. ade history list itemises every run the session billed.

Configuration (~/.ade/config.json)

Key Effect
{"agent": {"default": "claude"}} Written automatically when you pick from the menu, so it asks once per machine.
{"agent": {"name": "aider", "command": ["aider", "--message"]}} Launch any agent CLI not in the built-in table. The seeded prompt is appended as the final argument.
{"schema_prompt": false} Silence the post-parse offer permanently. ade schema build still works.

Result payload (ade schema build --json)

{
  "status": "schema_built",
  "job_item_id": "849a8c60463ea7db",
  "agent": "claude",
  "workspace": "/Users/you/.ade/schema-runs/invoice-2026-08-10",
  "schema": "/Users/you/.ade/schema-runs/invoice-2026-08-10/deliverable/schema.json",
  "extract_hint": "ade extract 849a8c60 --schema /…/deliverable/schema.json",
  "extracted": false
}

extracted is always false: the extraction is offered interactively, never performed silently on your behalf.

Which agents work

Agent How it gets the skill
Claude Code (claude) Natively, via the landingai-ade plugin. Best experience.
Codex (codex) The seeded prompt tells it to use a local copy of the skill, else shallow-clone the marketplace repo and follow SKILL.md.
Anything else Same as Codex, declared via agent.command in config.json.

Detection is a PATH lookup. If both known agents are present you get a one-time menu; the choice is remembered. ade never fetches, pins, or updates the skill — that stays the agent's job, so this PR adds no network surface and no version coupling. No agent installed at all fails legibly with install instructions (and non-fatally after a parse — the parse already succeeded and its next: line still serves).

Why it can't surprise a script or an agent

The machine contract is byte-identical. The offer fires only when all of these hold:

  • stdin, stdout, and stderr are all real terminals
  • not --json, not --id-only
  • surface.py reports no agent host and not CI — an agent running ade parse must never meet a menu
  • schema_prompt is not false

Bare Enter selects Exit (last in the list), so the default costs zero keystrokes of the old behavior. Esc/Ctrl-C declines and the parse still exits 0. stdout stays payload-only; every prompt, notice, and menu rides stderr. A door that cannot open (no agent CLI installed) is non-fatal — the parse already succeeded and billed; a door that opens and then fails ends with that command's exit code.

Implementation notes for review

  • Testability seams. Subprocess launch and PATH lookup are injected ports (run_agent, which) on the existing Ports dataclass, so the offline suite drives the entire handoff without spawning anything.
  • The extract handoff is a re-exec of this CLI, not an in-process call — it inherits the terminal so progress renders normally. view._reexec_argv (frozen-binary aware) was hoisted to update.reexec_argv and is now shared with the viewer daemons.
  • One trusted seam. deliverable/schema.json is the only thing ade assumes about the skill's output — the single place to follow if that layout ever changes.
  • agents.py is pure (no typer, no I/O), so the seeded prompt is unit-testable as a plain function.

Testing

786 passed, 10 skipped (was 765/8 on main) — 21 new offline tests, 2 new integration tests.

Offline unit coverage (tests/test_schema_build.py, tests/test_parse.py): the seeded prompt's contents and per-agent skill-acquisition wording; agent detection, the remembered default, custom commands, and --agent precedence; workspace slugging, reuse/resume, and --workspace; URL-sourced parses; the optional-path flags and a mistyped path re-prompting; non-default environments; deliverable found/missing; the --json contract; extract accepted/declined and exit-code propagation; an unlaunchable agent; both menu fallbacks (raw-mode failure and TERM=dumb); agent_ambiguous; unknown/wrong-kind ids; and every gate on the post-parse offer (--json, --id-only, non-tty, CLAUDECODE=1, CI=1, schema_prompt: false, Esc, corrupt config). The offer's three choices each have coverage: the schema handoff, the viewer re-exec and its exit-code propagation, and Exit restating both commands with an id that still resolves. Two tests pin the credit disclosure — that it reaches stderr before the agent launches, and that both help surfaces carry it.

Integration coverage (tests/integration/test_production.py): the one seam the offline suite fakes. A real agent executable is placed on PATH and spawned attached to a real pty, the deliverable is written by that real process, and ade's own re-exec runs the delivered schema against production. The chained extraction deliberately reuses the direct extract test's schema and parse item, so it dedups to that item — the added coverage bills no extra credits. A second test covers the PATH-lookup failure. Both are POSIX-only (pty) and skip on Windows.

ruff check src tests and uvx ty check src are clean; docs/reference/help.json is regenerated.

A bug the new tests caught

run_build read config.json unguarded, so an unreadable or corrupt config crashed the handoff — even though offer_after_parse already guarded it. Both now go through one tolerant reader: the agent preference and the prompt opt-out are optional, so a bad config costs the preference, never the handoff or a completed parse.

Docs

  • docs/adr/0010-post-parse-agent-handoff.md — the decision and its constraints (gating, adapter table, config shape, subprocess-of-self extract, network-free posture), citing ADR-0002 for the menu pattern.
  • SKILL.md — tells agents to invoke the build-schema skill directly rather than shelling out to ade schema build.
  • README.md — a user-facing "Build a schema" section.
  • ade help workflow and ade help schema build both teach the new step.

Known gap

The codex "<prompt>" launch shape follows its documented CLI but is untested against a real Codex install — I had none available. Claude Code's path is verified end-to-end. Worth a smoke test from anyone who has Codex before we advertise it broadly.

🤖 Generated with Claude Code

The workflow was parse -> hand-author a schema -> extract. The middle
step is now offered: after a parse completes on a real terminal, ade
asks whether to build an extraction schema with an AI-agent session
running the build-schema skill (landingai-ade plugin), collects the
extraction intent plus optional baseline schema / ground truth, launches
the agent seeded with the parsed job item, and on return names the
delivered deliverable/schema.json and offers to run the extract.

Also exposed standalone as `ade schema build JOB_ITEM_ID` so any stored
parse can reach the same flow.

Agents are adapters, not one agent: claude and codex are detected on
PATH, a menu choice is remembered as agent.default in config.json, and a
custom agent.command entry launches anything else. ade stays
network-free -- non-native agents are told to use a local copy of the
skill or clone the marketplace repo themselves; ade never manages skill
versions. A missing agent CLI fails legibly with install instructions
(non-fatally on the offer path, where the parse already succeeded).

Gating keeps the machine contract byte-identical: the offer never fires
under --json/--id-only, on any non-tty stream, inside an agent host or
CI, or with schema_prompt: false. stdout stays payload-only; every
prompt rides stderr.

Subprocess launch and PATH lookup are injected ports (run_agent, which),
so the suite drives the whole handoff offline -- no process is ever
spawned in tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an agent-driven schema-authoring handoff between parse and extract.

Changes:

  • Adds ade schema build and the gated post-parse offer.
  • Adds agent detection, configuration, prompting, and subprocess ports.
  • Documents and tests the workflow and shared CLI re-execution.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/ade_cli/agents.py Defines agent adapters and prompts.
src/ade_cli/schema_build.py Implements schema-build orchestration.
src/ade_cli/parse.py Adds the post-parse offer.
src/ade_cli/main.py Registers the schema command.
src/ade_cli/ports.py Adds PATH and subprocess ports.
src/ade_cli/config.py Adds atomic configuration persistence.
src/ade_cli/update.py Exposes shared re-exec arguments.
src/ade_cli/view.py Uses the shared re-exec helper.
src/ade_cli/help.py Documents command and output shape.
tests/test_schema_build.py Tests schema-build behavior.
tests/test_parse.py Tests post-parse gating.
tests/conftest.py Adds subprocess and PATH fakes.
tests/test_update.py Tests shared re-execution.
tests/test_telemetry.py Adds schema-build telemetry coverage.
README.md Documents schema building.
SKILL.md Guides agents toward direct skill use.
docs/reference/help.json Regenerates command reference data.
docs/adr/0010-post-parse-agent-handoff.md Records the handoff design.
Suppressed comments (4)

src/ade_cli/schema_build.py:267

  • In --json mode this attached child still inherits stdout (ports.py:56-60), so output from Claude, Codex, or a custom agent is emitted before the JSON object. That breaks the repository's one-object stdout contract; the recorder used by this test suite cannot expose it. Route the agent child's stdout to stderr in JSON mode while keeping it attached to the terminal, or reject --json for this flow.
        exit_code = ports.run_agent([*agent.command, prompt], cwd=workspace)

src/ade_cli/schema_build.py:296

  • extract_hint is documented as ready to run, but the schema path is interpolated without shell quoting. An ADE home or explicit workspace containing spaces makes the emitted command split the path into multiple arguments. Build the hint with platform-appropriate command-line quoting.
    extract_hint = f"ade extract {ref} --schema {schema_path}"

src/ade_cli/schema_build.py:248

  • A relative --workspace is also passed as the child's cwd and embedded unchanged in the seeded prompt. From inside that child, a workspace such as runs/demo then resolves to runs/demo/runs/demo, so the skill can write outside the directory that this code later checks. Normalize the workspace to an absolute path before creating it and building the prompt.
    workspace.mkdir(parents=True, exist_ok=True)

src/ade_cli/schema_build.py:259

  • CLI-supplied and interactively entered schema/ground-truth paths may be relative to the caller's directory, but the agent runs with cwd=workspace. Passing those paths unchanged makes the agent resolve them under the workspace and miss valid input files. Resolve both paths before placing them in the prompt.
        existing_schema=existing_schema,
        ground_truth=ground_truth,

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/ade_cli/schema_build.py Outdated
yuanwen-tian and others added 4 commits August 10, 2026 14:16
…s seam

Unit tests (offline) for the paths the first pass left uncovered:
workspace slugging and reuse, --workspace, URL-sourced parses, the
--existing-schema/--ground-truth flags, a mistyped optional path,
non-default environments in the seeded prompt, an unlaunchable agent,
extract exit-code propagation, both menu fallbacks (raw-mode failure and
TERM=dumb), agent_ambiguous under --json, and unknown ids.

Integration tests drive the one seam the offline suite fakes: a real
agent executable found on PATH and spawned attached to a real pty, then
ade's own re-exec running the delivered schema against production. The
chained extraction reuses the direct extract test's schema and parse
item, so it dedups to that item and the added coverage bills nothing.
POSIX-only (pty); skipped on Windows.

Fixes a bug these found: run_build read config.json unguarded, so an
unreadable or corrupt file crashed the handoff -- offer_after_parse
already guarded it. Both now go through one tolerant reader: the agent
preference and the prompt opt-out are optional, so a bad config costs
the preference, never the handoff or a completed parse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Building a schema was the only door the offer opened, but the other
thing a human does with a fresh parse is look at it, and a bare "Exit"
taught nothing. The menu now offers three choices: build a schema, open
the viewer, or exit.

Exit stays last and default, so bare Enter still costs no keystroke of
the pre-offer behavior -- but it is no longer a dead end. Exiting (and
aborting with Esc, and finding no agent installed) restates both
commands with a runnable short id, because by then the summary's next:
line has scrolled behind a menu and the person who chose Exit is exactly
the one who needs them back.

The viewer choice rides the same subprocess-of-self re-exec as the
extract handoff, so it inherits the terminal and the run ends as
`ade view` ended.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`schema build` looks free -- it makes no API call itself -- but the
session it launches does: the skill proves every schema version with a
real extract run, so a converging loop costs roughly one extraction per
iteration per document, not one at the end. That was only discoverable
by reading the skill or the invoice.

Now stated on every surface a user or agent meets before spending:
`ade schema build --help`, the published result note in `ade help
--json`, the `workflow` topic, and -- the moment it actually matters --
the launch notice printed just before the agent session starts. The
final offered extraction is called out as a normally-free cache hit so
the disclosure is not read as double-billing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…alone

Copilot review catch (#185): the default workspace was
<slug>-<date>, so two different documents both named invoice.pdf on the
same day shared one workspace -- and the second run could find the first
one's deliverable/schema.json and report schema_built for a schema its
agent never produced.

The item id (a content hash of source x content x params) now keys the
default: <slug>-<item-id[:8]>-<date>. Different documents can never
collide however they are named; the same document re-parsed maps to the
same id, so resuming an interrupted run still works. An explicit
--workspace remains the caller's own choice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lai-bianca added a commit that referenced this pull request Aug 11, 2026
> **Do not merge until
[landing-ai/docs#377](landing-ai/docs#377) is
live.** Eight links in this README point at `docs.landing.ai/cli/...`,
which 404 until that PR deploys.

## Why

The README was a 2,767-word manual with a section per verb, written when
it was the only documentation the CLI had. `docs.landing.ai` now has an
eight-page CLI tab, and its reference page regenerates from this repo's
`docs/reference/help.json` on every release.

That left two copies of the same material, one of which regenerates and
one of which does not. The hand-maintained copy had already drifted: the
version-pin example said `ADE_CLI_VERSION=0.2.1` against a shipped
1.0.5.

## What this does

Trims the README to what this repo owns and a GitHub visitor needs, and
links the docs for depth.

**Kept**

- What `ade` does, in one paragraph.
- **Install, in full.** The installer scripts live in this repo, so this
stays the canonical home: platform installers, where the app lands,
`ADE_HOME`, `ADE_CLI_VERSION`, `ADE_CLI_INSTALL_DIR`, the never-`rm -rf
~/.ade` warning, `ade update`, `ADE_NO_UPDATE_CHECK`.
- A three-command first run.
- One table, one line per command, no prose.
- `For agents`: `ade help --json` and the `SKILL.md` pointer.
- The `agentic-doc` lineage, contributing, license.

**Moved to the docs** (auth details, job items and the guarantee, parse
and extract prose, view/crop/find prose, `--json` and piping, help
topics). Each now has a link to the page that owns it.

**Two defects fixed**

- Two sections were both titled `## History`, one for the command and
one for the repo lineage, so their anchors collided and the rendered nav
showed "History" twice. The second is now `## Project history`.
- The top documentation link pointed at the docs root rather than the
CLI documentation.

2,767 words to 821. 335 lines to 133.

## Verification

- All 18 commands in `docs/reference/help.json` appear in the command
table, and no others.
- Every link returns 200 except the eight `docs.landing.ai/cli/` paths
noted above. I confirmed each of those eight matches a real page
filename in the docs PR, so they resolve on deploy.
- `ADE_CLI_VERSION` example now matches the shipped version.
- No duplicate headings.
- `LICENSE`, `SKILL.md`, and `CONTRIBUTING.md` all exist at the paths
linked.

## Notes for review

- **This conflicts with #185**, which adds a 19-line `## Build a schema`
section to the same file. Whichever merges second resolves it. In the
new structure that section belongs after `First run`.
- I deliberately did not document `ade schema build` here. It is not on
`main`, and the skill it links in #185 lives in
`landing-ai/claude-skills`, which is private, so a public README linking
it would 404 for every reader.
- This does not depend on #166. That PR adds
`docs/agents/writing-style.md` but does not touch `README.md`. I wrote
this against those standards anyway.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

2 participants