Ran the humanizer rules over 79 prose files in the active specs (brief, design, requirements, research and narrative docs). Removed em dashes, significance inflation, forced rule-of-three, bold-label list items and filler. Skipped tasks.md checklists and the retired specs (harnessx-*, openrouter-sast-harness, unseen-repo-evaluation). Every number, path, command, hash and identifier was preserved: checked each file against HEAD for drift in numbers, inline code and code fences, zero drift across all 79 files. Co-Authored-By: Claude Opus 4.8 <[email protected]>
10 KiB
Requirements Document
Introduction
The model-driven pipeline (classify sinks, triage, verify, judge, recheck, score) is a set of scripts run from
one long conversation, each with its own resume, budget and pass logic. Measured 2026-09-29: 90% of development
tokens are tool I/O around those scripts, and verification re-derives the same per-repository facts in every
hunt and every pass. This feature restructures the pipeline as a Kubernetes-style service plane at the model
level, in the shape of google/ax: declarative Task, Workspace and Model manifests, a reconciler that
executes them in isolation with resume, and a runner contract. The plane runs on ax itself: a kind cluster
with Agent Substrate and the ax controller on this host (maintainer decision 2026-09-29: "switch to ax
completely now"). What ax does not yet carry -- task dependencies, artifacts, token budgets (on ax's roadmap),
a DeepSeek provider -- is carried by our thin Run layer and dissolves into ax as ax gains it.
Boundary Context
- In scope: the manifest kinds and a
Runkind for dependencies, artifacts and budgets; bring-up of ax on this host (kind + Agent Substrate + ax controller) with a doctor command; an ax-backed reconciler that submits Tasks to ax (one engine-bearing task at a time) and is the only executor; the runner contract and a runner image; the first two tasks,repo-factsandverify, with the existing scripts as reference behaviour; measurement on the 46-candidate validation set. - Out of scope: a remote or multi-node cluster; contributing providers or budgets to ax upstream (a
follow-on once the increment is measured); changing what the model is asked (prompts, families, pass rules)
except as required to consume
repo-facts; the judge stage, the config-family question and population v3 (they become tasks later, under their own specs); any change toevaluate.py's protocol scoring. - Adjacent expectations: the independent-population protocols and records stay as they are;
finding_dumpand the engine path are untouched;vulnerabilities-over-plumbingapplies -- this is plumbing that must pay for itself in measured tokens and unchanged or better recall. - Target state (maintainer, 2026-09-29): this plane replaces HarnessX as the agentic plane; HarnessX is
removed by its own spec (
harnessx-removal) once Requirement 6's measurement shows value; theRunlayer shrinks as ax gains dependencies, budgets and artifacts. No new HarnessX feature is added meanwhile.
Requirements
Requirement 1: Manifests are ax's kinds plus a Run kind
User Story: As the maintainer, I want the pipeline declared in manifests that ax would accept, so that moving to a cluster later changes the reconciler and not the pipeline.
Acceptance Criteria
Task,WorkspaceandModelmanifests useapiVersion: ax.io/v1alpha1and only fields ax documents (spec.image,spec.command,spec.env,spec.resources,spec.workspaces;spec.git,spec.files;spec.provider,spec.model,spec.secretKey,spec.parameters).- A
Runmanifest (our kind) lists tasks withdependsOn, the artifacts each task produces and consumes, and a per-task budget (usd,calls); it contains nothing a task needs to execute. - A manifest that violates its schema is rejected before anything runs, with the field named.
- A DeepSeek model is expressed as a
Modelwith a provider adapter noted in the manifest; no ax field is repurposed to carry it.
Requirement 2: A task is isolated, idempotent and resumable
User Story: As the maintainer, I want a stopped or failed run to resume without repeating finished work and without ever recording partial work as a result.
Acceptance Criteria
- A task reads only its declared inputs and the bound workspace, and writes only its declared artifacts.
- Work is recorded per unit (file, candidate) as it completes; a restarted task skips completed units and repeats unverified ones.
- A task whose model calls fail on account or authentication errors stops at once with a distinct status; its artifacts are marked unfinished, never complete.
- A task's spend is metered from the provider's usage fields and stops the task at its budget; a task with no priced model reports "unpriced", not zero.
Requirement 3: An ax-backed reconciler executes a Run on this host's ax
User Story: As the maintainer, I want to run a whole pipeline with one command on the laptop, with every task executed by ax on the local cluster, so that the same manifests move to a bigger cluster unchanged.
Acceptance Criteria
- The reconciler submits each task to ax as a
Task(with its boundWorkspaces andModel) in dependency order, at most one engine-bearing task at a time, reads task outcome from ax's status plus the task's deliveredsummary.json, and passes artifacts between tasks by placing a producer's outputs where the consumer's manifest expects them. It never executes a task as a local subprocess; ax is the only executor. - Task status (
pending,running,suspended,done,failed,unfinished) is persisted and shown by one status command. - A suspended or interrupted run resumes from persisted status (Requirement 2.2) with the same command.
- The reconciler is under 500 lines and holds no pipeline logic: no prompts, no scoring, no model calls.
ousast plane doctorverifies the host's ax in one command: kind cluster reachable, Agent Substrate control API up, ax controller ready, runner image loaded; each check names what is missing and how to bring it up.- Bring-up on this host (kind + Agent Substrate + ax) is scripted, documented with its measured memory and CPU footprint, and smoke-tested with one trivial Task before any pipeline task runs.
Requirement 4: The runner contract is ax's
User Story: As the maintainer, I want each task entrypoint to run unchanged inside an ax task container later.
Acceptance Criteria
- An entrypoint receives its task and workspace manifests as YAML in
AX_TASK_YAMLandAX_WORKSPACES_YAMLand takes nothing else from the command line except what the manifest'scommandstates. - The runner image serves
/healthzand/readyzon port 80,/readyzreturning 503 until workspaces are materialised, as ax's runner contract requires; the image contains this package and is loaded into kind. - One fixture test per task type runs the entrypoint against a small workspace and checks its artifacts.
- Because ax has no artifact channel, the runner delivers the task's output directory (including
summary.jsonandunits.jsonl) to the reconciler's artifact endpoint named inOUSAST_ARTIFACT_URLbefore it reports completion; a delivery failure is reported asfailed, never as done. - The command never runs in Agent Substrate's golden-snapshot boot, which executes the same image with the same
task environment (observed on the live cluster, 2026-09-29): the runner prepares workspaces, reports ready and
waits; the reconciler starts the command with one request to the task's actor through Substrate's router
(
ate-target-actor: <atespace>/<task>), which never addresses the golden actor. A second start request for the same task is refused. - Provider credentials reach a task only in that start request, held in the runner's memory and passed to the command's environment; they never appear in a manifest, a rendered Task, an ax template or any file.
Requirement 5: repo-facts is the memory layer
User Story: As the maintainer, I want per-repository facts computed once and reused by every hunt and pass, so that verification stops paying tool turns for the same lookups.
Acceptance Criteria
repo-factsproduces, without model calls, per product file: the functions defined, and for each candidate function the call sites in other product files (path and line) -- from source text only, never from git.- The artifact is deterministic for a given workspace and is reused across tasks and passes unchanged.
verifyincludes each candidate's callers fromrepo-factsin its prompt and reports the number of tool turns per hunt.
Requirement 6: The first increment is measured before the rest moves
User Story: As the maintainer, I want proof that the restructuring saves tokens without losing recall before the remaining concerns are converted.
Acceptance Criteria
repo-facts+verifyrun on the 46-candidate validation set through the reconciler on ax, two passes, and report cost per candidate, tool turns per hunt and declared sites agreed.- Acceptance: declared sites agreed at least 16 of 20 (the current value) and cost per candidate below the current $0.022 for two passes; otherwise the increment is reported as not met and the design revisited.
- The development transcript measure (
benchmarks/dev/token_report.py) is run at the end of the increment and recorded next to the result. - After the first measurement (plane-increment-1.json: 14/20 agreed, 19/20 in either pass), agreement is decided 2-of-3 with a third pass run only on candidates passes a and b dispute; the gate of 6.2 is re-evaluated on that decision, and the result is recorded as plane-increment-2.json next to the first.
Requirement 7: Routing is a declared concern of the plane
User Story: As the maintainer, I want which model serves which concern to be a manifest decision with a budget, not a prompt trick inside one conversation, so that token spend is structured by component.
Acceptance Criteria
- Every
Taskin aRunbinds exactly oneModel; a Run may bind different Models to different tasks (a cheap model for reads and triage, a stronger one for verification and judgement), and the reconciler passes the bound Model's identity and prices to the task. - A task never chooses or switches its model at runtime; the Model it was bound to is recorded in its
summary.json. ousast plane status <run>prints a token attribution table: one row per task and Model binding with calls, input tokens, cache-hit tokens, output tokens and USD, a total row, and a per-Model subtotal; every number comes from the provider's usage fields recorded in the tasks'summary.json, never estimated. A--unitsflag breaks a task down per unit (file or candidate) fromunits.jsonl. The same table is written toattribution.jsonin the run directory so runs can be compared.- A task that needs a second model for a sub-step (a judge over a verifier's output) is a separate task with its own Model binding, never a second client inside one task.