Skip to content

SpecFlow 2.0: local spec-refinement plugin (skills + a small comparison CLI) - #59

Draft
awrobel-gd wants to merge 11 commits into
mainfrom
feat/specflow-2.0-refinement-plugin
Draft

SpecFlow 2.0: local spec-refinement plugin (skills + a small comparison CLI)#59
awrobel-gd wants to merge 11 commits into
mainfrom
feat/specflow-2.0-refinement-plugin

Conversation

@awrobel-gd

@awrobel-gd awrobel-gd commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

A local spec-refinement loop shipped as a second marketplace plugin. Independent subagents each read the spec under a different adversarial lens, and each one first draws its own cross-product of the decisions its angle implies and then has to account for every intersection. Where two readings answer the same question differently, or where a lens reaches a cell its axes forced into existence and reports that the spec cannot settle it, that is the finding. Every judgment about whether the spec is sound stays with the model, stated out loud rather than dressed up as a check.

Entrypoint

mcp_server/services/refine_compare.py — the module docstring states the boundary the whole design rests on, and the comment where novelty used to live states the one it refuses to cross. Wired into the CLI by refine_commands.register(subparsers) at mcp_server/cli.py:745 and driven by plugins/specflow2/skills/specflow-refine/SKILL.md.

Diagram

Single-screen visual — source in repo at docs/specflow-2.0/refinement-loop.html, which GitHub renders as source rather than as a page, so open the link or the local file. Testing walkthrough: docs/specflow-2.0/testing-the-refine-loop.md. Plan-vs-reality, including what was cut and what is still unproven: plans/specflow-2.0/status.md.

specs/ ──► /specflow-refine ⇄ /specflow-resolve ──► a spec with fewer holes
                                                    + the holes that remain

one round:

  refine new-round ──► grid: enumerate the decisions the spec implies, answer none
                            │
                            ▼
                       6 subagents, one lens each, no shared context
                            │  three passes each: name your own axes answering
                            │  nothing, fill every intersection, then re-read your
                            │  own matrix and account for every remaining cell
                            │  also fills the shared grid, writes reading.<lens>.json
                            ▼
                       coherence ──► can these answers all be true at once?
                            ▼
  refine round      ──► where readings disagree, with each lens's own phrasing
                        matrix cells a lens says the spec cannot answer, and why
                        matrix cells a lens enumerated and never came back to
                        grid cells nobody answered
                        grid cells every lens answered by guessing
                        blockers merged by id, with attribution
                        (no verdict, no score, no stop rule)
                            ▼
                       the skill judges: ask / assume / drop
                            ▼
  refine resolve    ──► decision recorded, spec file edited
                            │
                            └──► another round, or stop and say so as your own call

Details

Two plugins, and plugins/specflow/ is byte-identical to main. The refinement skills live in a new specflow2 plugin in the same marketplace. An earlier state of this branch de-symlinked specflow-analysis and specflow-planning and forked them, which meant two flows writing the same reserved filenames with content the 1.0 contract validator rejects; splitting the plugins fixed that at the root, and specflow plugin install installs specflow2 because it is the only one whose skills need this CLI present.

The interesting history is inside the branch. An early commit built roughly 2,850 lines of validators, a totality gate and a weighted ranking score, and a later commit deletes all of it. The gate was gameable and self-scoped, checking only the axes the agent under test had declared itself, so an artifact with every value marked guessed and sixteen undefined matrix cells passed clean. Reviewing the deleted code is optional, but the git log is the argument for the shape that remains.

There is deliberately no stop rule, which is the sharpest decision here. A round-to-round diff existed and reported what was new, and the skill read "nothing new" as convergence; that inference does not hold, because zero new blockers is equally consistent with this round's lenses simply finding less, and nothing holds lens effort constant between rounds. Turning it into a threshold would need the false-negative rate of a single round, which is unmeasured, so novelty, record_round, state.json and the new/repeat counts are gone and TestNoStopRule keeps them gone.

So completeness is reported as located incompleteness, never as a verdict. Disagreements say the spec did not determine this and name where, uncovered grid cells count gaps no reading even reached against an enumerated denominator, and agreed-but-guessed cells name consensus over silence — the failure a disagreement-only design structurally cannot see. Nothing sums them, because a found-something claim verifies one item at a time while a found-everything claim needs a denominator this design cannot have.

The per-lens matrix is what recovers building's one irreplaceable property, and plans/specflow-2.0/status.md §1a is the argument. A compiler never had to be told which decisions existed — it walked the graph and stopped at each one. Prose walks nothing and can omit a case without looking incomplete. So each lens names its own rows and columns before answering, then accounts for every intersection, and a cell it reaches and cannot answer is reported with its reason as a finding in its own right; that needs no second lens to corroborate, because "this intersection has no answer" is a fact about the spec rather than an opinion about it. Six lenses declare six cross-products from six angles, so a case invisible from one is often forced by another, and matrices are never merged across lenses because aligning one lens's axes with another's would take a similarity threshold.

Which is not the deleted gate, and the difference is the whole point. The gate scored an agent against axes the agent itself had declared, so declaring fewer axes passed it. The matrix declares axes too, but nothing passes, fails or blocks: unfilled cells are counted and printed, and a cell the lens skipped is reported as a hole in the reading rather than in the spec. The shared grid stays as well, narrower but comparable across lenses and the only thing here with a denominator. Both share the same bound: a decision no one's axes reach stays invisible, and no count reveals that.

Coherence contributes blockers and never votes. It runs after the lenses, reads all of their output, and asks which two answers cannot both be true, so compare() keeps it out of lenses and lens_count; counting it as a seventh independent reading would overstate the only evidence this design has.

One gap reads as one gap, but at most one. A disagreement at a location some lens already flagged attaches to that blocker rather than becoming a second entry, bounded at one per blocker so that N gaps under one coarse heading stay N items instead of collapsing into a single open blocker.

The filename decides a reading's lens. Concurrent subagents hand-write that field and a copy-pasted prompt yields two files claiming the same name, which would silently collapse them into one and take the disagreement between them with it; a mismatch is reported as a note rather than obeyed.

A malformed reading costs one lens, a malformed grid costs the round. Bad JSON or a wrong container shape in one reading is reported and the round continues with the rest, because six subagents write those concurrently; a broken grid refuses, because quietly treating it as absent would report a round with no coverage as a round with nothing to cover.

Models are the user's choice. 2.0 never calls a model, so the skills state the capability class each job needs and leave selection to the harness, and a harness that can route subagents to different providers can spread the lenses across model families.

Verification. 830 tests pass, 77 of them covering the refinement code, and the loop was driven end to end against the real CLI with hand-written artifacts. That walkthrough is written up as Level 1 of the testing doc so it is reproducible without a model, and running it shows the two channels agreeing independently: the concurrency lens reported seat hold × timer expiry as a cell the spec cannot answer, and the same underlying question surfaced separately as a cross-lens disagreement about who owns the timer.

What building still does that this does not. Two of its five properties are recovered at zero: contradiction by impossibility (a lens can argue two requirements conflict; nothing here fails to compile) and runtime discovery. The unbuilt specflow-contracts from plan §5 would recover the first cheaply by generating real SQL DDL or OpenAPI from the spec and running real validators; nothing short of building recovers the second, which is why step 7 tells the model to spike the one decision that is expensive, irreversible and split. status.md §1a scores all five.

Still unvalidated, and this is the thing to fix next. That disagreement between independent readings tracks real spec defects is the core hypothesis and has not been tested against a real spec, the agreed-guess and matrix signals inherit the same debt, and independence is unenforceable and currently unrecorded. Level 3 of the testing doc is the manual procedure, and until it has been run the honest claim is that the loop surfaces where independent readings diverged, not that it finds spec defects.

Scope. backend/, server.py and mcp_server/services/skills/ are untouched, the 1.0 flow is unchanged, and Steel Commandments I–XI still govern live backend code and come out with it rather than before. plans/specflow-2.0/specflow-plugin-plan.md now carries a banner marking sections 3 and 5 through 8 superseded by what actually shipped.

awrobel-gd and others added 5 commits August 3, 2026 16:09
Adds the local, plugin-only refinement loop: independent subagents simulate
building a spec under different adversarial lenses, deterministic scripts merge
and rank what they disagree about, and the user resolves only the decisions that
need a human. No backend, no third party, no network in the measurement path.

Four new published skills (specflow-refine, -simulate, -resolve, -contracts),
two reworked (-analysis emits machine-checkable dimensions; -planning now runs
after refinement rather than before), one repurposed (-report), and
specflow-mutate as internal QA in .claude/skills.

Orchestration is prose; every count, ranking and verdict is code. The oracle
library is stdlib-only so the plugin needs no pip install. Its core is the
totality gate: a prose blocker list is partial by nature, so lenses must fill a
total structure instead, and every admitted gap (inferred anchor,
undefined_in_spec outcome, unhandled failure mode) has to be paid for with a
matching blocker. Without that rule the escape hatches become a quiet way past
the hard cells.

Planning moved after refinement deliberately. A plan built on an ambiguous spec
encodes one arbitrary reading of it, which is how sync_plan_to_workspaces made
1.0's variance signal unmeasurable.

37 stdlib unittest tests; each maps to a defect the loop must keep catching.
Verified end-to-end against a two-lens fixture and from a simulated install
location.

backend/ and mcp_server/ are untouched. Two fixes outside the plugin:
- .gitignore: `lib/` (Python build output) was excluding the whole oracle
  library, so the plugin would have shipped referencing absent scripts.
- pyrightconfig.json: extraPaths only, no typeCheckingMode, so existing code
  keeps the defaults it was written against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PLAN.md and PLUGIN-SKILLS.md are earlier drafts superseded by
specflow-plugin-plan.md. They were swept into 2b5aa56 unintentionally;
only the final plan belongs in the PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plugin shipped its own Python library and invoked it by path, which
assumed an interpreter on the user's machine. python3 is a given on Linux,
a Command Line Tools shim on macOS, and absent on Windows — and the failure
mode was exit 127, indistinguishable from "your artifact did not pass".

Split the two halves along how they are distributed instead:

  plugins/specflow  prose only — skills and lenses, via the marketplace
  gd-specflow       all code — oracles and commands, via PyPI

Skills now call `specflow refine ...`. The interpreter question disappears:
`specflow plugin install --target claude` drives Claude Code's own installer
against the published marketplace, so the CLI necessarily exists before the
plugin that calls it does.

- services/oracles/      the library, moved unchanged (stdlib-only)
- services/refine_commands.py  the command group; oracles stay argparse-free
- `specflow refine schema NAME` prints an artifact contract, since a skill
  can no longer name a path to one inside the wheel

cli.py: every subparser now carries its own handler via set_defaults, and
_COMMAND_MAP is gone. It bound function objects at import time, which
defeated patching them — test_init_skips_localhost_guard was running a real
docker build instead of its mock. _LOCAL_COMMANDS replaces the `init`
special case: refine and plugin skip backend config and the localhost guard
because none of it means anything to them, not as an exception.

Oracle tests move into the MCP suite, so `make unit-tests` and CI cover
them. Verified from a built wheel in a clean venv. 790 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eping

The oracle library used code for judgment and left bookkeeping to prose. This
inverts it: code compares readings and remembers decisions, the skill judges.

A schema validator cannot tell you whether an architecture or a product design
is right, and the deleted modules were shaped as though it could.

Removed:
- totality.py — the "forcing function". Gameable and self-scoped: one blocker
  per spec file licensed unlimited escape hatches in that file (file-level
  fallback in is_raised, set membership, no per-gap accounting), and the matrix
  check only verified the axes the same agent declared. An artifact with every
  anchor inferred, 16 undefined matrix cells and 5 unhandled failure modes
  passed clean and surfaced zero questions. Also rejected legitimate values
  (authorization "none", field type "any") as evasions.
- rank.py — score = weight * (1 + concordance) * irreversibility, with
  uncalibrated constants deciding what interrupts a user. Now the skill's call,
  made out loud where the user can disagree.
- jsonschema_mini.py — justified by "ships in a plugin, must be stdlib-only".
  That died when the library moved into the gd-specflow wheel, which already
  depends on pydantic and ships jsonschema transitively.
- contracts.py — checked a data model nobody will build.
- saturation.py, tree.py, mutate.py, the three JSON schemas, specflow-contracts
  and specflow-mutate skills, and the superseded plan doc.

Kept, as services/refine_compare.py + refine_artifacts.py:
- disagreement between independent readings, grouped by question content so two
  phrasings collide — the actual sampling signal
- blocker merge by id with lens attribution
- novelty across rounds, and resolved-decision memory so rounds stop re-asking

A disagreement at a location some lens already flagged now attaches to that
blocker instead of becoming a second list item, so one gap reads as one gap.

CLI drops from nine commands to four (new-round, round, resolve, status) and no
longer exits non-zero on a judgment call. Reading format is decisions + blockers
instead of the 188-line interpretation schema; lens files and skills updated to
match, including the removal of every "the validator confirmed" framing.

Also fixes a real packaging bug found on the way: a stale setuptools build/ dir
meant the wheel still shipped every deleted module.

777 tests pass; 24 cover the survivors. Verified end-to-end over two rounds:
disagreements located, resolutions dropping out, novelty tracked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@awrobel-gd awrobel-gd changed the title SpecFlow 2.0: local spec-refinement plugin (skills + oracle library) SpecFlow 2.0: local spec-refinement plugin (skills + a small comparison CLI) Aug 4, 2026
Adds a comprehensive plan for the SpecFlow 2.0 plugin, detailing the new user flow, the functionality of the `/specflow-refine` skill, and the roles of various lenses in the refinement process. This document supersedes earlier drafts and outlines the architecture, user interactions, and the rationale behind the design choices. Additionally, updates the README to clarify model usage and the interaction between skills and subagents.

This commit lays the groundwork for a more structured and reliable approach to spec refinement, emphasizing the importance of resolving ambiguities before planning.
Introduces functionality to manage grid and coherence files within the refinement process. The `Layout` class now includes methods for accessing grid and coherence paths, while new functions handle loading grid data and checking coherence. The `compare` function has been updated to incorporate grid coverage, reporting on unanswered cells and agreements reached by guessing. Additionally, tests have been added to ensure proper coverage and functionality of these new features, enhancing the overall refinement process.
This commit refines the project root resolution logic by moving it to the `local_env` module, allowing consistent handling across command groups. The `_configure_env` function in `cli.py` has been updated to reflect these changes. Additionally, the CLI output messages have been improved for clarity, particularly in the installation feedback.

New tests have been added to ensure the correct behavior of the `refine` command group, including handling of usage errors and root path resolution. This enhances the overall user experience and reliability of the refinement process.
… refinement

This commit adds the `specflow2` plugin to the marketplace, which focuses on refining specifications through independent subagents that analyze the spec under various adversarial lenses. The new plugin operates entirely locally, ensuring no data leaves the user's machine.

Key changes include:
- Addition of the `specflow2` plugin with its own skills: `specflow-refine` and `specflow-resolve`.
- Updated marketplace configuration to include the new plugin alongside the existing `specflow` plugin.
- Enhanced CLI commands to facilitate the installation and usage of the `specflow2` plugin.
- Comprehensive documentation for the new plugin, detailing its functionality and usage.

This update aims to improve the user experience in specification refinement by providing a dedicated toolset for local analysis and decision-making.
…atrices

This commit introduces a new HTML document detailing the refinement loop process in SpecFlow 2.0, explaining how specification gaps are identified and addressed. The document includes a comprehensive style guide for improved readability and user experience.

Additionally, the  file has been updated to include new matrix structures for reading outputs, enhancing the clarity of how different lenses interact with the spec. The changes also include improved handling of unanswerable cells and the reporting of matrix counts in the CLI output.

These updates aim to provide users with better insights into the refinement process and improve the overall functionality of the SpecFlow toolset.
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