From 0bbf4be380d2de0e49249e723acb9dbbfd4f12e4 Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:44:09 +0200 Subject: [PATCH 01/10] chore(plan): protocols at aep 0.64.0; close shared-docs-toolchain; new stories - project.yaml `protocols:` pins the aep 0.64.0 release commit 58433bd (was b857bbe), as the aep maintainers asked; validate: valid. - story:shared-docs-toolchain implemented: the bundle workflow pins docs-system 339b4b8, which contains the 1d4c026 the story named (merge-base --is-ancestor); evidence the bundle run 36406238132 on 44bb0ee. - dependency-blocker:governed-dogfood-run blocks story:drive-entry-skill: the aep story it waits on is draft on beyond10x/aep main (b11db55). - New active stories with scope: story:aep-tutorial (a public AEP tutorial on the ESS tutorial's library) and story:docs-manifest-links-resolve (the check that would have stopped the four-day publication outage). --- .../20260928T095338Z-6e4f2e2a0e6b.json | 13 ++++++ .../governed-dogfood-run.md | 15 +++++++ .engineering/planning/story/aep-tutorial.md | 41 +++++++++++++++++++ .../story/docs-manifest-links-resolve.md | 32 +++++++++++++++ .../planning/story/shared-docs-toolchain.md | 16 +++++++- .engineering/project.yaml | 2 +- 6 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 .engineering/evidence/story/shared-docs-toolchain/20260928T095338Z-6e4f2e2a0e6b.json create mode 100644 .engineering/planning/dependency-blocker/governed-dogfood-run.md create mode 100644 .engineering/planning/story/aep-tutorial.md create mode 100644 .engineering/planning/story/docs-manifest-links-resolve.md diff --git a/.engineering/evidence/story/shared-docs-toolchain/20260928T095338Z-6e4f2e2a0e6b.json b/.engineering/evidence/story/shared-docs-toolchain/20260928T095338Z-6e4f2e2a0e6b.json new file mode 100644 index 0000000..fe4a048 --- /dev/null +++ b/.engineering/evidence/story/shared-docs-toolchain/20260928T095338Z-6e4f2e2a0e6b.json @@ -0,0 +1,13 @@ +{ + "at": "2026-09-28T09:53:38Z", + "actor": "human:timo", + "artifact": "story:shared-docs-toolchain", + "kind": "story", + "revision": 3, + "change": { + "change": "evidence", + "kind": "test_result", + "source": "b10x-docs-bundle on 44bb0ee with docs-system 339b4b8 (contains 1d4c026)", + "reference": "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/beyond10x/agentplugins/actions/runs/36406238132" + } +} diff --git a/.engineering/planning/dependency-blocker/governed-dogfood-run.md b/.engineering/planning/dependency-blocker/governed-dogfood-run.md new file mode 100644 index 0000000..65c7f01 --- /dev/null +++ b/.engineering/planning/dependency-blocker/governed-dogfood-run.md @@ -0,0 +1,15 @@ +--- +format: aep.planning-md/3 +id: dependency-blocker:governed-dogfood-run +kind: dependency-blocker +status: open +title: aep story:governed-dogfood-run has not landed; the driven walk does not reach complete +relations: +- blocks: story:drive-entry-skill +revision: 1 +--- +# Blocker: the aep governed dogfood run + +`story:drive-entry-skill` says in its Out of Scope that it is blocked until the `aep` repository's +`story:governed-dogfood-run` lands. That story is `draft` on `beyond10x/aep` `origin/main` +(b11db555f4, read 2026-09-28). Cleared when it reaches `implemented` there. diff --git a/.engineering/planning/story/aep-tutorial.md b/.engineering/planning/story/aep-tutorial.md new file mode 100644 index 0000000..76be432 --- /dev/null +++ b/.engineering/planning/story/aep-tutorial.md @@ -0,0 +1,41 @@ +--- +format: aep.planning-md/3 +id: story:aep-tutorial +kind: story +status: active +title: A public tutorial takes the ESS tutorial's library to a governed, critiqued plan and one implemented story with AEP +summary: 'tutorials/first-governed-plan: adopt, a new noun modelled in ESS first, decompose, scope, critic panel, one wave; real output; held true by tools and a trial.' +relations: +- informed_by: story:ess-tutorial-and-onboarding +scope: +- confidence: cited + path: trials/aep-tutorial +- confidence: cited + path: website/docs/tutorials +revision: 5 +transitions: +- {from: "draft", to: "proposed", at: "2026-09-28T13:12:54Z", actor: "human:timo", revision: 2} +- {from: "proposed", to: "active", at: "2026-09-28T13:12:54Z", actor: "human:timo", revision: 3} +--- +# Story: a public AEP tutorial + +## Outcome +A developer who finished *Your first ESS specification* continues on the same library repository: +adopts an AEP planning store, asks for a feature that introduces a new noun, and gets a modelled +noun, a decomposed and scoped plan argued with by the critic panel, and one story implemented in a +wave with its evidence recorded. Every output on the page is from a real run. + +## Context +- `website/docs/golden-path.md` records AEP 0.44.0 and ESS 0.5.1 output on an `aep.project/1` + store; new stores are `aep.project/5` since aep 0.62. +- The ESS tutorial (0.17.0) ends with "a tutorial like this one for AEP follows". + +## Acceptance +- `website/docs/tutorials/first-governed-plan.md` exists, recorded with the newest aep and ess. +- `agentplugins-check tools` checks every command the page spells against the newest releases. +- A trial gives a fresh agent only the page and reaches a valid store with the planned stories and + one story implemented. +- `task check` and `task site-build` pass. + +## Out of Scope +Driving a story with `metaharness aep drive` (blocked, see `story:drive-entry-skill`). diff --git a/.engineering/planning/story/docs-manifest-links-resolve.md b/.engineering/planning/story/docs-manifest-links-resolve.md new file mode 100644 index 0000000..1c0ac96 --- /dev/null +++ b/.engineering/planning/story/docs-manifest-links-resolve.md @@ -0,0 +1,32 @@ +--- +format: aep.planning-md/3 +id: story:docs-manifest-links-resolve +kind: story +status: active +title: agentplugins-check refuses a b10x.docs.yaml URL that names no page +summary: Every https://beyond10x.github.io/docs/agentplugins// in b10x.docs.yaml maps to website/docs/.md. +scope: +- confidence: cited + path: crates/agentplugins-check/src/main.rs +revision: 4 +transitions: +- {from: "draft", to: "proposed", at: "2026-09-28T13:12:54Z", actor: "human:timo", revision: 2} +- {from: "proposed", to: "active", at: "2026-09-28T13:12:54Z", actor: "human:timo", revision: 3} +--- +# Story: the docs manifest links resolve + +## Outcome +A `b10x.docs.yaml` section URL under this repository's route base that names no page fails +`task check`, before it reaches the public website build. + +## Context +From 2026-09-24 to 2026-09-28 every Atlas "Publish unified documentation" run failed: the only +broken link was `/ecosystem/agentplugins/` → `/docs/agentplugins/plugins/beyond10x/`, the reference +URL `b10x.docs.yaml` kept after 0.14.0 renamed that page (atlas run 36390633830). The public site +stayed on the 2026-09-24 08:20 publication for four days. 0.17.0 fixed the URL. + +## Acceptance +`agentplugins-check` maps every `url` in `b10x.docs.yaml` that starts with the surface's +`canonicalUrl` to `website/docs/.md` (or `/index.md`, or the intro at the root) and +refuses one with no page, naming the line; a unit test shows the old `plugins/beyond10x/` URL +refused. diff --git a/.engineering/planning/story/shared-docs-toolchain.md b/.engineering/planning/story/shared-docs-toolchain.md index e2a6c7e..2be3b27 100644 --- a/.engineering/planning/story/shared-docs-toolchain.md +++ b/.engineering/planning/story/shared-docs-toolchain.md @@ -2,12 +2,16 @@ format: aep.planning-md/3 id: story:shared-docs-toolchain kind: story -status: draft +status: implemented title: Align the passive producer with the shared contract viewer runtime scope: - confidence: cited path: .github/workflows/b10x-docs-bundle.yml -revision: 2 +revision: 6 +transitions: +- {from: "draft", to: "proposed", at: "2026-09-28T13:12:32Z", actor: "human:timo", revision: 4, decided_on: {"recorded":{"test_result":1}}} +- {from: "proposed", to: "active", at: "2026-09-28T13:12:32Z", actor: "human:timo", revision: 5, decided_on: {"recorded":{"test_result":1}}} +- {from: "active", to: "implemented", at: "2026-09-28T13:12:33Z", actor: "human:timo", revision: 6, decided_on: {"recorded":{"test_result":1}}} --- ## Outcome @@ -20,3 +24,11 @@ The Atlas-generated producer caller pins Docs System commit 1d4c0262911761118ffd ## Scope .github/workflows/b10x-docs-bundle.yml and this planning record. The coordinating authority is the shared documentation rollout; this repository's product/runtime contracts do not change. + +## Resolution + +Delivered by a later pin. `.github/workflows/b10x-docs-bundle.yml` runs +`beyond10x/docs-system/.github/actions/bundle@339b4b8462f19b4c9d3716e6a44ed2a3691eb9d8` +(2026-09-23, "a v5 registry surface is a documentation surface"), and 1d4c026 is an ancestor of +339b4b8 (`git merge-base --is-ancestor`), so the reviewed runtime this story named is in force. +The producer ran credential-free on 44bb0ee and succeeded (run 36406238132, 2026-09-28). diff --git a/.engineering/project.yaml b/.engineering/project.yaml index ca96395..b4e2f3c 100644 --- a/.engineering/project.yaml +++ b/.engineering/project.yaml @@ -1,7 +1,7 @@ planning_scope: agentplugins profile: development.standard protocol: adp/1 -protocols: git+https://github.com/beyond10x/aep#b857bbebcb44f77275bc745659226f4826897e78 +protocols: git+https://github.com/beyond10x/aep#58433bd85a1ccf939566c53d5543df86c3852b19 store: git: {} summary: Curated Codex and Claude Code plugins for AEP planning, ADP development and ESS validation, planned in their own store. From 7aa28c4e800eb2b10ab086fb6ad6331a4653e97a Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:44:13 +0200 Subject: [PATCH 02/10] docs(ess): verify the skills against aep 0.64.0 and ess 0.39.0 - verified.json pins aep 0.64.0 (was 0.63.1) and ess 0.39.0 (was 0.38.0). agentplugins-check tools: aep 141, ess 43, worktree 19 spelled commands; the ESS tutorial's spec, suite and go test pass on 0.39.0. No plugin text spells the removed `protocol` binary. - Trial round: ess-new 14 tool calls, ess-pipeline 10, ess-full-package 51 (15/15 go test), ess-tutorial 38 (17/17), aep-backlog 314, all within the baseline. ess-retrofit first regressed (8 refusals): the agent modelled GET /tools/{id} as a view filtered `id == param.id`, which synthesis cannot bind (ESS-SYNTH-005, same on 0.38.0). With the identity filter removed the same spec gives 20 scenarios and 0 refusals; the rerun with the fix below gave 25 and 0. - ess:retrofitting and syntax.md: a read by identity is the unfiltered view with a comment; never `filter: id == param.id`. Filed with the link-field gap as beyond10x/ess#193. - ess:hardening: ExploreConcurrent / exploreConcurrent and `ess verify conform check-history` (ess 0.39.0) for services that take concurrent calls. --- plugins/ess/skills/hardening/SKILL.md | 7 +++++++ plugins/ess/skills/retrofitting/SKILL.md | 3 +++ plugins/ess/skills/specifying/references/syntax.md | 2 ++ verified.json | 4 ++-- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/ess/skills/hardening/SKILL.md b/plugins/ess/skills/hardening/SKILL.md index 133a84f..1483f71 100644 --- a/plugins/ess/skills/hardening/SKILL.md +++ b/plugins/ess/skills/hardening/SKILL.md @@ -83,6 +83,13 @@ somewhere nobody looked. failure and fails on an unreached outcome. Use it for technique 2. Techniques 3 and 5 still need a model you drive yourself, and so does a construct the explorer lists as `excluded`: build it from [references/reference-model.md](references/reference-model.md). +- **Several clients at once:** the same packages carry `ExploreConcurrent`/`exploreConcurrent`, + which drive 2–4 clients on a seeded clock, write `history-.json`, and call + `ess verify conform check-history --history ` to search for an order the specification's + model accepts. Exit 0 is linearizable, 1 a violation (with a shrunk history), 3 the search budget + ran out, which is never a pass. With `Inject`/`inject` it adds only the faults the specification + declares (`delivery: at_least_once`, `replays:`, `external:` branches). Use it for technique 2 + wherever the service takes concurrent calls. ## Reporting diff --git a/plugins/ess/skills/retrofitting/SKILL.md b/plugins/ess/skills/retrofitting/SKILL.md index dfcdd91..cca11f1 100644 --- a/plugins/ess/skills/retrofitting/SKILL.md +++ b/plugins/ess/skills/retrofitting/SKILL.md @@ -62,6 +62,9 @@ Retrofit-specific rules: response carries. An entity with `invariants` also needs a view holding every state, or the suite refuses the invariant checks (`ess:specifying`, conformance section). That view is structural, not a read the code has: say so in a comment. It is the one view a retrofit may add. + A read by identity (`GET /tools/{id}`) is not a view of its own: `filter: id == param.id` leaves + every outcome it observes unsynthesized (`ESS-SYNTH-005`). Declare the entity's view without that + filter and say in a comment that the service also reads it by id. - **A command the code accepts and ignores in every state it does not act from is `wrong_state: true` with `refuses: false`** and no error: the scenario then requires success and no change, which is the code's behaviour. Add no error the code never raises. A command has at most one diff --git a/plugins/ess/skills/specifying/references/syntax.md b/plugins/ess/skills/specifying/references/syntax.md index 7dada20..d5e8b4b 100644 --- a/plugins/ess/skills/specifying/references/syntax.md +++ b/plugins/ess/skills/specifying/references/syntax.md @@ -290,6 +290,8 @@ events: # Views: read models over an entity. `read_your_writes` or `eventual`; an optional `filter`. A view # the caller narrows takes `params: [{name: title, type: library.lending.Title}]` with # `filter: title == param.title` (the key is `params:`); the creating outcome must `sets:` the field. +# Never narrow on the identity (`filter: id == param.id`): synthesis refuses every outcome that view +# observes (ESS-SYNTH-005, "bound by nothing a scenario knows"). A read by id is the unfiltered view. views: - name: library.lending.AvailableCopies source: library.lending.Copy diff --git a/verified.json b/verified.json index 34379aa..2583ee3 100644 --- a/verified.json +++ b/verified.json @@ -1,5 +1,5 @@ { - "aep": "0.63.1", - "ess": "0.38.0", + "aep": "0.64.0", + "ess": "0.39.0", "worktree": "0.8.2" } From 25cc2592c2a1170cb0575bc01ac4a1ce66af5ae9 Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:44:15 +0200 Subject: [PATCH 03/10] feat(check): a b10x.docs.yaml link must name a page - Every section URL under a surface's canonicalUrl maps to website/docs/.md or /index.md, or the check fails naming the manifest line. From 2026-09-24 to 2026-09-28 every Atlas "Publish unified documentation" run failed on the one link this would have caught (plugins/beyond10x/, renamed in 0.14.0), and the public site stayed on its 2026-09-24 publication. - Test a_docs_manifest_link_to_a_missing_page_fails_the_check; on the real manifest with the old URL restored the check prints "b10x.docs.yaml:65 links .../plugins/beyond10x/, and website/docs/ has no plugins/beyond10x.md". --- crates/agentplugins-check/src/main.rs | 104 ++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/crates/agentplugins-check/src/main.rs b/crates/agentplugins-check/src/main.rs index 88fbd71..e84a5fa 100644 --- a/crates/agentplugins-check/src/main.rs +++ b/crates/agentplugins-check/src/main.rs @@ -1017,7 +1017,74 @@ fn plan_hosts(root: &Path) -> Result<(), String> { )) } +/// Every `b10x.docs.yaml` URL under a surface's own `canonicalUrl` names a page in `website/docs/`. +/// +/// The organization website builds every source together and refuses a broken link, so one stale +/// URL here stops every publication, not only this repository's: from 2026-09-24 to 2026-09-28 the +/// reference section still named `plugins/beyond10x/`, a page 0.14.0 renamed, and every Atlas +/// "Publish unified documentation" run failed on it while the public site stayed four days old. +fn docs_manifest_links(root: &Path) -> Result<(), String> { + let path = root.join("b10x.docs.yaml"); + let text = + std::fs::read_to_string(&path).map_err(|error| format!("b10x.docs.yaml: {error}"))?; + let manifest: serde_yaml::Value = + serde_yaml::from_str(&text).map_err(|error| format!("b10x.docs.yaml: {error}"))?; + let docs = root.join("website/docs"); + let mut missing = Vec::new(); + let surfaces = manifest + .get("surfaces") + .and_then(serde_yaml::Value::as_sequence) + .map(Vec::as_slice) + .unwrap_or_default(); + for surface in surfaces { + let Some(base) = surface + .get("canonicalUrl") + .and_then(serde_yaml::Value::as_str) + else { + continue; + }; + let sections = surface + .get("sections") + .and_then(serde_yaml::Value::as_sequence) + .map(Vec::as_slice) + .unwrap_or_default(); + for section in sections { + let Some(url) = section.get("url").and_then(serde_yaml::Value::as_str) else { + continue; + }; + let Some(page) = url.strip_prefix(base) else { + continue; + }; + let page = page.trim_matches('/'); + if page.is_empty() + || docs.join(format!("{page}.md")).is_file() + || docs.join(page).join("index.md").is_file() + { + continue; + } + let line = text + .lines() + .position(|candidate| candidate.contains(url)) + .map_or(0, |index| index + 1); + missing.push(format!( + " b10x.docs.yaml:{line} links {url}, and website/docs/ has no {page}.md" + )); + } + } + if missing.is_empty() { + Ok(()) + } else { + Err(format!( + "{} documentation link(s) name no page; the organization website build refuses a \ + broken link:\n{}", + missing.len(), + missing.join("\n") + )) + } +} + fn check(root: &Path) -> Result<(), String> { + docs_manifest_links(root)?; marketplace(root, ".agents/plugins/marketplace.json")?; marketplace(root, ".claude-plugin/marketplace.json")?; catalog(root)?; @@ -1565,6 +1632,43 @@ one product only: `b10x upgrade ess` std::fs::remove_dir_all(&sandbox).expect("the sandbox is removable"); } + /// A section URL naming a page that does not exist fails; the committed manifest passes. + #[test] + fn a_docs_manifest_link_to_a_missing_page_fails_the_check() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("checker is under repository root"); + docs_manifest_links(root).expect("the committed manifest's links resolve"); + let sandbox = std::env::temp_dir().join(format!( + "agentplugins-check-docs-links-{}-{:?}", + std::process::id(), + std::thread::current().id() + )); + std::fs::create_dir_all(sandbox.join("website/docs/plugins")).expect("writable"); + std::fs::write(sandbox.join("website/docs/plugins/b10x.md"), "# b10x\n").expect("writable"); + let base = "https://beyond10x.github.io/docs/agentplugins/"; + let manifest = |page: &str| { + format!( + "surfaces:\n- canonicalUrl: {base}\n sections:\n - kind: reference\n url: {base}{page}\n - kind: source\n url: https://github.com/beyond10x/agentplugins\n" + ) + }; + std::fs::write(sandbox.join("b10x.docs.yaml"), manifest("plugins/b10x/")) + .expect("writable"); + docs_manifest_links(&sandbox).expect("an existing page resolves"); + std::fs::write( + sandbox.join("b10x.docs.yaml"), + manifest("plugins/beyond10x/"), + ) + .expect("writable"); + let error = docs_manifest_links(&sandbox).expect_err("a renamed page must fail"); + assert!( + error.contains("b10x.docs.yaml:5 links https://beyond10x.github.io/docs/agentplugins/plugins/beyond10x/"), + "{error}" + ); + std::fs::remove_dir_all(&sandbox).expect("the sandbox is removable"); + } + /// A retired plugin's display name on a public page fails like its id, and a longer word that /// begins with it (`AEP Planning`) does not. #[test] From cbcdbcc456508e4c510ca1a2860ae34f381e14d3 Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:17:59 +0200 Subject: [PATCH 04/10] docs(aep): check each critic's verdict line; decomposed stories serve the epic's objective MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings from recording the AEP tutorial (2026-09-28, aep 0.64.0): - Two of the four plan critics returned their verdict off line 1 (design put it last; parallel-safety wrote `approve — …`), although each procedure and critic-rubric.md say line 1 is exactly the verdict. aep:planning section 7 now has the recorder check line 1 and send a malformed reply back once before recording it. - Accepting the six drafted stories was refused: "story:... is proposed and serves no objective" (development.standard). The decomposer now relates each story `serves:` the epic's objective at creation. --- plugins/aep/skills/planning/SKILL.md | 5 +++++ plugins/aep/skills/planning/references/decomposer.md | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/plugins/aep/skills/planning/SKILL.md b/plugins/aep/skills/planning/SKILL.md index 92474f8..7384035 100644 --- a/plugins/aep/skills/planning/SKILL.md +++ b/plugins/aep/skills/planning/SKILL.md @@ -506,6 +506,11 @@ created review-result:acceptance-round-1 (active) at .engineering/planning/revie the report that you made that edit. * Repeat `--relate` once per artifact the critic judged. Read the edge name from `aep plan artifact relations` before you rely on it, the way you would any other vocabulary. +* Check the first line before you record. It must be exactly `approve` or exactly + `needs-revision`, alone. A reply that opens with its reading notes or `approve — …` goes back to + that critic once, asking for the same report with the verdict alone on line 1; record what it + returns then, and say in your report which critic needed it. (Observed 2026-09-28: two of four + critics put the verdict elsewhere.) * Write them one at a time. Four critics return at once; the store takes one writer. * A later round is a **new** record, not an edit of the first. Two records that disagree are the history of a plan changing its mind, which is the thing worth having. diff --git a/plugins/aep/skills/planning/references/decomposer.md b/plugins/aep/skills/planning/references/decomposer.md index 05feb37..9262f03 100644 --- a/plugins/aep/skills/planning/references/decomposer.md +++ b/plugins/aep/skills/planning/references/decomposer.md @@ -155,6 +155,12 @@ $ aep plan artifact new story credential-store \ --relate decomposes:epic:passkey-login ``` +Where the epic `serves` an objective (a `vision` artifact; `aep plan artifact show ` lists +it), add `--relate serves:` to each story too. A store under +`development.standard` refuses to move a story that serves no objective from `proposed` to +`active` (`… is proposed and serves no objective`), so a story drafted without the edge cannot be +accepted as written. + Then write each story's complete body through `aep plan artifact body --from `: the context, every `inferable` relation the story rests on with its citation, and **one acceptance statement** — a single sentence naming an From 9b07526f845cb1428c6fda8cca4259882ad70206 Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:18:05 +0200 Subject: [PATCH 05/10] feat(check): upstream report and the following-upstream skill - `agentplugins-check upstream` (network) prints what moved in every repository this one depends on: the aep, ess, worktree, metaharness and connectors releases against their pins (verified.json, eval.yml, the integrating skill) with the CHANGELOG sections since each pin; every `uses: beyond10x//...@` against that repository's main; and the state of every beyond10x/# cited in plugins/ and website/docs/. It reports and fails only when it cannot read. - First run: connectors v0.7.2 -> v0.13.3 (the v1 line is deliberate, an operator decision); docs-system, gates and website pins behind main (Atlas-generated files, moved by Atlas); aep#60 and ess#186 open. A `git+...#` locator was first read as issue aep#8; issue numbers now need a word boundary, with a test. - New .agents/skills/following-upstream (linked for Claude Code): detect with the report, sort each changelog entry, update, verify with tools and a trial round before verified.json moves, release, then the website Start page pins and the next Atlas publication. - AGENTS.md: map row and gate line. --- .agents/skills/following-upstream/SKILL.md | 87 ++++++ .claude/skills/following-upstream | 1 + AGENTS.md | 2 + crates/agentplugins-check/src/main.rs | 5 + crates/agentplugins-check/src/tools.rs | 6 +- crates/agentplugins-check/src/upstream.rs | 330 +++++++++++++++++++++ 6 files changed, 428 insertions(+), 3 deletions(-) create mode 100644 .agents/skills/following-upstream/SKILL.md create mode 120000 .claude/skills/following-upstream create mode 100644 crates/agentplugins-check/src/upstream.rs diff --git a/.agents/skills/following-upstream/SKILL.md b/.agents/skills/following-upstream/SKILL.md new file mode 100644 index 0000000..1f3096d --- /dev/null +++ b/.agents/skills/following-upstream/SKILL.md @@ -0,0 +1,87 @@ +--- +name: following-upstream +description: Bring this repository up to date with everything it takes from other repositories — the aep, ess, worktree, metaharness and connectors releases its plugins drive, the workflows pinned by commit, and the issues its skills work around. Use when asked to check for upstream releases, sync or refresh agentplugins, follow a new aep or ess release, or when the Tools check fails with "is newer than verified.json". Run it on a schedule. +--- + +# Following upstream + +This repository teaches agents to use CLIs it does not build. Every release of one of them can +rename a command, change a format, fix a bug a skill works around, or add something worth teaching. +This skill is the loop that finds those changes and carries them here: detect, read, update, +verify, release, then follow the change downstream. + +## 1. Detect + +```console +cargo run --locked --bin agentplugins-check -- upstream > ~/.cache/agentplugins-upstream.md +``` + +The report has three sections, and ends with ` item(s) moved.` A run with 0 moved items ends +this skill: report that and stop. + +| section | moved means | where the pin lives | +|---|---|---| +| Releases | a newer release than the pin, with its `CHANGELOG.md` sections since the pin | `verified.json` (aep, ess, worktree), `.github/workflows/eval.yml` (metaharness), the `integrating` skill (connectors) | +| Workflow pins | `main` of a `beyond10x/*` repository is past the commit a workflow uses | `.github/workflows/*.yml` | +| Cited issues | an issue a skill or page cites is closed | the file that cites it | + +## 2. Read, and sort every change + +Read each changelog section in full. Sort each entry into one row; an entry may land in two. + +| kind | what to do here | +|---|---| +| a command, flag or verb removed or renamed | fix every spelling in `plugins/` and `website/docs/`; `tools` names each one it no longer finds | +| a format, store or protocol version | the skills' version tables and upgrade paths (`aep:upgrade`, `ess:specifying` `later-formats.md`, the store section of `aep:planning`) | +| a new capability an agent would use | the skill that owns the activity (`ess:hardening`, `ess:testing-conformance`, `aep:implementing` …), one paragraph, with the command | +| a fix for something a skill works around | remove the workaround when the release carries the fix; cite the release | +| internal only (tests, refactors, CI) | nothing | + +For a **closed cited issue**, read the closing change and remove or rewrite the text that cites it. +For a **moved workflow pin**: a file that starts `Generated by atlas docs reconcile` is not edited +here; it moves when Atlas reconciles. Report it. A hand-written workflow moves by one commit that +names the new pin and what changed. + +For **connectors**, the `integrating` skill deliberately targets the v1 line (`v0.7.2`) while the +newest releases are the v2 lineage. A moved connectors release is a decision for the operator, not +an edit: report it with its changelog, once. + +## 3. Update + +In a managed worktree from `origin/main`. The rules of `AGENTS.md` hold: no CLI version in plugin +text (R5), grouped verbs, no retired names. Edit what § 2 sorted; do not move `verified.json` yet. +A store `protocols:` pin in `.engineering/project.yaml` moves to the new aep release commit. + +## 4. Verify + +1. `cargo run --locked --bin agentplugins-check -- tools` — every spelled command against the newest + releases, the ESS syntax example, and the ESS tutorial's specification, suite and `go test`. +2. A trial round per [`improving-by-trial`](../improving-by-trial/SKILL.md): the ESS trials and + `ess-tutorial` for an ess release, `aep-backlog` and `aep-tutorial` for an aep release. A run + worse than `trials/baseline.json` is triaged there; a defect in the other repository becomes a + `trial-finding` issue there, and a workaround here that cites it. +3. Then, and only then, `verified.json` moves to the new releases. +4. `task check` and `task site-build`. + +## 5. Release + +The release in `AGENTS.md` § Publishing: `CHANGELOG.md` names the releases verified against and what +changed for an agent; versions agree; bot commits, a pull request, merge on the required checks, a +bare annotated tag, then verify the release run, the GitHub Release, its assets and `SHA256SUMS`. + +## 6. Downstream + +- **The website's Start page** pins agentplugins, aep and ess releases + (`beyond10x/website` `data/experiences.json`, its validator and contract test). Move them in one + website pull request: lock only the `agentplugins` source beside `main`'s lock, commit it, render + `atlas docs snapshot` from a managed Atlas checkout at `origin/main`, and pass `npm run gate`. +- **Publication.** After the merge, the next Atlas "Publish unified documentation" run must pass. + One broken link anywhere fails every source's publication; read the failed run's log before + assuming the delay is Atlas's. +- Tell whoever asked for the release (a peer session, an issue) which version carries it. + +## Report + +Per moved item: what moved, what changed here (commit), or why nothing did. Then the trial numbers +against the baseline, the release and its verification, the issues filed, and anything left for the +operator (a connectors lineage decision, an Atlas-generated pin). diff --git a/.claude/skills/following-upstream b/.claude/skills/following-upstream new file mode 120000 index 0000000..9edd667 --- /dev/null +++ b/.claude/skills/following-upstream @@ -0,0 +1 @@ +../../.agents/skills/following-upstream \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index e2ee58b..fc66b9e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,7 @@ CLIs. Serves O2 (decisions as data) and O3 (any harness). | `website/` | public docs; must pass `task site-build` | | `SETUP.md` | agent bootstrap, published as a release asset | | `.agents/skills/improving-by-trial/` | how plugins are improved: isolated headless trials (`task trial:sandbox`, `task trial:run`), triage, fix, re-run | +| `.agents/skills/following-upstream/` | how this repository follows the releases, workflow pins and issues it depends on: `agentplugins-check upstream` reports what moved, then update, verify, release, follow downstream | | `trials/` | the round's trial definitions (`/trial.yaml`, fixtures) and `baseline.json`; `task trial:run TRIAL=` runs one, `agentplugins-check trial-report` measures it against the baseline | ## Rules @@ -41,6 +42,7 @@ CLIs. Serves O2 (decisions as data) and O3 (any harness). task check # fmt, clippy, tests, agentplugins-check (includes the trial definitions) task site-build # when website/ changes cargo run --locked --bin agentplugins-check -- tools # network: every spelled command exists in the newest aep, ess, worktree +cargo run --locked --bin agentplugins-check -- upstream # network: what moved upstream (releases and changelogs, workflow pins, cited issues) ``` ## Planning diff --git a/crates/agentplugins-check/src/main.rs b/crates/agentplugins-check/src/main.rs index e84a5fa..bc0f448 100644 --- a/crates/agentplugins-check/src/main.rs +++ b/crates/agentplugins-check/src/main.rs @@ -12,6 +12,7 @@ mod report; mod tools; mod trial; mod trials; +mod upstream; /// The marketplace identity in every marketplace format. const MARKETPLACE: &str = "b10x"; @@ -1146,6 +1147,9 @@ struct Cli { enum Top { /// The offline gate, then every spelled CLI command against the newest releases (network). Tools, + /// What moved in every repository this one depends on: releases with their changelog sections, + /// workflow pins and cited issues (network). Reports; fails only when it cannot read. + Upstream, /// Release checks. Release { #[command(subcommand)] @@ -1270,6 +1274,7 @@ fn main() -> ExitCode { let result = match Cli::parse().command { None => check(&root), Some(Top::Tools) => check(&root).and_then(|()| tools::verify(&root)), + Some(Top::Upstream) => upstream::report(&root), Some(Top::Release { action: ReleaseAction::Verify { version }, }) => check(&root).and_then(|()| verify_release(&root, &version)), diff --git a/crates/agentplugins-check/src/tools.rs b/crates/agentplugins-check/src/tools.rs index be84250..4d316ce 100644 --- a/crates/agentplugins-check/src/tools.rs +++ b/crates/agentplugins-check/src/tools.rs @@ -31,7 +31,7 @@ const DEPTH: usize = 4; pub const VERIFIED: &str = "verified.json"; /// A key for an `x.y.z` version or tag. -fn key(version: &str) -> Option<(u64, u64, u64)> { +pub(crate) fn key(version: &str) -> Option<(u64, u64, u64)> { let mut parts = version.trim_start_matches('v').split('.'); let triple = ( parts.next()?.parse().ok()?, @@ -74,7 +74,7 @@ pub fn unverified(cli: &str, newest: &str, verified: &str) -> Option { }) } -fn run(program: &str, arguments: &[&str]) -> Result { +pub(crate) fn run(program: &str, arguments: &[&str]) -> Result { let output = Command::new(program) .args(arguments) .output() @@ -100,7 +100,7 @@ fn target() -> Result<&'static str, String> { } } -fn latest(repository: &str) -> Result { +pub(crate) fn latest(repository: &str) -> Result { let location = run( "curl", &[ diff --git a/crates/agentplugins-check/src/upstream.rs b/crates/agentplugins-check/src/upstream.rs new file mode 100644 index 0000000..7f1a129 --- /dev/null +++ b/crates/agentplugins-check/src/upstream.rs @@ -0,0 +1,330 @@ +//! `agentplugins-check upstream` (network): everything this repository takes from another one, and +//! whether it has moved. +//! +//! The report the `following-upstream` skill starts from. It never fails on news: a newer release, +//! a moved workflow pin or a closed issue is work to do, and `tools` is the check that refuses. +//! It fails only when it cannot read something. +//! +//! Three kinds of dependency: +//! +//! - **Releases.** Each CLI a plugin drives, at the release this repository last verified or names: +//! `verified.json` for `aep`, `ess` and `worktree`, and the `*_VERSION` pins of +//! `.github/workflows/eval.yml` or a release named in a skill for the rest. A newer release gets +//! its `CHANGELOG.md` sections from the pinned release (exclusive) to the newest (inclusive). +//! - **Workflow pins.** Every `uses: beyond10x//…@` against that repository's `main`. +//! - **Cited issues.** Every `beyond10x/#` in plugin or website text: a closed one is a +//! workaround that may be removed. + +use std::collections::BTreeSet; +use std::path::{Path, PathBuf}; + +use crate::tools; + +/// A release this repository depends on, and where its pin is written. +struct Tracked { + name: &'static str, + repository: &'static str, + pin: Pin, +} + +enum Pin { + /// The CLI's entry in `verified.json`. + Verified, + /// The first `` in this file. + Text { + file: &'static str, + prefix: &'static str, + }, +} + +const TRACKED: &[Tracked] = &[ + Tracked { + name: "aep", + repository: "beyond10x/aep", + pin: Pin::Verified, + }, + Tracked { + name: "ess", + repository: "beyond10x/ess", + pin: Pin::Verified, + }, + Tracked { + name: "worktree", + repository: "beyond10x/worktree", + pin: Pin::Verified, + }, + Tracked { + name: "metaharness", + repository: "beyond10x/metaharness", + pin: Pin::Text { + file: ".github/workflows/eval.yml", + prefix: "METAHARNESS_VERSION: '", + }, + }, + Tracked { + name: "connectors", + repository: "beyond10x/connectors", + pin: Pin::Text { + file: "plugins/connectors/skills/integrating/SKILL.md", + prefix: "[Connectors `", + }, + }, +]; + +/// The version string that starts right after `prefix` in `text`. +#[must_use] +pub fn pinned_in(text: &str, prefix: &str) -> Option { + let start = text.find(prefix)? + prefix.len(); + let version: String = text[start..] + .chars() + .take_while(|c| c.is_ascii_alphanumeric() || *c == '.' || *c == '-') + .collect(); + (!version.is_empty()).then_some(version) +} + +/// The `CHANGELOG.md` sections for releases newer than `from` and no newer than `to`, in the order +/// the file has them. A section starts at a `## ` heading that names a version. +#[must_use] +pub fn sections_between(changelog: &str, from: &str, to: &str) -> Vec { + let (Some(from), Some(to)) = (tools::key(from), tools::key(to)) else { + return Vec::new(); + }; + let mut sections = Vec::new(); + let mut current: Option = None; + for line in changelog.lines() { + if let Some(heading) = line.strip_prefix("## ") { + if let Some(section) = current.take() { + sections.push(section); + } + let version = heading + .trim_start_matches('[') + .split([']', ' ', '—']) + .next() + .unwrap_or_default(); + if tools::key(version).is_some_and(|key| key > from && key <= to) { + current = Some(format!("{line}\n")); + } + continue; + } + if let Some(section) = current.as_mut() { + section.push_str(line); + section.push('\n'); + } + } + sections.extend(current); + sections +} + +/// Every `beyond10x/#` in `text`. +#[must_use] +pub fn cited_issues(text: &str) -> BTreeSet<(String, u64)> { + let mut found = BTreeSet::new(); + let mut rest = text; + while let Some(at) = rest.find("beyond10x/") { + rest = &rest[at + "beyond10x/".len()..]; + let repository: String = rest + .chars() + .take_while(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '.') + .collect(); + let after = &rest[repository.len()..]; + if let Some(digits) = after.strip_prefix('#') { + let number: String = digits.chars().take_while(char::is_ascii_digit).collect(); + let boundary = digits[number.len()..] + .chars() + .next() + .is_none_or(|c| !c.is_ascii_alphanumeric()); + if let (true, Ok(number)) = (boundary, number.parse()) { + found.insert((repository, number)); + } + } + } + found +} + +/// Every `uses: beyond10x//…@` in `text`, as (repo, commit). +#[must_use] +pub fn workflow_pins(text: &str) -> BTreeSet<(String, String)> { + text.lines() + .filter_map(|line| line.trim().strip_prefix("uses: beyond10x/")) + .filter_map(|rest| { + let repository = rest.split('/').next()?.to_owned(); + let commit = rest.rsplit_once('@')?.1.trim().to_owned(); + (commit.len() == 40).then_some((repository, commit)) + }) + .collect() +} + +fn files(root: &Path, extension: &str) -> Vec { + let mut found = Vec::new(); + let mut stack = vec![root.to_path_buf()]; + while let Some(dir) = stack.pop() { + for entry in std::fs::read_dir(&dir).into_iter().flatten().flatten() { + let path = entry.path(); + if path.is_dir() { + stack.push(path); + } else if path.extension().and_then(|e| e.to_str()) == Some(extension) { + found.push(path); + } + } + } + found.sort(); + found +} + +fn fetch(url: &str) -> Result { + tools::run("curl", &["-fsSL", "--retry", "2", url]) +} + +fn short(commit: &str) -> &str { + commit.get(..7).unwrap_or(commit) +} + +/// Print the report; fail only when something cannot be read. +pub fn report(root: &Path) -> Result<(), String> { + let verified = tools::verified(root)?; + let mut moved = 0; + println!("# Upstream report\n\n## Releases\n"); + for tracked in TRACKED { + let pinned = match &tracked.pin { + Pin::Verified => verified.get(tracked.name).cloned(), + Pin::Text { file, prefix } => std::fs::read_to_string(root.join(file)) + .ok() + .and_then(|text| pinned_in(&text, prefix)), + } + .ok_or_else(|| format!("{}: no pinned release found", tracked.name))?; + let newest = tools::latest(tracked.repository)?; + let behind = tools::key(&newest) > tools::key(&pinned); + println!( + "- `{}` pinned {pinned}, newest {newest}{}", + tracked.name, + if behind { " — **moved**" } else { "" } + ); + if behind { + moved += 1; + let url = format!( + "https://raw.githubusercontent.com/{}/{newest}/CHANGELOG.md", + tracked.repository + ); + match fetch(&url) { + Ok(changelog) => { + for section in sections_between(&changelog, &pinned, &newest) { + println!("\n```markdown\n{section}```\n"); + } + } + Err(error) => println!(" (no changelog at {url}: {error})"), + } + } + } + + println!("\n## Workflow pins\n"); + let mut pins = BTreeSet::new(); + for workflow in files(&root.join(".github/workflows"), "yml") { + let text = std::fs::read_to_string(&workflow).map_err(|error| error.to_string())?; + pins.extend(workflow_pins(&text)); + } + for (repository, commit) in pins { + let head = tools::run( + "git", + &[ + "ls-remote", + &format!("/beyond10x/{repository}"), + "refs/heads/main", + ], + )?; + let main = head.split_whitespace().next().unwrap_or_default(); + if main == commit { + println!("- `{repository}` pinned {}, main", short(&commit)); + } else { + moved += 1; + println!( + "- `{repository}` pinned {}, main is {} — **moved**", + short(&commit), + short(main) + ); + } + } + + println!("\n## Cited issues\n"); + let mut issues = BTreeSet::new(); + for dir in ["plugins", "website/docs"] { + for file in files(&root.join(dir), "md") { + let text = std::fs::read_to_string(&file).map_err(|error| error.to_string())?; + issues.extend(cited_issues(&text)); + } + } + for (repository, number) in issues { + let json = fetch(&format!( + "https://api.github.com/repos/beyond10x/{repository}/issues/{number}" + ))?; + let value: serde_json::Value = + serde_json::from_str(&json).map_err(|error| error.to_string())?; + let state = value.get("state").and_then(|s| s.as_str()).unwrap_or("?"); + if state == "closed" { + moved += 1; + } + println!( + "- beyond10x/{repository}#{number} {state}{}", + if state == "closed" { + " — **read the text that cites it: a workaround may go**" + } else { + "" + } + ); + } + println!("\n{moved} item(s) moved."); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_pin_is_read_after_its_prefix() { + assert_eq!( + pinned_in( + "x\n METAHARNESS_VERSION: '0.8.0'\n", + "METAHARNESS_VERSION: '" + ), + Some("0.8.0".to_owned()) + ); + assert_eq!( + pinned_in("[Connectors `v0.7.2` release](…)", "[Connectors `"), + Some("v0.7.2".to_owned()) + ); + } + + #[test] + fn only_the_sections_after_the_pin_are_returned() { + let changelog = "# Changelog\n\n## [0.64.0] — 2026-09-28\n\nnew\n\n## [0.63.1] — 2026-09-28\n\nfix\n\n## [0.63.0]\n\nold\n"; + let sections = sections_between(changelog, "0.63.0", "0.64.0"); + assert_eq!(sections.len(), 2); + assert!(sections[0].starts_with("## [0.64.0]") && sections[0].contains("new")); + assert!(sections[1].starts_with("## [0.63.1]") && !sections[1].contains("old")); + let plain = "## 0.39.0 — x\n\na\n\n## 0.38.0 — y\n\nb\n"; + assert_eq!(sections_between(plain, "0.38.0", "0.39.0").len(), 1); + } + + #[test] + fn issues_and_workflow_pins_are_found() { + let issues = cited_issues("see beyond10x/ess#186 and (beyond10x/aep#8), not beyond10x/ess, nor git+https://github.com/beyond10x/aep#8b4342a41fdd9143"); + assert_eq!( + issues.into_iter().collect::>(), + vec![("aep".to_owned(), 8), ("ess".to_owned(), 186)] + ); + assert!( + cited_issues("git+https://github.com/beyond10x/aep#8b4342a41fdd9143").is_empty(), + "a commit in a git+ locator is not an issue" + ); + let pins = workflow_pins( + " uses: beyond10x/docs-system/.github/actions/check@339b4b8462f19b4c9d3716e6a44ed2a3691eb9d8\n uses: actions/checkout@v4\n", + ); + assert_eq!( + pins.into_iter().collect::>(), + vec![( + "docs-system".to_owned(), + "339b4b8462f19b4c9d3716e6a44ed2a3691eb9d8".to_owned() + )] + ); + } +} From 4d8406c45ae58975371cdef78aba0d9992f9c89e Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:19:00 +0200 Subject: [PATCH 06/10] docs(skills): following-upstream says "a new release of ess" - The flat-spelling gate read "an ess release" as the retired verb `ess release` (now `ess generate release`); the two sentences say "a new release of ess" instead. --- .agents/skills/following-upstream/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.agents/skills/following-upstream/SKILL.md b/.agents/skills/following-upstream/SKILL.md index 1f3096d..8f3de5f 100644 --- a/.agents/skills/following-upstream/SKILL.md +++ b/.agents/skills/following-upstream/SKILL.md @@ -1,6 +1,6 @@ --- name: following-upstream -description: Bring this repository up to date with everything it takes from other repositories — the aep, ess, worktree, metaharness and connectors releases its plugins drive, the workflows pinned by commit, and the issues its skills work around. Use when asked to check for upstream releases, sync or refresh agentplugins, follow a new aep or ess release, or when the Tools check fails with "is newer than verified.json". Run it on a schedule. +description: Bring this repository up to date with everything it takes from other repositories — the aep, ess, worktree, metaharness and connectors releases its plugins drive, the workflows pinned by commit, and the issues its skills work around. Use when asked to check for upstream releases, sync or refresh agentplugins, follow a new release of aep or ess, or when the Tools check fails with "is newer than verified.json". Run it on a schedule. --- # Following upstream @@ -57,7 +57,7 @@ A store `protocols:` pin in `.engineering/project.yaml` moves to the new aep rel 1. `cargo run --locked --bin agentplugins-check -- tools` — every spelled command against the newest releases, the ESS syntax example, and the ESS tutorial's specification, suite and `go test`. 2. A trial round per [`improving-by-trial`](../improving-by-trial/SKILL.md): the ESS trials and - `ess-tutorial` for an ess release, `aep-backlog` and `aep-tutorial` for an aep release. A run + `ess-tutorial` for a new release of ess, `aep-backlog` and `aep-tutorial` for a new release of aep. A run worse than `trials/baseline.json` is triaged there; a defect in the other repository becomes a `trial-finding` issue there, and a workaround here that cites it. 3. Then, and only then, `verified.json` moves to the new releases. From 94b4f0ce220680ba003753441cc9af62498c981c Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:19:03 +0200 Subject: [PATCH 07/10] test(evals): live recording through metaharness; a fixture for the critic cases - aep 0.64.0 moved live evaluation: `aep drive eval run` without --stream prints "live evaluation moved to `metaharness aep drive eval run`". The corpus README, every recorded/README.md and eval.yml now spell the metaharness command; eval.yml pins aep 0.64.0 and metaharness 0.8.0 (were 0.44.0 and 0.4.2) and installs the metaharness-cli package. Recorded READMEs name plugins/aep, not the retired plugins/aep-plan and plugins/aep-drive. - Recording is blocked: metaharness 0.8.0 and its main link aep 0.55.0 and refuse every case with EVAL-RUN-017 beside aep 0.64.0 (observed on the four plan-critic cases). Filed beyond10x/metaharness#10; dependency-blocker:metaharness-links-aep-0-55 blocks story:plugin-eval-cases. - fixtures/library-reservations-drafted: the AEP tutorial's library at the commit before its critics ran (epic:book-reservations, six draft stories), outside evals/ because the corpus refuses a directory that is not a case. The four plan-critic cases now name epic:book-reservations and that fixture as their working tree; `aep plan artifact validate` on a Git copy: valid, 10 artifacts. --- .../metaharness-links-aep-0-55.md | 20 + .github/workflows/eval.yml | 8 +- evals/README.md | 15 +- .../recorded/README.md | 2 +- .../recorded/README.md | 2 +- evals/adversary-tests-only/recorded/README.md | 4 +- .../recorded/README.md | 2 +- .../recorded/README.md | 2 +- .../recorded/README.md | 4 +- .../recorded/README.md | 2 +- .../golden-path-end-to-end/recorded/README.md | 4 +- .../plan-critic-acceptance-verdict/case.yaml | 2 +- .../recorded/README.md | 12 +- evals/plan-critic-design-verdict/case.yaml | 2 +- .../recorded/README.md | 12 +- .../case.yaml | 2 +- .../recorded/README.md | 12 +- evals/plan-critic-scope-verdict/case.yaml | 2 +- .../recorded/README.md | 12 +- .../recorded/README.md | 2 +- .../recorded/README.md | 2 +- .../recorded/README.md | 2 +- evals/wave-claim-verdict/recorded/README.md | 2 +- .../planning/epic/book-reservations.md | 82 +++ .../library.md | 39 ++ .../story/borrow-member-check-tested.md | 37 ++ .../planning/story/cancel-reservation.md | 56 ++ .../planning/story/collect-held-book.md | 63 ++ .../planning/story/release-hold.md | 51 ++ .../story/reservations-suite-baseline.md | 65 ++ .../planning/story/reserve-book-on-loan.md | 70 +++ .../story/return-reserved-book-to-hold.md | 61 ++ .../planning/vision/lending-library.md | 29 + .../.engineering/project.yaml | 11 + .../library-reservations-drafted/.gitignore | 3 + .../impl/conformance_test.go | 191 ++++++ .../library-reservations-drafted/impl/go.mod | 3 + .../impl/library.go | 156 +++++ .../spec/components.yaml | 29 + .../spec/domains/lending.yaml | 569 ++++++++++++++++++ .../spec/ess-inputs.yaml | 7 + .../spec/system.yaml | 6 + 42 files changed, 1608 insertions(+), 49 deletions(-) create mode 100644 .engineering/planning/dependency-blocker/metaharness-links-aep-0-55.md create mode 100644 fixtures/library-reservations-drafted/.engineering/planning/epic/book-reservations.md create mode 100644 fixtures/library-reservations-drafted/.engineering/planning/executable-system-specification/library.md create mode 100644 fixtures/library-reservations-drafted/.engineering/planning/story/borrow-member-check-tested.md create mode 100644 fixtures/library-reservations-drafted/.engineering/planning/story/cancel-reservation.md create mode 100644 fixtures/library-reservations-drafted/.engineering/planning/story/collect-held-book.md create mode 100644 fixtures/library-reservations-drafted/.engineering/planning/story/release-hold.md create mode 100644 fixtures/library-reservations-drafted/.engineering/planning/story/reservations-suite-baseline.md create mode 100644 fixtures/library-reservations-drafted/.engineering/planning/story/reserve-book-on-loan.md create mode 100644 fixtures/library-reservations-drafted/.engineering/planning/story/return-reserved-book-to-hold.md create mode 100644 fixtures/library-reservations-drafted/.engineering/planning/vision/lending-library.md create mode 100644 fixtures/library-reservations-drafted/.engineering/project.yaml create mode 100644 fixtures/library-reservations-drafted/.gitignore create mode 100644 fixtures/library-reservations-drafted/impl/conformance_test.go create mode 100644 fixtures/library-reservations-drafted/impl/go.mod create mode 100644 fixtures/library-reservations-drafted/impl/library.go create mode 100644 fixtures/library-reservations-drafted/spec/components.yaml create mode 100644 fixtures/library-reservations-drafted/spec/domains/lending.yaml create mode 100644 fixtures/library-reservations-drafted/spec/ess-inputs.yaml create mode 100644 fixtures/library-reservations-drafted/spec/system.yaml diff --git a/.engineering/planning/dependency-blocker/metaharness-links-aep-0-55.md b/.engineering/planning/dependency-blocker/metaharness-links-aep-0-55.md new file mode 100644 index 0000000..6f8ef64 --- /dev/null +++ b/.engineering/planning/dependency-blocker/metaharness-links-aep-0-55.md @@ -0,0 +1,20 @@ +--- +format: aep.planning-md/3 +id: dependency-blocker:metaharness-links-aep-0-55 +kind: dependency-blocker +status: open +title: metaharness links aep 0.55.0, so no eval case can be recorded beside aep 0.64.0 +relations: +- blocks: story:plugin-eval-cases +revision: 2 +--- +# Blocker: metaharness links aep 0.55.0 + +aep 0.64.0 moved live evaluation to `metaharness aep drive eval run`. metaharness 0.8.0 and its +`main` (13a8378) link aep at 28abe09 (0.55.0) and refuse every case with EVAL-RUN-017 when the `aep` +on the child's PATH is 0.64.0 (observed 2026-09-28 on the four plan-critic cases). Filed as +beyond10x/metaharness#10. Cleared when a metaharness release links aep 0.64.0 or newer. + +Ready for then: `fixtures/library-reservations-drafted` (the AEP tutorial's store before its +critics ran) is the working tree for the four plan-critic cases, whose tasks now name +`epic:book-reservations`. diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml index 1d9a643..b73373f 100644 --- a/.github/workflows/eval.yml +++ b/.github/workflows/eval.yml @@ -55,8 +55,8 @@ jobs: # and this workflow runs the version pinned below, which has neither. A grouped spelling here # would not teach a reader anything; it would fail to parse on the runner. Move both when the # pin moves. - AEP_VERSION: '0.44.0' - METAHARNESS_VERSION: '0.4.2' + AEP_VERSION: '0.64.0' + METAHARNESS_VERSION: '0.8.0' steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: @@ -195,7 +195,7 @@ jobs: # stop at `unrecognized subcommand 'doctor'`; aep 0.45.0's `eval run` refuses a mismatch. install -d "$HOME/.local/bin" install -m 0755 "aep-${AEP_VERSION}-${target}/aep" "$HOME/.local/bin/aep" - cargo install --locked --git https://github.com/beyond10x/metaharness --tag "$METAHARNESS_VERSION" metaharness + cargo install --locked --git https://github.com/beyond10x/metaharness --tag "$METAHARNESS_VERSION" metaharness-cli aep --version metaharness --version @@ -217,7 +217,7 @@ jobs: [[ -n "$case" ]] || continue echo "::group::$case" case_out="$RUNNER_TEMP/eval-out/${case#evals/}" - aep eval run \ + metaharness aep drive eval run \ --case "$case" \ --arm plugin \ --harness claude \ diff --git a/evals/README.md b/evals/README.md index d860b8a..53714bf 100644 --- a/evals/README.md +++ b/evals/README.md @@ -155,22 +155,25 @@ $ cargo run --quiet --locked --bin agentplugins-check Live, which costs money and is refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run --corpus evals --workflow adp/default \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run --corpus evals --workflow adp/default \ --arm plugin --harness claude --plugin-dir plugins/aep \ --cwd --budget-usd 20 --assume-usd-per-run 5 \ --observed-at --redact --out ``` -Without `METAHARNESS_LIVE=1` the runner accepts the corpus and refuses to spawn, by name: +Since aep 0.64.0 `aep drive eval run` only ingests a recorded run (`--stream`); a live run is +metaharness's: ```console $ aep drive eval run --corpus evals --workflow adp/default --arm plugin --harness claude \ - --out eval-out --observed-at 2026-09-03 -error: eval-out — 1 refusal(s): - EVAL-RUN-002 a spawn costs money and `METAHARNESS_LIVE=1` is not in this environment. Set it - deliberately, or pass `--stream FILE` to ingest a run that already happened, which spends nothing + --out eval-out --observed-at 2026-09-28 +error: live evaluation moved to `metaharness aep drive eval run`; use --stream for offline ingestion ``` +**Blocked on 2026-09-28:** metaharness 0.8.0 links aep 0.55.0 and refuses every case with +`EVAL-RUN-017` when the `aep` on the path is 0.64.0 (beyond10x/metaharness#10; +`dependency-blocker:metaharness-links-aep-0-55` in this repository's store). + ## What a full live run costs | | | diff --git a/evals/adversary-panel-one-family/recorded/README.md b/evals/adversary-panel-one-family/recorded/README.md index 17eb735..67bc156 100644 --- a/evals/adversary-panel-one-family/recorded/README.md +++ b/evals/adversary-panel-one-family/recorded/README.md @@ -11,7 +11,7 @@ rows were fitted to, which measures the document and not the plugin. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/adversary-panel-one-family \ --arm plugin \ --harness claude \ diff --git a/evals/adversary-tautological-test/recorded/README.md b/evals/adversary-tautological-test/recorded/README.md index 6726a1f..6ae3de4 100644 --- a/evals/adversary-tautological-test/recorded/README.md +++ b/evals/adversary-tautological-test/recorded/README.md @@ -11,7 +11,7 @@ rows were fitted to, which measures the document and not the plugin. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/adversary-tautological-test \ --arm plugin \ --harness claude \ diff --git a/evals/adversary-tests-only/recorded/README.md b/evals/adversary-tests-only/recorded/README.md index 5d50a00..32c185d 100644 --- a/evals/adversary-tests-only/recorded/README.md +++ b/evals/adversary-tests-only/recorded/README.md @@ -13,11 +13,11 @@ up with no change to the case. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/adversary-tests-only \ --arm plugin \ --harness claude \ - --plugin-dir plugins/aep-drive \ + --plugin-dir plugins/aep \ --cwd \ --budget-usd 5 \ --observed-at \ diff --git a/evals/authoring-prohibition-only-rule/recorded/README.md b/evals/authoring-prohibition-only-rule/recorded/README.md index f3edc78..94127cd 100644 --- a/evals/authoring-prohibition-only-rule/recorded/README.md +++ b/evals/authoring-prohibition-only-rule/recorded/README.md @@ -11,7 +11,7 @@ rows were fitted to, which measures the document and not the plugin. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/authoring-prohibition-only-rule \ --arm plugin \ --harness claude \ diff --git a/evals/decomposer-expand-migrate-contract/recorded/README.md b/evals/decomposer-expand-migrate-contract/recorded/README.md index cc45a93..020b97d 100644 --- a/evals/decomposer-expand-migrate-contract/recorded/README.md +++ b/evals/decomposer-expand-migrate-contract/recorded/README.md @@ -11,7 +11,7 @@ rows were fitted to, which measures the document and not the plugin. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/decomposer-expand-migrate-contract \ --arm plugin \ --harness claude \ diff --git a/evals/decomposer-relation-census/recorded/README.md b/evals/decomposer-relation-census/recorded/README.md index b9389bd..f33bb6b 100644 --- a/evals/decomposer-relation-census/recorded/README.md +++ b/evals/decomposer-relation-census/recorded/README.md @@ -13,11 +13,11 @@ up with no change to the case. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/decomposer-relation-census \ --arm plugin \ --harness claude \ - --plugin-dir plugins/aep-plan \ + --plugin-dir plugins/aep \ --cwd \ --budget-usd 5 \ --observed-at \ diff --git a/evals/diagnosing-red-loop-first/recorded/README.md b/evals/diagnosing-red-loop-first/recorded/README.md index 217e874..1d2366b 100644 --- a/evals/diagnosing-red-loop-first/recorded/README.md +++ b/evals/diagnosing-red-loop-first/recorded/README.md @@ -11,7 +11,7 @@ rows were fitted to, which measures the document and not the plugin. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/diagnosing-red-loop-first \ --arm plugin \ --harness claude \ diff --git a/evals/golden-path-end-to-end/recorded/README.md b/evals/golden-path-end-to-end/recorded/README.md index 099bfd1..9dcf975 100644 --- a/evals/golden-path-end-to-end/recorded/README.md +++ b/evals/golden-path-end-to-end/recorded/README.md @@ -35,11 +35,11 @@ up with no change to the case. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/golden-path-end-to-end \ --arm plugin \ --harness claude \ - --plugin-dir plugins/aep-plan \ + --plugin-dir plugins/aep \ --plugin beyond10x/agentplugins@aep-drive@ \ --plugin beyond10x/ess@ess@ \ --cwd \ diff --git a/evals/plan-critic-acceptance-verdict/case.yaml b/evals/plan-critic-acceptance-verdict/case.yaml index 507ea2a..5fd4e1a 100644 --- a/evals/plan-critic-acceptance-verdict/case.yaml +++ b/evals/plan-critic-acceptance-verdict/case.yaml @@ -51,7 +51,7 @@ subject: task: | Run the `aep:plan-critic-acceptance` agent over the stories drafted under - `epic:commercial-clients`, and judge whether every drafted acceptance statement names an observable outcome and the transition it turns on. + `epic:book-reservations`, and judge whether every drafted acceptance statement names an observable outcome and the transition it turns on. Give the critic the story ids and the epic id, and nothing else. diff --git a/evals/plan-critic-acceptance-verdict/recorded/README.md b/evals/plan-critic-acceptance-verdict/recorded/README.md index 9e975f0..641ee1c 100644 --- a/evals/plan-critic-acceptance-verdict/recorded/README.md +++ b/evals/plan-critic-acceptance-verdict/recorded/README.md @@ -13,12 +13,12 @@ up with no change to the case. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/plan-critic-acceptance-verdict \ --arm plugin \ --harness claude \ - --plugin-dir plugins/aep-plan \ - --cwd \ + --plugin-dir plugins/aep \ + --cwd \ --budget-usd 5 \ --observed-at \ --redact \ @@ -34,6 +34,8 @@ recorded beside it — a transcript with no provenance is a file, not evidence. ## What it needs in the working tree -A store holding `epic:commercial-clients` and at least two draft stories decomposed from it — the -shape [the golden path](../../../website/docs/golden-path.md) § 3 produces. With fewer than two stories +`fixtures/library-reservations-drafted`, copied out and committed as a Git repository: a store +holding `epic:book-reservations` and the six draft stories decomposed from it, recorded from [Your +first governed plan](../../../website/docs/tutorials/first-governed-plan.md) before its critics ran — +the shape [the golden path](../../../website/docs/golden-path.md) § 3 produces. With fewer than two stories the panel step is skipped and the case measures nothing. diff --git a/evals/plan-critic-design-verdict/case.yaml b/evals/plan-critic-design-verdict/case.yaml index 09ed4e4..f301292 100644 --- a/evals/plan-critic-design-verdict/case.yaml +++ b/evals/plan-critic-design-verdict/case.yaml @@ -51,7 +51,7 @@ subject: task: | Run the `aep:plan-critic-design` agent over the stories drafted under - `epic:commercial-clients`, and judge whether the drafted set is the right shape — cycles, a serialising chain, a split abstraction, an unrecorded dependency. + `epic:book-reservations`, and judge whether the drafted set is the right shape — cycles, a serialising chain, a split abstraction, an unrecorded dependency. Give the critic the story ids and the epic id, and nothing else. diff --git a/evals/plan-critic-design-verdict/recorded/README.md b/evals/plan-critic-design-verdict/recorded/README.md index ee287d5..1caa3bf 100644 --- a/evals/plan-critic-design-verdict/recorded/README.md +++ b/evals/plan-critic-design-verdict/recorded/README.md @@ -13,12 +13,12 @@ up with no change to the case. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/plan-critic-design-verdict \ --arm plugin \ --harness claude \ - --plugin-dir plugins/aep-plan \ - --cwd \ + --plugin-dir plugins/aep \ + --cwd \ --budget-usd 5 \ --observed-at \ --redact \ @@ -34,6 +34,8 @@ recorded beside it — a transcript with no provenance is a file, not evidence. ## What it needs in the working tree -A store holding `epic:commercial-clients` and at least two draft stories decomposed from it — the -shape [the golden path](../../../website/docs/golden-path.md) § 3 produces. With fewer than two stories +`fixtures/library-reservations-drafted`, copied out and committed as a Git repository: a store +holding `epic:book-reservations` and the six draft stories decomposed from it, recorded from [Your +first governed plan](../../../website/docs/tutorials/first-governed-plan.md) before its critics ran — +the shape [the golden path](../../../website/docs/golden-path.md) § 3 produces. With fewer than two stories the panel step is skipped and the case measures nothing. diff --git a/evals/plan-critic-parallel-safety-verdict/case.yaml b/evals/plan-critic-parallel-safety-verdict/case.yaml index a2ff3c7..673586e 100644 --- a/evals/plan-critic-parallel-safety-verdict/case.yaml +++ b/evals/plan-critic-parallel-safety-verdict/case.yaml @@ -51,7 +51,7 @@ subject: task: | Run the `aep:plan-critic-parallel-safety` agent over the stories drafted under - `epic:commercial-clients`, and judge which pair of drafted items lands on one surface, and whether the plan says so. + `epic:book-reservations`, and judge which pair of drafted items lands on one surface, and whether the plan says so. Give the critic the story ids and the epic id, and nothing else. diff --git a/evals/plan-critic-parallel-safety-verdict/recorded/README.md b/evals/plan-critic-parallel-safety-verdict/recorded/README.md index b5c18d7..e465b4d 100644 --- a/evals/plan-critic-parallel-safety-verdict/recorded/README.md +++ b/evals/plan-critic-parallel-safety-verdict/recorded/README.md @@ -13,12 +13,12 @@ up with no change to the case. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/plan-critic-parallel-safety-verdict \ --arm plugin \ --harness claude \ - --plugin-dir plugins/aep-plan \ - --cwd \ + --plugin-dir plugins/aep \ + --cwd \ --budget-usd 5 \ --observed-at \ --redact \ @@ -34,6 +34,8 @@ recorded beside it — a transcript with no provenance is a file, not evidence. ## What it needs in the working tree -A store holding `epic:commercial-clients` and at least two draft stories decomposed from it — the -shape [the golden path](../../../website/docs/golden-path.md) § 3 produces. With fewer than two stories +`fixtures/library-reservations-drafted`, copied out and committed as a Git repository: a store +holding `epic:book-reservations` and the six draft stories decomposed from it, recorded from [Your +first governed plan](../../../website/docs/tutorials/first-governed-plan.md) before its critics ran — +the shape [the golden path](../../../website/docs/golden-path.md) § 3 produces. With fewer than two stories the panel step is skipped and the case measures nothing. diff --git a/evals/plan-critic-scope-verdict/case.yaml b/evals/plan-critic-scope-verdict/case.yaml index 70d6fca..cbffa67 100644 --- a/evals/plan-critic-scope-verdict/case.yaml +++ b/evals/plan-critic-scope-verdict/case.yaml @@ -51,7 +51,7 @@ subject: task: | Run the `aep:plan-critic-scope` agent over the stories drafted under - `epic:commercial-clients`, and judge whether every promise the parent makes is claimed by something in the set, and whether anything in the set was not asked for. + `epic:book-reservations`, and judge whether every promise the parent makes is claimed by something in the set, and whether anything in the set was not asked for. Give the critic the story ids and the epic id, and nothing else. diff --git a/evals/plan-critic-scope-verdict/recorded/README.md b/evals/plan-critic-scope-verdict/recorded/README.md index b715be6..b26b35e 100644 --- a/evals/plan-critic-scope-verdict/recorded/README.md +++ b/evals/plan-critic-scope-verdict/recorded/README.md @@ -13,12 +13,12 @@ up with no change to the case. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/plan-critic-scope-verdict \ --arm plugin \ --harness claude \ - --plugin-dir plugins/aep-plan \ - --cwd \ + --plugin-dir plugins/aep \ + --cwd \ --budget-usd 5 \ --observed-at \ --redact \ @@ -34,6 +34,8 @@ recorded beside it — a transcript with no provenance is a file, not evidence. ## What it needs in the working tree -A store holding `epic:commercial-clients` and at least two draft stories decomposed from it — the -shape [the golden path](../../../website/docs/golden-path.md) § 3 produces. With fewer than two stories +`fixtures/library-reservations-drafted`, copied out and committed as a Git repository: a store +holding `epic:book-reservations` and the six draft stories decomposed from it, recorded from [Your +first governed plan](../../../website/docs/tutorials/first-governed-plan.md) before its critics ran — +the shape [the golden path](../../../website/docs/golden-path.md) § 3 produces. With fewer than two stories the panel step is skipped and the case measures nothing. diff --git a/evals/security-reviewer-safety-fact/recorded/README.md b/evals/security-reviewer-safety-fact/recorded/README.md index a91ff79..b459654 100644 --- a/evals/security-reviewer-safety-fact/recorded/README.md +++ b/evals/security-reviewer-safety-fact/recorded/README.md @@ -11,7 +11,7 @@ rows were fitted to, which measures the document and not the plugin. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/security-reviewer-safety-fact \ --arm plugin \ --harness claude \ diff --git a/evals/specifying-interview-headless/recorded/README.md b/evals/specifying-interview-headless/recorded/README.md index 5fbf92c..07da2e9 100644 --- a/evals/specifying-interview-headless/recorded/README.md +++ b/evals/specifying-interview-headless/recorded/README.md @@ -11,7 +11,7 @@ rows were fitted to, which measures the document and not the plugin. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/specifying-interview-headless \ --arm plugin \ --harness claude \ diff --git a/evals/story-scoper-safety-fact/recorded/README.md b/evals/story-scoper-safety-fact/recorded/README.md index a0ced7d..79112b3 100644 --- a/evals/story-scoper-safety-fact/recorded/README.md +++ b/evals/story-scoper-safety-fact/recorded/README.md @@ -11,7 +11,7 @@ rows were fitted to, which measures the document and not the plugin. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/story-scoper-safety-fact \ --arm plugin \ --harness claude \ diff --git a/evals/wave-claim-verdict/recorded/README.md b/evals/wave-claim-verdict/recorded/README.md index ff8f0f9..496f098 100644 --- a/evals/wave-claim-verdict/recorded/README.md +++ b/evals/wave-claim-verdict/recorded/README.md @@ -11,7 +11,7 @@ rows were fitted to, which measures the document and not the plugin. Live, paid, and refused without both `METAHARNESS_LIVE=1` and a cap: ```console -$ METAHARNESS_LIVE=1 aep drive eval run \ +$ METAHARNESS_LIVE=1 metaharness aep drive eval run \ --case evals/wave-claim-verdict \ --arm plugin \ --harness claude \ diff --git a/fixtures/library-reservations-drafted/.engineering/planning/epic/book-reservations.md b/fixtures/library-reservations-drafted/.engineering/planning/epic/book-reservations.md new file mode 100644 index 0000000..7232b6d --- /dev/null +++ b/fixtures/library-reservations-drafted/.engineering/planning/epic/book-reservations.md @@ -0,0 +1,82 @@ +--- +format: aep.planning-md/3 +id: epic:book-reservations +kind: epic +status: draft +title: Members can reserve a book that is on loan +summary: A book on loan can be reserved for one member; when it comes back it is held for them instead of going on the shelf. +relations: +- serves: vision:lending-library +- informed_by: executable-system-specification:library +revision: 1 +--- +# Members can reserve a book that is on loan + +## Outcome + +A librarian can reserve a book that is on loan for a member. When that book is returned it does +not go back on the shelf: it is held for the member who reserved it until they collect it, or until +a librarian releases the hold. The Go library in `impl/` does this, and the conformance suite +synthesized from the specification passes against it. + +## Model + +The behaviour is specified in ESS before any code is written around it, and the specification is +the contract every story below is held to: + +- `spec/domains/lending.yaml:19` — the `Optional -> MemberId` conversion `BookHeld` needs. +- `spec/domains/lending.yaml:44` — `Book.reserved_for_id`, with a `reserved_for` relation to `Member` + (`references`, cardinality `one`). +- `spec/domains/lending.yaml:59` — Book states become `OnShelf, OnLoan, OnLoanReserved, OnHold, + Withdrawn`, with transitions `reserve`, `cancel_reservation`, `return_to_hold`, `collect`, + `release_hold` beside the existing `lend`, `return`, `withdraw`. +- `spec/domains/lending.yaml:229` — `ReturnBook` branches on state: `returned` (OnLoan -> OnShelf, + `BookReturned`) or `held` (OnLoanReserved -> OnHold, `BookHeld` with the reserving member). +- `spec/domains/lending.yaml:305` — `ReserveBook` (new), refused with `MemberAlreadyHasBook` for the + current borrower and with `BookStateConflict` from any state but `OnLoan`. +- `spec/domains/lending.yaml:344` — `CancelReservation` (new). +- `spec/domains/lending.yaml:375` — `CollectHold` (new), refused with `BookHeldForAnotherMember` for + anyone but the reserving member. +- `spec/domains/lending.yaml:417` — `ReleaseHold` (new). +- `spec/domains/lending.yaml:507`, `:540`, `:556` — `Catalogue` gains `reserved_for_id`, + `BooksOnLoan` includes reserved loans, `BooksOnHold` (new) lists held books. +- `spec/components.yaml` — the component accepts the four new commands and publishes the four new + events. + +`ess specify validate --path spec` → `library v1 — 3 file(s), valid`. +`ess verify conform synthesize --path spec` → `55 scenario(s) (0 authored), 0 refusal(s)` (was 17). + +## Decisions (operator, 2026-09-28) + +1. One reservation per book at a time; a second is refused (`BookStateConflict` from `OnLoanReserved`). +2. A returned reserved book enters a new state, `OnHold`. +3. a. Only the reserving member can take a held book; anyone else gets `BookHeldForAnotherMember`. + b. A librarian releases a hold with `ReleaseHold`; there is no time-based expiry (the spec has no clock). +4. A reservation can be cancelled while the book is on loan (`CancelReservation`). +5. A held book cannot be withdrawn; `WithdrawBook` still acts only from `OnShelf`. +6. The Librarian actor reserves on a member's behalf; no Member actor is added. +7. Reserving a book on the shelf, or one the member already has on loan, is refused. +8. Whether the reserving member is registered is left to the implementation, as for `BorrowBook`. +9. `Catalogue` shows `reserved_for_id`; `BooksOnHold` is added; a reserved return has its own `held` + outcome emitting `BookHeld`. + +### Where the model departs from what was agreed + +Decision 3a said the reserving member *borrows* the held book. ESS refuses one command that both +branches on the book's state and guards on a stored field (`ESS-COMMAND-004`, +`spec/domains/lending.yaml:203`), so a held book is taken with a separate command, `CollectHold`. +`BorrowBook` is unchanged and answers `BookStateConflict` for a book `OnHold`, whoever asks. + +## Out of scope + +- Queues of reservations (decision 1). +- Hold expiry by time (decision 3b). +- A Member actor or member self-service (decision 6). +- A network-facing service; `impl/` stays an in-memory package. +- The registered-member check itself — `story:borrow-member-check-tested` owns that for `Borrow`. + +## Acceptance + +`go test ./...` in `impl/`, run against an `impl/essconform` regenerated from the specification +above (`ess verify conform synthesize --target go`), passes all 55 synthesized scenarios with none +skipped. diff --git a/fixtures/library-reservations-drafted/.engineering/planning/executable-system-specification/library.md b/fixtures/library-reservations-drafted/.engineering/planning/executable-system-specification/library.md new file mode 100644 index 0000000..a689b77 --- /dev/null +++ b/fixtures/library-reservations-drafted/.engineering/planning/executable-system-specification/library.md @@ -0,0 +1,39 @@ +--- +format: aep.planning-md/3 +id: executable-system-specification:library +kind: executable-system-specification +status: draft +title: library v1 — ESS specification of the lending domain +summary: spec/system.yaml (ess/15), one domain library.lending, one component library-service. +relations: +- derived_from: vision:lending-library +revision: 2 +--- +# library v1 — ESS specification of the lending domain + +## Evidence + +- spec/system.yaml:1 — `format: ess/15`; lines 2-3 `system: library`, `version: v1`; lines 5-6 the one domain, `library.lending`. +- spec/ess-inputs.yaml:1 — `format: ess-inputs/2`; line 2 `requires: ess 0.38.0`; lines 3-6 the specification is system.yaml, components.yaml and domains/lending.yaml; line 7 `scenarios: []`. +- spec/components.yaml:2 — component `library-service`, owning `library.lending` (line 6), accepting five commands (lines 8-13), publishing five events (lines 15-20), `reached_by: network` (line 21). +- spec/domains/lending.yaml:1 — domain `library.lending`: entities Book (line 22, lifecycle OnShelf/OnLoan/Withdrawn, lines 39-52) and Member (line 55), actor Librarian (line 68), errors BookStateConflict and BookNotFound (lines 79-89), commands AddBook, RegisterMember, BorrowBook, ReturnBook, WithdrawBook (lines 92-232), events (lines 234-266), views Catalogue, Members, BooksOnLoan (lines 268-314). +- impl/conformance_test.go:13 — `TestConformance` runs the synthesized `essconform` suite against the Go implementation. + +## Context + +This artifact stands for the specification that already exists and is committed in spec/; it does +not restate it. spec/system.yaml is the root document and spec/ess-inputs.yaml names the files that +make up the specification. + +Decisions recorded in the specification itself, which a reader of the implementation should know: + +- spec/domains/lending.yaml:148-150 — whether the borrowing member is registered is left to the + implementation; the specification does not state it and no scenario covers it. +- spec/domains/lending.yaml:175 — a return names only the book; who brings it back is not checked. +- spec/domains/lending.yaml:54 — members never leave (Member has the single state Active). + +Conformance: impl/essconform/ is generated by `ess verify conform synthesize --target go` and is +git-ignored (.gitignore:1); its README (impl/essconform/README.md:3) reports 17 synthesized +scenarios. `go test ./...` in impl/ passed on 2026-09-28 during this draft, but no +`ess-conformance-report/2` has been recorded as evidence against this artifact, so it stays in +`draft`; whether to validate it and record conformance is the operator's call. diff --git a/fixtures/library-reservations-drafted/.engineering/planning/story/borrow-member-check-tested.md b/fixtures/library-reservations-drafted/.engineering/planning/story/borrow-member-check-tested.md new file mode 100644 index 0000000..7097684 --- /dev/null +++ b/fixtures/library-reservations-drafted/.engineering/planning/story/borrow-member-check-tested.md @@ -0,0 +1,37 @@ +--- +format: aep.planning-md/3 +id: story:borrow-member-check-tested +kind: story +status: draft +title: The registered-member check on Borrow is exercised by a test +summary: The only caller of Library.Borrow passes requireMember=false, so ErrUnknownMember is reached by no test. +relations: +- serves: vision:lending-library +- informed_by: executable-system-specification:library +revision: 3 +--- +# The registered-member check on Borrow is exercised by a test + +## Evidence + +- spec/domains/lending.yaml:197-199 — "The borrowing member must be a registered member. ... Decided: the implementation checks it; this specification does not, and no scenario covers it." (Was :148-150 before the reservation model was added; the text is unchanged.) +- impl/library.go:38-39 — `ErrUnknownMember` "is returned when a borrow names a member nobody registered." +- impl/library.go:82-85 — `Borrow(bookID, memberID string, requireMember bool)`; "the conformance target turns it off." +- impl/library.go:93-97 — the check itself, taken only when `requireMember` is true. +- impl/conformance_test.go:88-90 — the conformance target calls `t.lib.Borrow(bookID, memberID, false)`: "the suite borrows for members it never registered, so it is off here." + +## Context + +The specification hands the registered-member rule to the implementation and says no scenario +covers it, so the conformance suite cannot be the thing that checks it — and the target turns it +off. impl/conformance_test.go is the only test file in the repository and the only caller of +`Borrow` in the tree, so the branch at impl/library.go:93-97 and `ErrUnknownMember` are executed by +nothing. A regression there (the check removed, inverted, or run after the state change) would pass +every test in the repository. Recorded as of the single commit 30e5ba2 (2026-09-28); there is no +older history to date it by. + +## Acceptance + +A Go test in impl/ borrows a shelved book for an unregistered member with `requireMember` true, +receives `ErrUnknownMember`, and observes the book still `OnShelf` with no borrower — and `go test +./...` runs it. diff --git a/fixtures/library-reservations-drafted/.engineering/planning/story/cancel-reservation.md b/fixtures/library-reservations-drafted/.engineering/planning/story/cancel-reservation.md new file mode 100644 index 0000000..e5a01d9 --- /dev/null +++ b/fixtures/library-reservations-drafted/.engineering/planning/story/cancel-reservation.md @@ -0,0 +1,56 @@ +--- +format: aep.planning-md/3 +id: story:cancel-reservation +kind: story +status: draft +title: A librarian cancels a reservation while the book is still on loan +summary: 'Library cancel operation and the CancelReservation command: OnLoanReserved goes back to OnLoan with reserved_for_id cleared; every other state is refused.' +relations: +- decomposes: epic:book-reservations +- informed_by: executable-system-specification:library +- depends_on: story:reserve-book-on-loan +- depends_on: story:return-reserved-book-to-hold +scope: +- confidence: cited + path: impl/conformance_test.go +- confidence: cited + path: impl/library.go +revision: 2 +--- +# A librarian cancels a reservation while the book is still on loan + +## Context + +Epic decision 4: a reservation can be cancelled while the book is on loan. Once the book is on hold +the reservation is ended by `CollectHold` or `ReleaseHold`, not by `CancelReservation`, which refuses +there. + +Specification it is held to: + +- `spec/domains/lending.yaml:71-73` — transition `cancel_reservation`, `OnLoanReserved -> OnLoan`. +- `spec/domains/lending.yaml:344-372` — `CancelReservation`: `cancelled` (clears `reserved_for_id`, + emits `ReservationCancelled`), `wrong-state` (`BookStateConflict` from any state but + `OnLoanReserved`), `no-such-book`. +- `spec/domains/lending.yaml:488-491` — event `ReservationCancelled { book_id }`. +- `spec/components.yaml:15`, `:26` — the component accepts `CancelReservation` and publishes `ReservationCancelled`. + +It depends on `story:reserve-book-on-loan` because `cancelled` needs a reserved book, and on +`story:return-reserved-book-to-hold` because one of its refusals (`OnHold`) needs a held book. + +## Domain relations + +- `Book → Member` via `reserved_for`: many-to-one, `references`; cleared by `cancel_reservation`, + the borrower keeping the book — inferable from `spec/domains/lending.yaml`, entity + `library.lending.Book`, relation `reserved_for` (lines 52-56), with the lifecycle at lines 57-85. + +## Scope + +- `impl/library.go` — a cancel-reservation method. +- `impl/conformance_test.go` — `ExecuteCommand` case for `library.lending.CancelReservation`. +- `impl/essconform/` — regenerated locally if absent (git-ignored), not committed. + +Shares both Go files with every other story decomposing `epic:book-reservations`; not parallel-safe with any of them. + +## Acceptance + +With `impl/essconform` regenerated from `spec/`, `go test -v ./...` in `impl/` reports PASS (not SKIP) for the 7 scenarios `CancelReservation/outcome/{cancelled,no-such-book}`, `Book/transition/cancel_reservation/by/library.lending.CancelReservation/cancelled` and `Book/state/{OnShelf,OnLoan,OnHold,Withdrawn}/refuses/CancelReservation`, and fails no scenario. diff --git a/fixtures/library-reservations-drafted/.engineering/planning/story/collect-held-book.md b/fixtures/library-reservations-drafted/.engineering/planning/story/collect-held-book.md new file mode 100644 index 0000000..00f7113 --- /dev/null +++ b/fixtures/library-reservations-drafted/.engineering/planning/story/collect-held-book.md @@ -0,0 +1,63 @@ +--- +format: aep.planning-md/3 +id: story:collect-held-book +kind: story +status: draft +title: The member a book is held for collects it +summary: 'Library collect operation and the CollectHold command: OnHold goes to OnLoan for the reserving member with BookBorrowed; anyone else gets BookHeldForAnotherMember.' +relations: +- decomposes: epic:book-reservations +- informed_by: executable-system-specification:library +- depends_on: story:return-reserved-book-to-hold +scope: +- confidence: cited + path: impl/conformance_test.go +- confidence: cited + path: impl/library.go +revision: 2 +--- +# The member a book is held for collects it + +## Context + +Epic decision 3a, as modelled: only the reserving member can take a held book, and anyone else is +refused with `BookHeldForAnotherMember`. The epic's "Where the model departs" section explains why +this is a separate command, `CollectHold`, rather than `BorrowBook` — `BorrowBook` is unchanged and +keeps answering `BookStateConflict` for a book on hold (`spec/domains/lending.yaml:197-203`). + +Specification it is held to: + +- `spec/domains/lending.yaml:77-79` — transition `collect`, `OnHold -> OnLoan`. +- `spec/domains/lending.yaml:374-413` — `CollectHold`: `held-for-another` + (`BookHeldForAnotherMember` when `reserved_for_id != input.member_id`), `collected` (sets + `borrower_id`, clears `reserved_for_id`, emits `BookBorrowed`), `wrong-state` (`BookStateConflict` + from any state but `OnHold`), `no-such-book`. +- `spec/domains/lending.yaml:128-132` — error `BookHeldForAnotherMember`. +- `spec/domains/lending.yaml:464-469` — event `BookBorrowed { book_id, member_id }` (reused). +- `spec/components.yaml:16` — the component accepts `CollectHold`. + +The synthesized scenarios answer `wrong-state`, not `held-for-another`, for a book on the shelf, +on loan, reserved or withdrawn, so the state check comes before the member guard; read the outcome +order at `spec/domains/lending.yaml:384-413` against those scenarios. + +## Domain relations + +- `Book → Member` via `reserved_for`: many-to-one, `references`; compared with the collecting member, + then cleared by `collect` — inferable from `spec/domains/lending.yaml`, entity + `library.lending.Book`, relation `reserved_for` (lines 52-56). +- `Book → Member` via `borrower`: many-to-one, optional, `references`; set to the collecting member — + inferable from `spec/domains/lending.yaml`, entity `library.lending.Book`, relation `borrower` (lines 47-51). + +## Scope + +- `impl/library.go` — a collect method and an exported error for "held for another member". +- `impl/conformance_test.go` — `ExecuteCommand` case for `library.lending.CollectHold`; the new + error maps to outcome `held-for-another`, error `library.lending.BookHeldForAnotherMember`. +- `impl/essconform/` — regenerated locally if absent (git-ignored), not committed. + +Shares both Go files with every other story decomposing `epic:book-reservations`, including its +sibling `story:release-hold` which depends on the same story; the two are not parallel-safe. + +## Acceptance + +With `impl/essconform` regenerated from `spec/`, `go test -v ./...` in `impl/` reports PASS (not SKIP) for the 8 scenarios `CollectHold/outcome/{collected,held-for-another,no-such-book}`, `Book/transition/collect/by/library.lending.CollectHold/collected` and `Book/state/{OnShelf,OnLoan,OnLoanReserved,Withdrawn}/refuses/CollectHold`, and fails no scenario. diff --git a/fixtures/library-reservations-drafted/.engineering/planning/story/release-hold.md b/fixtures/library-reservations-drafted/.engineering/planning/story/release-hold.md new file mode 100644 index 0000000..667239c --- /dev/null +++ b/fixtures/library-reservations-drafted/.engineering/planning/story/release-hold.md @@ -0,0 +1,51 @@ +--- +format: aep.planning-md/3 +id: story:release-hold +kind: story +status: draft +title: A librarian releases a hold and the book goes back on the shelf +summary: 'Library release operation and the ReleaseHold command: OnHold goes to OnShelf with reserved_for_id cleared and HoldReleased emitted; every other state is refused.' +relations: +- decomposes: epic:book-reservations +- informed_by: executable-system-specification:library +- depends_on: story:return-reserved-book-to-hold +scope: +- confidence: cited + path: impl/conformance_test.go +- confidence: cited + path: impl/library.go +revision: 2 +--- +# A librarian releases a hold and the book goes back on the shelf + +## Context + +Epic decision 3b: a hold ends when a librarian releases it with `ReleaseHold`; there is no time-based +expiry (the specification has no clock, `spec/domains/lending.yaml:415-416`). + +Specification it is held to: + +- `spec/domains/lending.yaml:80-82` — transition `release_hold`, `OnHold -> OnShelf`. +- `spec/domains/lending.yaml:417-445` — `ReleaseHold`: `released` (clears `reserved_for_id`, emits + `HoldReleased`), `wrong-state` (`BookStateConflict` from any state but `OnHold`), `no-such-book`. +- `spec/domains/lending.yaml:500-503` — event `HoldReleased { book_id }`. +- `spec/components.yaml:17`, `:28` — the component accepts `ReleaseHold` and publishes `HoldReleased`. + +## Domain relations + +- `Book → Member` via `reserved_for`: many-to-one, `references`; cleared by `release_hold`, leaving + the book held for nobody — inferable from `spec/domains/lending.yaml`, entity + `library.lending.Book`, relation `reserved_for` (lines 52-56). + +## Scope + +- `impl/library.go` — a release-hold method. +- `impl/conformance_test.go` — `ExecuteCommand` case for `library.lending.ReleaseHold`. +- `impl/essconform/` — regenerated locally if absent (git-ignored), not committed. + +Shares both Go files with every other story decomposing `epic:book-reservations`, including its +sibling `story:collect-held-book` which depends on the same story; the two are not parallel-safe. + +## Acceptance + +With `impl/essconform` regenerated from `spec/`, `go test -v ./...` in `impl/` reports PASS (not SKIP) for the 7 scenarios `ReleaseHold/outcome/{released,no-such-book}`, `Book/transition/release_hold/by/library.lending.ReleaseHold/released` and `Book/state/{OnShelf,OnLoan,OnLoanReserved,Withdrawn}/refuses/ReleaseHold`, and fails no scenario. diff --git a/fixtures/library-reservations-drafted/.engineering/planning/story/reservations-suite-baseline.md b/fixtures/library-reservations-drafted/.engineering/planning/story/reservations-suite-baseline.md new file mode 100644 index 0000000..4c6f91f --- /dev/null +++ b/fixtures/library-reservations-drafted/.engineering/planning/story/reservations-suite-baseline.md @@ -0,0 +1,65 @@ +--- +format: aep.planning-md/3 +id: story:reservations-suite-baseline +kind: story +status: draft +title: Existing lending behaviour passes the regenerated reservations suite +summary: Regenerate impl/essconform from the reservations spec and extend the Book model and the three book views so the 17 pre-existing scenarios pass against it. +relations: +- decomposes: epic:book-reservations +- informed_by: executable-system-specification:library +scope: +- confidence: cited + path: impl/conformance_test.go +- confidence: cited + path: impl/library.go +revision: 2 +--- +# Existing lending behaviour passes the regenerated reservations suite + +## Context + +The specification now models reservations, and a suite synthesized from it (55 scenarios) no longer +matches the Go library: every Catalogue row it checks carries `reserved_for_id`, most scenarios also +query `BooksOnHold`, and `BooksOnLoan` is filtered on two states. Until the target answers those +views, even the 17 scenarios that exercise only the old commands cannot pass. This story is the +prefactoring every other reservations story builds on: it regenerates the suite, gives `Book` the +new states and the `reserved_for_id` field, and answers the three book views as specified. It adds +no new command; the four new commands keep answering `ErrUnsupported`, so their scenarios are +reported skipped, which `go test` does not fail. + +Specification it is held to: + +- `spec/domains/lending.yaml:25-56` — `Book` gains `reserved_for_id: Optional`. +- `spec/domains/lending.yaml:57-60` — Book states are `OnShelf, OnLoan, OnLoanReserved, OnHold, Withdrawn`. +- `spec/domains/lending.yaml:505-525` — `Catalogue` has a `reserved_for_id` field. +- `spec/domains/lending.yaml:539-553` — `BooksOnLoan` filters `state == OnLoan` or `state == OnLoanReserved`. +- `spec/domains/lending.yaml:555-569` — `BooksOnHold` (new): `book_id`, `title`, `reserved_for_id`, filtered `state == OnHold`. + +The suite is regenerated with `ess verify conform synthesize --path spec --target go --out impl/essconform` +(run from the repository root; see `impl/essconform/README.md`), which must report +`55 scenario(s) (0 authored), 0 refusal(s)`. `impl/essconform/` is git-ignored: regenerating it is a +local precondition for this and every later reservations story, not a committed change. + +## Domain relations + +- `Book → Member` via `reserved_for`: many-to-one, zero or one member per book, `references` (no + ownership; a Member is never removed, `spec/domains/lending.yaml:87`) — inferable from + `spec/domains/lending.yaml`, entity `library.lending.Book`, relation `reserved_for` (lines 52-56). + This story only stores and exposes the field; nothing sets it yet. +- `Book → Member` via `borrower`: many-to-one, optional, `references` — inferable from + `spec/domains/lending.yaml`, entity `library.lending.Book`, relation `borrower` (lines 47-51). Unchanged. + +## Scope + +- `impl/library.go` — add the `OnLoanReserved` and `OnHold` `BookState` constants and a + `ReservedFor *string` (or equivalent) on `Book`. No behaviour of `Borrow`, `Return` or `Withdraw` changes. +- `impl/conformance_test.go` — `QueryView`: `Catalogue` rows add `reserved_for_id`; `BooksOnLoan` + includes `OnLoanReserved` books; new `BooksOnHold` case. +- `impl/essconform/` — regenerated locally (git-ignored), not committed. + +Shares both Go files with every other story decomposing `epic:book-reservations`; not parallel-safe with any of them. + +## Acceptance + +After regenerating `impl/essconform` from `spec/`, `go test -v ./...` in `impl/` reports PASS (not SKIP) for all 17 scenarios that use only `AddBook`, `RegisterMember`, `BorrowBook`, `ReturnBook` and `WithdrawBook` — `AddBook/outcome/added`, `RegisterMember/outcome/registered`, `BorrowBook/outcome/{borrowed,no-such-book}`, `ReturnBook/outcome/{returned,no-such-book}`, `WithdrawBook/outcome/{withdrawn,no-such-book}`, `Book/transition/{lend,return,withdraw}/…`, `Book/state/OnLoan/refuses/{BorrowBook,WithdrawBook}`, `Book/state/OnShelf/refuses/ReturnBook` and `Book/state/Withdrawn/refuses/{BorrowBook,ReturnBook,WithdrawBook}` — and fails none. diff --git a/fixtures/library-reservations-drafted/.engineering/planning/story/reserve-book-on-loan.md b/fixtures/library-reservations-drafted/.engineering/planning/story/reserve-book-on-loan.md new file mode 100644 index 0000000..9ace4de --- /dev/null +++ b/fixtures/library-reservations-drafted/.engineering/planning/story/reserve-book-on-loan.md @@ -0,0 +1,70 @@ +--- +format: aep.planning-md/3 +id: story:reserve-book-on-loan +kind: story +status: draft +title: A librarian reserves a book on loan for a member +summary: 'Library.Reserve and the ReserveBook command: a book OnLoan moves to OnLoanReserved with reserved_for_id set; the borrower and every other state are refused.' +relations: +- decomposes: epic:book-reservations +- informed_by: executable-system-specification:library +- depends_on: story:reservations-suite-baseline +scope: +- confidence: cited + path: impl/conformance_test.go +- confidence: cited + path: impl/library.go +revision: 2 +--- +# A librarian reserves a book on loan for a member + +## Context + +Epic decisions 1, 6 and 7: one reservation per book, made by the Librarian on a member's behalf, +and only for a book that is on loan to somebody else. The library gains a reserve operation and the +conformance target a `library.lending.ReserveBook` case. + +Specification it is held to: + +- `spec/domains/lending.yaml:57-73` — transition `reserve`, `OnLoan -> OnLoanReserved`. +- `spec/domains/lending.yaml:302-342` — `ReserveBook`: `already-borrower` (`MemberAlreadyHasBook` + when `borrower_id == input.member_id`), `reserved` (sets `reserved_for_id`, emits `BookReserved`), + `wrong-state` (`BookStateConflict` from any state but `OnLoan`), `no-such-book` (`BookNotFound`). +- `spec/domains/lending.yaml:134-138` — error `MemberAlreadyHasBook`. +- `spec/domains/lending.yaml:481-486` — event `BookReserved { book_id, member_id }`. +- `spec/components.yaml:14`, `:25` — the component accepts `ReserveBook` and publishes `BookReserved`. + +The synthesized scenarios show a state conflict answered before the borrower guard (a book on the +shelf or already reserved answers `wrong-state`), so the implementor should read the outcome order +at `spec/domains/lending.yaml:314-342` against those scenarios rather than assume it. + +`Borrow` and `Withdraw` already refuse any state but `OnShelf`, so the `OnLoanReserved` refusals of +`BorrowBook` and `WithdrawBook` should need no change beyond being reachable now. + +Whether the reserving member is a registered member is not checked here: the specification leaves it +to the implementation (`spec/domains/lending.yaml:302-304`) and the epic puts the registered-member +check out of scope. This story adds no such check. + +## Domain relations + +- `Book → Member` via `reserved_for`: many-to-one, zero or one member per book, `references`, set by + `reserve` and held until the reservation is cancelled, collected or released — inferable from + `spec/domains/lending.yaml`, entity `library.lending.Book`, relation `reserved_for` (lines 52-56), + with the lifecycle at lines 26-32 and 57-85. +- `Book → Member` via `borrower`: many-to-one, optional, `references` — inferable from + `spec/domains/lending.yaml`, entity `library.lending.Book`, relation `borrower` (lines 47-51); + read here to refuse the current borrower. + +## Scope + +- `impl/library.go` — a reserve method and an exported error for "member already has this book". +- `impl/conformance_test.go` — `ExecuteCommand` case for `library.lending.ReserveBook`; `refusal` + (or its replacement) maps the new error to outcome `already-borrower`, error + `library.lending.MemberAlreadyHasBook`. +- `impl/essconform/` — regenerated locally if absent (git-ignored), not committed. + +Shares both Go files with every other story decomposing `epic:book-reservations`; not parallel-safe with any of them. + +## Acceptance + +With `impl/essconform` regenerated from `spec/`, `go test -v ./...` in `impl/` reports PASS (not SKIP) for the 9 scenarios `ReserveBook/outcome/{reserved,already-borrower,no-such-book}`, `Book/transition/reserve/by/library.lending.ReserveBook/reserved`, `Book/state/OnShelf/refuses/ReserveBook`, `Book/state/Withdrawn/refuses/ReserveBook` and `Book/state/OnLoanReserved/refuses/{ReserveBook,BorrowBook,WithdrawBook}`, and fails no scenario. diff --git a/fixtures/library-reservations-drafted/.engineering/planning/story/return-reserved-book-to-hold.md b/fixtures/library-reservations-drafted/.engineering/planning/story/return-reserved-book-to-hold.md new file mode 100644 index 0000000..fa9840d --- /dev/null +++ b/fixtures/library-reservations-drafted/.engineering/planning/story/return-reserved-book-to-hold.md @@ -0,0 +1,61 @@ +--- +format: aep.planning-md/3 +id: story:return-reserved-book-to-hold +kind: story +status: draft +title: A returned reserved book is held for the member who reserved it +summary: 'Library.Return branches on state: OnLoan goes to OnShelf as before, OnLoanReserved goes to OnHold and the ReturnBook command answers held with BookHeld naming the reserving member.' +relations: +- decomposes: epic:book-reservations +- informed_by: executable-system-specification:library +- depends_on: story:reserve-book-on-loan +scope: +- confidence: cited + path: impl/conformance_test.go +- confidence: cited + path: impl/library.go +revision: 2 +--- +# A returned reserved book is held for the member who reserved it + +## Context + +Epic decisions 2 and 9: a returned reserved book does not go back on the shelf; it enters `OnHold`, +and the return has its own `held` outcome emitting `BookHeld`. Decision 5: a held book cannot be +withdrawn (`WithdrawBook` still acts only from `OnShelf`). + +Specification it is held to: + +- `spec/domains/lending.yaml:74-76` — transition `return_to_hold`, `OnLoanReserved -> OnHold`. +- `spec/domains/lending.yaml:228-272` — `ReturnBook`: `returned` when `OnLoan` (unchanged), `held` + when `OnLoanReserved` (clears `borrower_id`, emits `BookHeld` with `member_id` taken from the + book's `reserved_for_id`), `wrong-state` otherwise, `no-such-book`. +- `spec/domains/lending.yaml:19-23` — the `Optional -> MemberId` conversion that makes + `BookHeld.member_id` always present. +- `spec/domains/lending.yaml:493-498` — event `BookHeld { book_id, member_id }`. +- `spec/domains/lending.yaml:555-569` — `BooksOnHold` now has rows to show. +- `spec/components.yaml:27` — the component publishes `BookHeld`. + +`reserved_for_id` stays set in `OnHold`; only `borrower_id` is cleared. + +## Domain relations + +- `Book → Member` via `reserved_for`: many-to-one, `references`; survives the return into `OnHold` + and names the member in `BookHeld` — inferable from `spec/domains/lending.yaml`, entity + `library.lending.Book`, relation `reserved_for` (lines 52-56), with the conversion at lines 19-23. +- `Book → Member` via `borrower`: many-to-one, optional, `references`; cleared on return — inferable + from `spec/domains/lending.yaml`, entity `library.lending.Book`, relation `borrower` (lines 47-51). + +## Scope + +- `impl/library.go` — `Return` accepts `OnLoanReserved` as well as `OnLoan` and reports which way + the book went (and, for a hold, for whom), without changing the `OnLoan -> OnShelf` path. +- `impl/conformance_test.go` — the `library.lending.ReturnBook` case answers `held` with a + `library.lending.BookHeld` event when the book went on hold. +- `impl/essconform/` — regenerated locally if absent (git-ignored), not committed. + +Shares both Go files with every other story decomposing `epic:book-reservations`; not parallel-safe with any of them. + +## Acceptance + +With `impl/essconform` regenerated from `spec/`, `go test -v ./...` in `impl/` reports PASS (not SKIP) for the 7 scenarios `ReturnBook/outcome/held`, `ReturnBook/outcome/wrong-state`, `Book/transition/return_to_hold/by/library.lending.ReturnBook/held` and `Book/state/OnHold/refuses/{BorrowBook,ReserveBook,ReturnBook,WithdrawBook}`, and fails no scenario. diff --git a/fixtures/library-reservations-drafted/.engineering/planning/vision/lending-library.md b/fixtures/library-reservations-drafted/.engineering/planning/vision/lending-library.md new file mode 100644 index 0000000..fa7ae4d --- /dev/null +++ b/fixtures/library-reservations-drafted/.engineering/planning/vision/lending-library.md @@ -0,0 +1,29 @@ +--- +format: aep.planning-md/3 +id: vision:lending-library +kind: vision +status: draft +title: A small lending library, specified in ESS and implemented in Go +summary: Books are added, members registered, and a member borrows a book and returns it. +revision: 2 +--- +# A small lending library, specified in ESS and implemented in Go + +## Evidence + +- spec/domains/lending.yaml:3 — "A small lending library. Books are added to the collection, members are registered, and a member borrows a book and later returns it." +- spec/components.yaml:3 — the single component `library-service` "Holds the collection and the members, and lends books to members." +- impl/library.go:1 — "Package library is a minimal in-memory lending library: books are added and withdrawn, members are registered, and a member borrows a book and returns it." +- spec/system.yaml:2 — `system: library`, version v1, one domain `library.lending`. + +## Context + +The repository has no README of its own (the only README the scan found, impl/essconform/README.md, +is generated and git-ignored), so the statement of purpose is taken from the specification's domain +summary and the Go package comment, which agree. The system is one domain, one component and one +actor (Librarian, spec/domains/lending.yaml:68): add books, register members, borrow, return and +withdraw books, and read three views (Catalogue, Members, BooksOnLoan). The ESS specification in +spec/ is the contract; impl/ is a Go implementation checked against it by a generated conformance +suite. + +Nothing in the tree states goals beyond this: no roadmap, no stages, no non-goals. diff --git a/fixtures/library-reservations-drafted/.engineering/project.yaml b/fixtures/library-reservations-drafted/.engineering/project.yaml new file mode 100644 index 0000000..f6234c2 --- /dev/null +++ b/fixtures/library-reservations-drafted/.engineering/project.yaml @@ -0,0 +1,11 @@ +version: aep.project/5 + +# Written by `aep plan reverse init`. It points; it does not duplicate — a rule restated +# here would be a second copy with no way to say which one is in force. Principles and +# profiles of this project's own go under `principles/` and `profiles/` beside this file. +protocol: adp/1 +profile: development.standard +protocols: git+https://github.com/beyond10x/aep#58433bd85a1ccf939566c53d5543df86c3852b19 +planning_scope: "library" +store: + git: {} diff --git a/fixtures/library-reservations-drafted/.gitignore b/fixtures/library-reservations-drafted/.gitignore new file mode 100644 index 0000000..643fc95 --- /dev/null +++ b/fixtures/library-reservations-drafted/.gitignore @@ -0,0 +1,3 @@ +impl/essconform/ +impl/.ess-output/ +.engineering/drafts/ diff --git a/fixtures/library-reservations-drafted/impl/conformance_test.go b/fixtures/library-reservations-drafted/impl/conformance_test.go new file mode 100644 index 0000000..2242c10 --- /dev/null +++ b/fixtures/library-reservations-drafted/impl/conformance_test.go @@ -0,0 +1,191 @@ +package library_test + +import ( + "errors" + "fmt" + "os" + "testing" + + "example.com/library" + "example.com/library/essconform" +) + +func TestConformance(t *testing.T) { + // The suite runs only once a report format is chosen; choose it here so plain `go test ./...` + // runs it. ESS_REPORT_OUT, when set, still decides where the report goes. + if os.Getenv("ESS_REPORT_FORMAT") == "" { + t.Setenv("ESS_REPORT_FORMAT", "2") + } + essconform.Run(t, func() essconform.Target { return newTarget() }) +} + +// target drives one in-memory Library through the suite's Target interface. +type target struct { + lib *library.Library +} + +func newTarget() *target { return &target{lib: library.New()} } + +func (t *target) Identity() (essconform.Identity, error) { + return essconform.Identity{Name: "example.com/library", Version: "v1"}, nil +} + +func (t *target) BeginScenario(essconform.ScenarioContext) error { return nil } +func (t *target) EndScenario(essconform.ScenarioContext) error { return nil } + +func (t *target) ExecuteCommand(req essconform.CommandRequest) (essconform.CommandResult, error) { + in := func(name string) (string, error) { + v, ok := req.Input[name].(string) + if !ok { + return "", fmt.Errorf("%s: input %q is not text", req.Command, name) + } + return v, nil + } + event := func(name string, payload map[string]essconform.Node) []essconform.ObservedEvent { + return []essconform.ObservedEvent{{Event: name, Payload: payload}} + } + + switch req.Command { + case "library.lending.AddBook": + title, err := in("title") + if err != nil { + return essconform.CommandResult{}, err + } + author, err := in("author") + if err != nil { + return essconform.CommandResult{}, err + } + b := t.lib.AddBook(title, author) + return essconform.CommandResult{ + Outcome: "added", + DirectEvents: event("library.lending.BookAdded", map[string]essconform.Node{ + "book_id": b.ID, "title": b.Title, "author": b.Author, + }), + }, nil + + case "library.lending.RegisterMember": + name, err := in("name") + if err != nil { + return essconform.CommandResult{}, err + } + m := t.lib.RegisterMember(name) + return essconform.CommandResult{ + Outcome: "registered", + DirectEvents: event("library.lending.MemberRegistered", map[string]essconform.Node{ + "member_id": m.ID, "name": m.Name, + }), + }, nil + + case "library.lending.BorrowBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + memberID, err := in("member_id") + if err != nil { + return essconform.CommandResult{}, err + } + // The member check is the implementation's own rule, outside the specification; the + // suite borrows for members it never registered, so it is off here. + if err := t.lib.Borrow(bookID, memberID, false); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "borrowed", + DirectEvents: event("library.lending.BookBorrowed", map[string]essconform.Node{ + "book_id": bookID, "member_id": memberID, + }), + }, nil + + case "library.lending.ReturnBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + if err := t.lib.Return(bookID); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "returned", + DirectEvents: event("library.lending.BookReturned", map[string]essconform.Node{"book_id": bookID}), + }, nil + + case "library.lending.WithdrawBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + if err := t.lib.Withdraw(bookID); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "withdrawn", + DirectEvents: event("library.lending.BookWithdrawn", map[string]essconform.Node{"book_id": bookID}), + }, nil + } + return essconform.CommandResult{}, fmt.Errorf("unknown command %q: %w", req.Command, essconform.ErrUnsupported) +} + +// refusal maps a library error onto the declared outcome and error. +func refusal(err error) (essconform.CommandResult, error) { + var conflict *library.StateConflictError + switch { + case errors.As(err, &conflict): + return essconform.CommandResult{Outcome: "wrong-state", Error: "library.lending.BookStateConflict"}, nil + case errors.Is(err, library.ErrBookNotFound): + return essconform.CommandResult{Outcome: "no-such-book", Error: "library.lending.BookNotFound"}, nil + } + return essconform.CommandResult{}, err +} + +func (t *target) QueryView(req essconform.ViewRequest) (essconform.ViewResult, error) { + var rows []essconform.Row + switch req.View { + case "library.lending.Catalogue": + for _, b := range t.lib.Books() { + rows = append(rows, essconform.Row{ + "book_id": b.ID, "title": b.Title, "author": b.Author, + "state": string(b.State), "borrower_id": borrower(b), + }) + } + case "library.lending.BooksOnLoan": + for _, b := range t.lib.Books() { + if b.State == library.OnLoan { + rows = append(rows, essconform.Row{ + "book_id": b.ID, "title": b.Title, "borrower_id": borrower(b), + }) + } + } + case "library.lending.Members": + for _, m := range t.lib.Members() { + rows = append(rows, essconform.Row{"member_id": m.ID, "name": m.Name}) + } + default: + return essconform.ViewResult{}, fmt.Errorf("unknown view %q: %w", req.View, essconform.ErrUnsupported) + } + return essconform.ViewResult{Rows: rows}, nil +} + +func borrower(b library.Book) essconform.Node { + if b.Borrower == nil { + return nil + } + return *b.Borrower +} + +// Every event is returned directly by the command that emits it; there is nothing to observe apart. +func (t *target) ObserveEvents(essconform.EventObservationRequest) ([]essconform.ObservedEvent, error) { + return nil, essconform.ErrUnsupported +} + +func (t *target) ConfigureExternalOutcome(essconform.ExternalOutcomeControl) error { + return essconform.ErrUnsupported +} + +func (t *target) RedeliverEvent(essconform.RedeliveryRequest) error { + return essconform.ErrUnsupported +} + +func (t *target) ObserveInvocations(essconform.InvocationObservationRequest) ([]essconform.Invocation, error) { + return nil, essconform.ErrUnsupported +} diff --git a/fixtures/library-reservations-drafted/impl/go.mod b/fixtures/library-reservations-drafted/impl/go.mod new file mode 100644 index 0000000..4b2c9c4 --- /dev/null +++ b/fixtures/library-reservations-drafted/impl/go.mod @@ -0,0 +1,3 @@ +module example.com/library + +go 1.23 diff --git a/fixtures/library-reservations-drafted/impl/library.go b/fixtures/library-reservations-drafted/impl/library.go new file mode 100644 index 0000000..db6e0a3 --- /dev/null +++ b/fixtures/library-reservations-drafted/impl/library.go @@ -0,0 +1,156 @@ +// Package library is a minimal in-memory lending library: books are added and withdrawn, members +// are registered, and a member borrows a book and returns it. +package library + +import ( + "crypto/rand" + "errors" + "fmt" +) + +// BookState is where a book is in its lifecycle. +type BookState string + +const ( + OnShelf BookState = "OnShelf" + OnLoan BookState = "OnLoan" + Withdrawn BookState = "Withdrawn" +) + +// Book is one physical book. Borrower is set only while the book is OnLoan. +type Book struct { + ID string + Title string + Author string + State BookState + Borrower *string +} + +// Member is a registered member. +type Member struct { + ID string + Name string +} + +// ErrBookNotFound is returned when no book has the given identity. +var ErrBookNotFound = errors.New("book not found") + +// ErrUnknownMember is returned when a borrow names a member nobody registered. +var ErrUnknownMember = errors.New("member not registered") + +// StateConflictError is returned when a book is not in a state the operation acts from. +type StateConflictError struct { + State BookState +} + +func (e *StateConflictError) Error() string { + return fmt.Sprintf("book is %s", e.State) +} + +// Library holds the collection and the members in memory. It is not safe for concurrent use. +type Library struct { + books map[string]*Book + members map[string]*Member + // order keeps listings stable in insertion order. + bookOrder []string + memberOrder []string +} + +// New returns an empty library. +func New() *Library { + return &Library{books: map[string]*Book{}, members: map[string]*Member{}} +} + +// AddBook puts a new book on the shelf. +func (l *Library) AddBook(title, author string) Book { + b := &Book{ID: newID(), Title: title, Author: author, State: OnShelf} + l.books[b.ID] = b + l.bookOrder = append(l.bookOrder, b.ID) + return *b +} + +// RegisterMember registers a new member. +func (l *Library) RegisterMember(name string) Member { + m := &Member{ID: newID(), Name: name} + l.members[m.ID] = m + l.memberOrder = append(l.memberOrder, m.ID) + return *m +} + +// Borrow lends a book on the shelf to a member. +// +// requireMember makes an unregistered member a refusal. The specification leaves that check to the +// implementation and its conformance suite borrows for members it never registered, so the +// conformance target turns it off. +func (l *Library) Borrow(bookID, memberID string, requireMember bool) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnShelf { + return &StateConflictError{State: b.State} + } + if requireMember { + if _, ok := l.members[memberID]; !ok { + return ErrUnknownMember + } + } + b.State = OnLoan + b.Borrower = &memberID + return nil +} + +// Return puts a book on loan back on the shelf. +func (l *Library) Return(bookID string) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnLoan { + return &StateConflictError{State: b.State} + } + b.State = OnShelf + b.Borrower = nil + return nil +} + +// Withdraw takes a book on the shelf out of the collection for good. +func (l *Library) Withdraw(bookID string) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnShelf { + return &StateConflictError{State: b.State} + } + b.State = Withdrawn + return nil +} + +// Books lists every book, in every state. +func (l *Library) Books() []Book { + out := make([]Book, 0, len(l.bookOrder)) + for _, id := range l.bookOrder { + out = append(out, *l.books[id]) + } + return out +} + +// Members lists every member. +func (l *Library) Members() []Member { + out := make([]Member, 0, len(l.memberOrder)) + for _, id := range l.memberOrder { + out = append(out, *l.members[id]) + } + return out +} + +func newID() string { + var b [16]byte + if _, err := rand.Read(b[:]); err != nil { + panic(err) + } + b[6] = b[6]&0x0f | 0x40 + b[8] = b[8]&0x3f | 0x80 + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) +} diff --git a/fixtures/library-reservations-drafted/spec/components.yaml b/fixtures/library-reservations-drafted/spec/components.yaml new file mode 100644 index 0000000..d5182f6 --- /dev/null +++ b/fixtures/library-reservations-drafted/spec/components.yaml @@ -0,0 +1,29 @@ +components: + - component: library-service + summary: Holds the collection and the members, and lends books to members. + owns: + domains: + - library.lending + accepts: + commands: + - library.lending.AddBook + - library.lending.RegisterMember + - library.lending.BorrowBook + - library.lending.ReturnBook + - library.lending.WithdrawBook + - library.lending.ReserveBook + - library.lending.CancelReservation + - library.lending.CollectHold + - library.lending.ReleaseHold + publishes: + events: + - library.lending.BookAdded + - library.lending.MemberRegistered + - library.lending.BookBorrowed + - library.lending.BookReturned + - library.lending.BookWithdrawn + - library.lending.BookReserved + - library.lending.ReservationCancelled + - library.lending.BookHeld + - library.lending.HoldReleased + reached_by: network diff --git a/fixtures/library-reservations-drafted/spec/domains/lending.yaml b/fixtures/library-reservations-drafted/spec/domains/lending.yaml new file mode 100644 index 0000000..fa93b83 --- /dev/null +++ b/fixtures/library-reservations-drafted/spec/domains/lending.yaml @@ -0,0 +1,569 @@ +domain: library.lending + +summary: A small lending library. Books are added to the collection, members are registered, and a + member borrows a book and later returns it. + +naming: + wire: lending + display: Lending + +types: + - name: library.lending.BookId + kind: newtype + of: Uuid + + - name: library.lending.MemberId + kind: newtype + of: Uuid + +conversions: + - from: Optional + to: library.lending.MemberId + because: BookHeld is emitted only from OnLoanReserved, and ReserveBook, the one way into that + state, sets reserved_for_id; nothing clears it until the hold is collected or released. + +entities: + # One physical book. There is no loan record: while the book is on loan it records which member + # has it, and the record is cleared when the book comes back. + # + # A book on loan may carry one reservation at a time (OnLoanReserved), for one member. When a + # reserved book comes back it is held for that member (OnHold) instead of going on the shelf, + # until they borrow it or a librarian releases the hold. There is no reservation record either: + # reserved_for_id names the member from the reservation until the hold is collected or ended. + - name: library.lending.Book + identity: + name: book_id + type: library.lending.BookId + fields: + - name: title + type: String + - name: author + type: String + - name: borrower_id + type: Optional + - name: reserved_for_id + type: Optional + relations: + - name: borrower + kind: references + target: library.lending.Member + cardinality: one + via: borrower_id + - name: reserved_for + kind: references + target: library.lending.Member + cardinality: one + via: reserved_for_id + lifecycle: + initial: OnShelf + states: [OnShelf, OnLoan, OnLoanReserved, OnHold, Withdrawn] + terminal: [Withdrawn] + transitions: + - name: lend + from: [OnShelf] + to: OnLoan + - name: return + from: [OnLoan] + to: OnShelf + - name: reserve + from: [OnLoan] + to: OnLoanReserved + - name: cancel_reservation + from: [OnLoanReserved] + to: OnLoan + - name: return_to_hold + from: [OnLoanReserved] + to: OnHold + - name: collect + from: [OnHold] + to: OnLoan + - name: release_hold + from: [OnHold] + to: OnShelf + - name: withdraw + from: [OnShelf] + to: Withdrawn + + # Members are registered and never leave. + - name: library.lending.Member + identity: + name: member_id + type: library.lending.MemberId + fields: + - name: name + type: String + lifecycle: + initial: Active + states: [Active] + terminal: [Active] + +actors: + - name: library.lending.Librarian + may: + - library.lending.AddBook + - library.lending.RegisterMember + - library.lending.BorrowBook + - library.lending.ReturnBook + - library.lending.WithdrawBook + - library.lending.ReserveBook + - library.lending.CancelReservation + - library.lending.CollectHold + - library.lending.ReleaseHold + naming: + display: Librarian + +errors: + - name: library.lending.BookStateConflict + summary: The book is not in a state this command acts from, so nothing changed. + fields: + - name: state + type: library.lending.Book.State + + - name: library.lending.BookNotFound + summary: No book in the collection has this identity. + fields: + - name: book_id + type: library.lending.BookId + + - name: library.lending.BookHeldForAnotherMember + summary: The book is held for a different member, so it was not lent. + fields: + - name: book_id + type: library.lending.BookId + + - name: library.lending.MemberAlreadyHasBook + summary: The member has this book on loan already, so there is nothing to reserve. + fields: + - name: book_id + type: library.lending.BookId + +commands: + - name: library.lending.AddBook + naming: + wire: add-book + display: Add a book + input: + - name: title + type: String + - name: author + type: String + outcomes: + - name: added + creates: library.lending.Book + instance: book_id + sets: + title: input.title + author: input.author + borrower_id: {cleared: true} + emits: + - library.lending.BookAdded + payload: + library.lending.BookAdded: + book_id: {generated: true} + title: input.title + author: input.author + summary: The book is in the collection, on the shelf. + + - name: library.lending.RegisterMember + naming: + wire: register-member + display: Register a member + input: + - name: name + type: String + outcomes: + - name: registered + creates: library.lending.Member + instance: member_id + sets: + name: input.name + emits: + - library.lending.MemberRegistered + payload: + library.lending.MemberRegistered: + member_id: {generated: true} + name: input.name + summary: The member is registered and may borrow. + + - name: library.lending.BorrowBook + naming: + wire: borrow-book + display: Borrow a book + input: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + # The borrowing member must be a registered member. That is a condition on another entity + # (Member), which a command outcome cannot guard on. Decided: the implementation checks it; + # this specification does not, and no scenario covers it. + # + # Only a book on the shelf is borrowed here. A book on hold is lent through CollectHold: one + # command cannot both branch on the book's state and guard on who it is held for + # (ESS-COMMAND-004). + outcomes: + - name: borrowed + moves: library.lending.Book.lend + instance: book_id + sets: + borrower_id: input.member_id + emits: + - library.lending.BookBorrowed + payload: + library.lending.BookBorrowed: + book_id: input.book_id + member_id: input.member_id + summary: The book is on loan to the member. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is on loan, on hold or withdrawn, so it was not lent. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was lent. + + # A return names only the book; the member who brings it back is not checked. + - name: library.lending.ReturnBook + naming: + wire: return-book + display: Return a book + input: + - name: book_id + type: library.lending.BookId + # A reserved book does not go back on the shelf: it is held for the member who reserved it. + outcomes: + - name: returned + when_subject_state: OnLoan + moves: library.lending.Book.return + instance: book_id + sets: + borrower_id: {cleared: true} + emits: + - library.lending.BookReturned + payload: + library.lending.BookReturned: + book_id: input.book_id + summary: The book is back on the shelf and no member has it. + + - name: held + when_subject_state: OnLoanReserved + moves: library.lending.Book.return_to_hold + instance: book_id + sets: + borrower_id: {cleared: true} + emits: + - library.lending.BookHeld + payload: + library.lending.BookHeld: + book_id: input.book_id + member_id: {subject: reserved_for_id} + summary: The book is back, off the shelf, held for the member who reserved it. + + - name: wrong-state + error: library.lending.BookStateConflict + summary: The book is not on loan, so nothing was returned. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was returned. + + - name: library.lending.WithdrawBook + naming: + wire: withdraw-book + display: Withdraw a book + input: + - name: book_id + type: library.lending.BookId + outcomes: + - name: withdrawn + moves: library.lending.Book.withdraw + instance: book_id + emits: + - library.lending.BookWithdrawn + payload: + library.lending.BookWithdrawn: + book_id: input.book_id + summary: The book has left the collection for good. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is on loan or already withdrawn, so it was not withdrawn. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was withdrawn. + + # A librarian reserves a book on loan for a member. Only a book on loan can be reserved (one on + # the shelf is simply borrowed), and only once at a time. Whether the member is registered is left + # to the implementation, as it is for BorrowBook. + - name: library.lending.ReserveBook + naming: + wire: reserve-book + display: Reserve a book + input: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + outcomes: + - name: already-borrower + when_subject: + predicate: borrower_id == input.member_id + error: library.lending.MemberAlreadyHasBook + summary: The member has this book on loan, so it was not reserved. + + - name: reserved + moves: library.lending.Book.reserve + instance: book_id + sets: + reserved_for_id: input.member_id + emits: + - library.lending.BookReserved + payload: + library.lending.BookReserved: + book_id: input.book_id + member_id: input.member_id + summary: The book is on loan and reserved for the member. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is on the shelf, already reserved, on hold or withdrawn, so it was not reserved. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was reserved. + + - name: library.lending.CancelReservation + naming: + wire: cancel-reservation + display: Cancel a reservation + input: + - name: book_id + type: library.lending.BookId + outcomes: + - name: cancelled + moves: library.lending.Book.cancel_reservation + instance: book_id + sets: + reserved_for_id: {cleared: true} + emits: + - library.lending.ReservationCancelled + payload: + library.lending.ReservationCancelled: + book_id: input.book_id + summary: The book is still on loan and no longer reserved. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is not on loan with a reservation, so nothing was cancelled. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was cancelled. + + # The member a book is held for borrows it; the hold is over. Anybody else is refused. + - name: library.lending.CollectHold + naming: + wire: collect-hold + display: Collect a held book + input: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + outcomes: + - name: held-for-another + when_subject: + predicate: reserved_for_id != input.member_id + error: library.lending.BookHeldForAnotherMember + summary: The book is held for a different member, so it was not lent. + + - name: collected + moves: library.lending.Book.collect + instance: book_id + sets: + borrower_id: input.member_id + reserved_for_id: {cleared: true} + emits: + - library.lending.BookBorrowed + payload: + library.lending.BookBorrowed: + book_id: input.book_id + member_id: input.member_id + summary: The member the book was held for has it on loan, and the hold is over. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is not on hold, so it was not collected. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was collected. + + # For a member who never collects: there is no clock here, so a hold ends only when a librarian + # releases it. + - name: library.lending.ReleaseHold + naming: + wire: release-hold + display: Release a hold + input: + - name: book_id + type: library.lending.BookId + outcomes: + - name: released + moves: library.lending.Book.release_hold + instance: book_id + sets: + reserved_for_id: {cleared: true} + emits: + - library.lending.HoldReleased + payload: + library.lending.HoldReleased: + book_id: input.book_id + summary: The book is back on the shelf and held for nobody. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is not on hold, so nothing was released. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was released. + +events: + - name: library.lending.BookAdded + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: author + type: String + + - name: library.lending.MemberRegistered + fields: + - name: member_id + type: library.lending.MemberId + - name: name + type: String + + - name: library.lending.BookBorrowed + fields: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + + - name: library.lending.BookReturned + fields: + - name: book_id + type: library.lending.BookId + + - name: library.lending.BookWithdrawn + fields: + - name: book_id + type: library.lending.BookId + + - name: library.lending.BookReserved + fields: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + + - name: library.lending.ReservationCancelled + fields: + - name: book_id + type: library.lending.BookId + + - name: library.lending.BookHeld + fields: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + + - name: library.lending.HoldReleased + fields: + - name: book_id + type: library.lending.BookId + +views: + # The whole collection, in every state, with who has each book. + - name: library.lending.Catalogue + source: library.lending.Book + consistency: read_your_writes + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: author + type: String + - name: state + type: library.lending.Book.State + - name: borrower_id + type: Optional + - name: reserved_for_id + type: Optional + naming: + wire: catalogue + display: Catalogue + + - name: library.lending.Members + source: library.lending.Member + consistency: read_your_writes + fields: + - name: member_id + type: library.lending.MemberId + - name: name + type: String + naming: + wire: members + display: Members + + # The loans in force: every book on loan, reserved or not, and the member who has it. + - name: library.lending.BooksOnLoan + source: library.lending.Book + consistency: read_your_writes + filter: {any: [state == OnLoan, state == OnLoanReserved]} + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: borrower_id + type: Optional + naming: + wire: books-on-loan + display: Books on loan + + # Books back from loan and waiting for the member who reserved them. + - name: library.lending.BooksOnHold + source: library.lending.Book + consistency: read_your_writes + filter: state == OnHold + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: reserved_for_id + type: Optional + naming: + wire: books-on-hold + display: Books on hold diff --git a/fixtures/library-reservations-drafted/spec/ess-inputs.yaml b/fixtures/library-reservations-drafted/spec/ess-inputs.yaml new file mode 100644 index 0000000..c404043 --- /dev/null +++ b/fixtures/library-reservations-drafted/spec/ess-inputs.yaml @@ -0,0 +1,7 @@ +format: ess-inputs/2 +requires: ess 0.38.0 +specification: + - system.yaml + - components.yaml + - domains/lending.yaml +scenarios: [] diff --git a/fixtures/library-reservations-drafted/spec/system.yaml b/fixtures/library-reservations-drafted/spec/system.yaml new file mode 100644 index 0000000..82586ba --- /dev/null +++ b/fixtures/library-reservations-drafted/spec/system.yaml @@ -0,0 +1,6 @@ +format: ess/15 +system: library +version: v1 + +domains: + - library.lending From 7293f5eaafda2ac8f45b42bc25012a2fc4b2ddd4 Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:41:48 +0200 Subject: [PATCH 08/10] docs: tutorial, your first governed plan - New website/docs/tutorials/first-governed-plan.md continues the ESS tutorial's library with AEP: install, adopt a /5 store, ask for reservations (nine numbered questions, answered "defaults"), the specification extended first (17 -> 55 scenarios, 0 refusals), an epic and six stories, four critic verdicts, a wave proposal, and the first story implemented, adversary-reviewed and merged. - Every output is a real run on 2026-09-28 with aep 0.64.0, ess 0.39.0 and plugins 0.17.0 in an isolated sandbox, one claude -p session resumed across five prompts (plan and critics $5.48, acceptance $6.34, wave $13.19). Recorded outputs include `aep plan artifact waves` (5 waves, 20 collisions), `explain` on the implemented story (the adversary outcomes, the 10/17 -> 17/17 verification, the gate on the merge), the board and the git graph. - The validate warnings on the four review records are aep#60, named on the page. - Sidebar Tutorials category, intro, b10x.docs.yaml guide section; the ESS tutorial's closing section links it (its trial fixture copies the page again). --- b10x.docs.yaml | 3 + trials/ess-tutorial/fixture/tutorial.md | 6 +- website/docs/intro.md | 3 +- .../docs/tutorials/first-ess-specification.md | 6 +- website/docs/tutorials/first-governed-plan.md | 392 ++++++++++++++++++ website/sidebars.ts | 2 +- 6 files changed, 404 insertions(+), 8 deletions(-) create mode 100644 website/docs/tutorials/first-governed-plan.md diff --git a/b10x.docs.yaml b/b10x.docs.yaml index 5c4facf..9d0e2ee 100644 --- a/b10x.docs.yaml +++ b/b10x.docs.yaml @@ -60,6 +60,9 @@ surfaces: - kind: guide label: 'Tutorial: your first ESS specification' url: https://beyond10x.github.io/docs/agentplugins/tutorials/first-ess-specification/ + - kind: guide + label: 'Tutorial: your first governed plan' + url: https://beyond10x.github.io/docs/agentplugins/tutorials/first-governed-plan/ - kind: reference label: Plugin reference url: https://beyond10x.github.io/docs/agentplugins/plugins/b10x/ diff --git a/trials/ess-tutorial/fixture/tutorial.md b/trials/ess-tutorial/fixture/tutorial.md index 46d18d4..7587133 100644 --- a/trials/ess-tutorial/fixture/tutorial.md +++ b/trials/ess-tutorial/fixture/tutorial.md @@ -1191,9 +1191,9 @@ specification or in the implementation. ## Next -- **Plan work around it.** Once a new noun has a specification, AEP plans the stories that build it. - The [golden path](../golden-path.md) walks an agent from a feature idea to a critiqued plan; a - tutorial like this one for AEP follows. +- **Plan work around it.** [Your first governed plan](./first-governed-plan.md) continues with this + library: AEP plans a new feature, four critics review the plan, and the first story is built in a + reviewed wave. - **Specify a system that already runs.** `ess:retrofitting` derives a specification from an OpenAPI document or from code, citing a source for every declaration. - **Go deeper into ESS.** The [ESS documentation](https://beyond10x.github.io/docs/ess/) covers the diff --git a/website/docs/intro.md b/website/docs/intro.md index 09e491f..c06455b 100644 --- a/website/docs/intro.md +++ b/website/docs/intro.md @@ -28,5 +28,6 @@ installed. New to ESS? [Your first ESS specification](./tutorials/first-ess-specification.md) takes you from an empty directory to a validated specification and a passing conformance suite, with an agent doing -the writing. Otherwise [set up with one sentence](./install.md), [start with the front +the writing, and [Your first governed plan](./tutorials/first-governed-plan.md) continues it with an +AEP plan and one reviewed wave. Otherwise [set up with one sentence](./install.md), [start with the front door](./plugins/b10x.md) or [choose a specialist](./choose-a-plugin.md). diff --git a/website/docs/tutorials/first-ess-specification.md b/website/docs/tutorials/first-ess-specification.md index 46d18d4..7587133 100644 --- a/website/docs/tutorials/first-ess-specification.md +++ b/website/docs/tutorials/first-ess-specification.md @@ -1191,9 +1191,9 @@ specification or in the implementation. ## Next -- **Plan work around it.** Once a new noun has a specification, AEP plans the stories that build it. - The [golden path](../golden-path.md) walks an agent from a feature idea to a critiqued plan; a - tutorial like this one for AEP follows. +- **Plan work around it.** [Your first governed plan](./first-governed-plan.md) continues with this + library: AEP plans a new feature, four critics review the plan, and the first story is built in a + reviewed wave. - **Specify a system that already runs.** `ess:retrofitting` derives a specification from an OpenAPI document or from code, citing a source for every declaration. - **Go deeper into ESS.** The [ESS documentation](https://beyond10x.github.io/docs/ess/) covers the diff --git a/website/docs/tutorials/first-governed-plan.md b/website/docs/tutorials/first-governed-plan.md new file mode 100644 index 0000000..d16658b --- /dev/null +++ b/website/docs/tutorials/first-governed-plan.md @@ -0,0 +1,392 @@ +--- +title: Your first governed plan +sidebar_label: Your first governed plan +description: Continue the ESS tutorial's lending library with AEP. Adopt a planning store, plan a feature that adds a new state, have four critics review the plan, and implement the first story in a reviewed wave. +--- + +# Your first governed plan + +[Your first ESS specification](./first-ess-specification.md) ended with a lending library: a +specification in `spec/` and a Go implementation that passes its 17 conformance scenarios. This +tutorial continues from there with **AEP**, which keeps the plan for changing that library inside the +repository as files the `aep` command validates: an epic, the stories under it, the critics' +verdicts, the evidence that moved each story, and what is still open. + +In this tutorial you ask for one feature, *members can reserve a book that is on loan*, and follow +it from a question to a merged, reviewed change. It takes about an hour, most of it the agent's. + +**What you end with:** + +- a planning store in `.engineering/`, which `aep plan artifact validate` accepts; +- the specification extended by the agent before any story was written, with the suite grown from + 17 to 55 scenarios; +- an epic and six stories, each naming the scenarios it turns from skipped to passing, and four + critic verdicts recorded as immutable review records; +- the first story implemented in a wave, attacked by an adversary agent, merged, and moved to + `implemented` on the evidence it recorded. + +Every output block on this page is what the command printed when this page was recorded, on +2026-09-28, with `aep` 0.64.0, `ess` 0.39.0, the `aep` and `ess` plugins 0.17.0 and Claude Code. +Paths are shortened to `~`. The recorded session cost $5.48 for the plan and critics, $6.34 for +accepting the stories, and $13.19 for the wave. + +## What you need + +- The `library/` directory from [the ESS tutorial](./first-ess-specification.md), committed to Git. +- Claude Code or Codex, and Go 1.23 or newer. + +## 1. Install `aep` and its plugin + +If you did the ESS tutorial through `b10x`, add the `aep` product: + +```shell-session +$ b10x init aep,ess --host claude --out plan.json +``` + +The actions it plans (the rest of the plan lists what is already installed): + +```text +Actions (6): + 1. register marketplace `b10x`: claude plugin marketplace add beyond10x/agentplugins + 2. install `b10x@b10x`: claude plugin install b10x@b10x --scope user + 3. install `aep@b10x`: claude plugin install aep@b10x --scope user + 4. install `ess@b10x`: claude plugin install ess@b10x --scope user + 5. install `aep`: install aep 0.64.0 into ~/.local/bin (prebuilt archive) + 6. install `ess`: install ess 0.39.0 into ~/.local/bin (prebuilt archive) + +``` + +```shell-session +$ b10x setup apply --plan plan.json --yes +``` + +Restart Claude Code so it loads the plugin. `aep@b10x` brings the `aep:planning` skill, which owns +the store, and `aep:implementing`, which runs waves. + +## 2. Adopt a planning store + +Ask: + +```text +This repository has an ESS specification in spec/ and a Go implementation in impl/. Set up AEP +planning here so the work on it is planned in the repository. Tell me what you did. +``` + +The agent ran `aep plan reverse init`, which wrote `.engineering/project.yaml` in the current store +format (`aep.project/5`) and pinned the shared planning rules to the exact commit of the `aep` +release. Then its `aep:reverse-engineer` agent read the repository and its history and drafted the +first artifacts, each citing the line it came from: + +```shell-session +$ aep plan artifact list +``` + +```text +executable-system-specification:library executable-system-specification draft library v1 — ESS specification of the lending domain +story:borrow-member-check-tested story draft The registered-member check on Borrow is exercised by a test +vision:lending-library vision draft A small lending library, specified in ESS and implemented in Go +``` + +It also found a real gap: the check that a borrower is a registered member is never exercised +(`impl/conformance_test.go:90` always passes `false`), and filed it as a story instead of fixing it +unasked. It ended with five questions the code could not answer, such as whether a network service +is owed. Commit the store before you go on: it is a plan, and a plan belongs in version control. + +## 3. Ask for a feature + +```text +Members should be able to reserve a book that is on loan, so that when it comes back it is held +for them instead of going back on the shelf. Plan this change. Before you write anything, ask me the +decisions you need from me, then stop and wait for my answers. +``` + +A reservation changes the book's lifecycle, so `aep:planning` will not write a story around it +until the specification says what a reservation is. It asked first, each question with the answer +it would take: + +
+The agent's questions (9) + +I read `spec/domains/lending.yaml` and `impl/library.go`. Right now a Book can only be `OnShelf → OnLoan → OnShelf` (or `Withdrawn`). The borrower is stored on the Book itself, there's no loan record, and the only actor in the spec is the Librarian. Reservations change the Book's lifecycle, so I need these decided before I model it. I've put a suggested default on each, so you can answer "defaults, except 3b" if that's easier. + +**The model** + +1. **How many reservations can a book have?** Just one at a time, or a queue served first come, first served? This decides whether a reservation is a field on Book (`reserved_for_id`) or its own entity with a one-to-many relation to Book. I won't guess this one. *Default: one at a time; a second attempt is refused.* +2. **What does a returned, reserved book become?** I'd add a new Book state, `OnHold`, alongside `OnShelf`. The alternative is to keep it `OnShelf` with a hold marker on it. *Default: a new state `OnHold`.* + +**The lifecycle** + +3. **How does a held book stop being held?** + a. The member who reserved it borrows it: `OnHold → OnLoan`. When anyone else tries to borrow it, should that be a `BookStateConflict` or a new error such as `BookHeldForAnotherMember`? *Default: a new error, since it tells the librarian why.* + b. Is there a way to release the hold, putting the book back on the shelf (`OnHold → OnShelf`), for when the member never comes to collect it? If so, who does it? Expiring holds after N days would need a clock, and the spec has none. *Default: a manual `ReleaseHold` command for the Librarian; no time-based expiry.* +4. **Can a reservation be cancelled while the book is still on loan?** *Default: yes, with a `CancelReservation` command.* +5. **Can a held book be withdrawn?** *Default: no. Release the hold first; withdraw keeps acting only from `OnShelf`.* + +**The rules** + +6. **Who places a reservation?** The only actor in the spec is the Librarian. Do they reserve on a member's behalf, or should I add a Member actor who can reserve for themselves? *Default: the Librarian, as with borrowing.* +7. **Which reservations are refused?** The member already has the book on loan? The book is on the shelf? (Your wording suggests on-loan books only, so the book would just be borrowed instead.) *Default: refuse both.* +8. **Must the reserving member be registered?** For borrowing, that check was left to the implementation and is off in the conformance target, which is the untested gap the draft story covers. Should reservations do the same, or should the spec model the check this time? *Default: the same as borrowing, for consistency.* + +**What people can see** + +9. **Views and events.** Should the Catalogue show `reserved_for_id`? Should there be a new "held awaiting pickup" view? And when a reserved book comes back, should the return emit `BookReturned` plus `BookHeld`, or have its own outcome? *Default: add the field to the Catalogue, add a `BooksOnHold` view, and give the return a separate `held` outcome that emits `BookHeld`.* + +Once you've answered, I'll update the ESS domain and validate it. Then I'll create an epic that cites the updated spec and break it down into stories. A panel of four review agents will check the stories for testability, overlap, scope and parallel work, and I'll write up anything they find that I didn't fix. I won't touch any file until you reply. + +
+ +This page answered: + +```text +Defaults for all nine. Go ahead: model it, plan it, and have the critics review the plan. Tell me +what you did and what is still open. +``` + +## 4. What it planned + +**The specification first.** The agent extended `spec/domains/lending.yaml` before drafting a +single story: two new book states, a `reserved_for_id` field, four commands, two errors, a +`BookHeld` event and a `BooksOnHold` view. The book's lifecycle now reads: + +```yaml + lifecycle: + initial: OnShelf + states: [OnShelf, OnLoan, OnLoanReserved, OnHold, Withdrawn] + terminal: [Withdrawn] + transitions: + - name: lend + from: [OnShelf] + to: OnLoan + - name: return + from: [OnLoan] + to: OnShelf + - name: reserve + from: [OnLoan] + to: OnLoanReserved + - name: cancel_reservation + from: [OnLoanReserved] + to: OnLoan + - name: return_to_hold + from: [OnLoanReserved] + to: OnHold + - name: collect + from: [OnHold] + to: OnLoan + - name: release_hold + from: [OnHold] + to: OnShelf + - name: withdraw + from: [OnShelf] + to: Withdrawn +``` + +It reported one deviation from the answers it had proposed: taking a held book out is its own +command, `CollectHold`, because one ESS command cannot both depend on the book's state and check who +the book is held for. It wrote that into the epic instead of bending the model. + +```shell-session +$ ess verify conform synthesize --path spec --target go --out impl +``` + +```text +warning: spec/ess-inputs.yaml requires ess 0.38.0 and this is ess 0.39.0, which is newer; continuing (--strict-requires refuses) +55 scenario(s) (0 authored), 0 refusal(s), 7 file(s) written to impl +``` + +**Then the plan.** An epic, `epic:book-reservations`, and six stories drafted by the +`aep:decomposer` agent. Each story's acceptance names the exact scenarios it turns from skipped to +passing; together they cover all 55 once. + +**Then the critics.** Four agents read the drafted set at once, none seeing another's verdict: +acceptance (can each story be checked?), design (coupling, cycles, a split abstraction), scope +(everything the epic promised is claimed, nothing else), and parallel safety (which stories land on +one file). All four approved in the first round, and each verdict is stored word for word as a +`review-result` that cannot be edited afterwards. + +```shell-session +$ aep plan artifact validate +``` + +```text +16 file(s) in ~/library/.engineering/planning: 16 artifact(s) +4 review(s) recorded no findings block: + - review-result:acceptance-round-1 states its findings as prose only — nothing can enumerate what it found, so the next review starts from nowhere + - review-result:design-round-1 states its findings as prose only — nothing can enumerate what it found, so the next review starts from nowhere + - review-result:parallel-safety-round-1 states its findings as prose only — nothing can enumerate what it found, so the next review starts from nowhere + - review-result:scope-round-1 states its findings as prose only — nothing can enumerate what it found, so the next review starts from nowhere +valid +``` + +`valid` is the verdict. The four warnings above it come from `aep` itself: it counts a review that +ended with an empty findings block as having none (beyond10x/aep#60). + +## 5. Accept the stories and propose a wave + +```text +Leave the open points as they are for now. Commit what you did. Then accept the six reservation +stories and propose the first wave with aep:implementing: tell me which stories are in it and why, +and stop there until I approve. +``` + +Accepting a story is a lifecycle move through `aep plan artifact move`, which the store validates. +Moving the first story to `active` was refused, `story:reservations-suite-baseline is proposed and +serves no objective`, so the agent linked every story to the vision it serves and moved them again. + +A **wave** is the set of stories that can be built at the same time without touching the same file. +The store derives it from each story's declared scope: + +```shell-session +$ aep plan artifact waves --kind story --status active +``` + +```text +wave 1 + story:reserve-book-on-loan +wave 2 + story:return-reserved-book-to-hold +wave 3 + story:cancel-reservation +wave 4 + story:collect-held-book +wave 5 + story:release-hold +collision: story:cancel-reservation story:collect-held-book impl/conformance_test.go +collision: story:cancel-reservation story:collect-held-book impl/library.go +collision: story:cancel-reservation story:release-hold impl/conformance_test.go +collision: story:cancel-reservation story:release-hold impl/library.go +collision: story:cancel-reservation story:reserve-book-on-loan impl/conformance_test.go +collision: story:cancel-reservation story:reserve-book-on-loan impl/library.go +collision: story:cancel-reservation story:return-reserved-book-to-hold impl/conformance_test.go +collision: story:cancel-reservation story:return-reserved-book-to-hold impl/library.go +collision: story:collect-held-book story:release-hold impl/conformance_test.go +collision: story:collect-held-book story:release-hold impl/library.go +collision: story:collect-held-book story:reserve-book-on-loan impl/conformance_test.go +collision: story:collect-held-book story:reserve-book-on-loan impl/library.go +collision: story:collect-held-book story:return-reserved-book-to-hold impl/conformance_test.go +collision: story:collect-held-book story:return-reserved-book-to-hold impl/library.go +collision: story:release-hold story:reserve-book-on-loan impl/conformance_test.go +collision: story:release-hold story:reserve-book-on-loan impl/library.go +collision: story:release-hold story:return-reserved-book-to-hold impl/conformance_test.go +collision: story:release-hold story:return-reserved-book-to-hold impl/library.go +collision: story:reserve-book-on-loan story:return-reserved-book-to-hold impl/conformance_test.go +collision: story:reserve-book-on-loan story:return-reserved-book-to-hold impl/library.go +5 wave(s), 20 collision(s), 0 unassessed +``` + +Every pair of stories lands on the same two Go files, so each wave holds one story. (This listing +was taken after wave 1, which held the sixth story, `reservations-suite-baseline`.) The agent +proposed that wave, named every commit your approval would authorise and nothing more (no push, no +tag, no second wave), and asked three things: which branch to use as the base, whether plain +`git worktree` is acceptable, and how much model budget is left. + +## 6. Run the wave + +```text +Approved. 1: use book-reservations as the base. 2: plain `git worktree add` is fine here. 3: the +budget is not a limit for this wave. Implement the first wave, with the adversary review, and stop +when it is merged. Tell me what happened, including anything the adversary found. +``` + +What the wave did, in order: + +1. The `aep:implementor` agent built the story in its own worktree: the smallest change that turns + its 17 scenarios from skipped to passing. +2. The claim was checked against the base: the same test command before and after. 10 of 17 passed + before, 17 of 17 after, none failed. That comparison is recorded as `verification` evidence. +3. The `aep:adversary` agent tried to break the change, for up to two passes. It found no failing + behaviour, but three gaps in the tests: the new view code was never exercised, a doc comment + misdescribed `Return`, and nothing tested that `Return` refuses the two new states. Each was + fixed by a test, and each finding's outcome was recorded. +4. The unit merged into the wave's integration branch, the gate ran there, and the integration + branch merged into the base. + +```shell-session +$ git log --oneline --graph +``` + +```text +* 9dce7df Merge wave-1/integration into book-reservations +|\ +| * 950900a Wave 1 closed: story:reservations-suite-baseline implemented +| * aee0d43 Merge impl/reservations-suite-baseline into wave-1/integration +|/| +| * c3bebc2 Pin Return's refusal of the two new states +| * e7658b7 Pin the reservation views with tests that force the new states +| * f0cc3e2 Add reservation states and answer the reservation views +|/ +* 3163f1b Wave 1: accept reservation stories, open wave page +* 7d2a962 Plan book reservations: ESS model, epic, six stories, critic round 1 +* e89b41c Adopt AEP planning +* 30e5ba2 Lending library: ESS specification and Go implementation +``` + +Ask the store why the story is where it is: + +```shell-session +$ aep plan artifact explain story:reservations-suite-baseline +``` + +```text +story:reservations-suite-baseline in ~/library/.engineering/planning: implemented, revision 7 + draft -> proposed 2026-09-28T13:54:57Z (revision 3) + no record: nothing was recorded about how this was decided + proposed -> active 2026-09-28T13:54:57Z (revision 4) + no record: nothing was recorded about how this was decided + active -> implemented 2026-09-28T14:09:35Z (revision 7) + review_outcome from review-result:adversary-wave-1-u1-pass-1, observed 2026-09-28T14:04:39Z, admitted at revision 4 + review_outcome from review-result:adversary-wave-1-u1-pass-1, observed 2026-09-28T14:04:40Z, admitted at revision 4 + verification from go test -count=1 -v ./... in impl/, impl/essconform regenerated from spec/ (55 scenarios): acceptance scenarios PASS 10/17 at baseline 3163f1b, 17/17 at c3bebc2; 0 FAIL both (.engineering/drafts/wave-1/integration/scratch/baseline.txt vs .engineering/drafts/wave-1/u1/scratch/treatment.txt), observed 2026-09-28T14:09:02Z, admitted at revision 4 + review_outcome from review-result:adversary-wave-1-u1-pass-2, observed 2026-09-28T14:09:02Z, admitted at revision 4 + test_result from gate on wave-1/integration at merge aee0d43: aep plan artifact validate (valid); ess specify validate (valid); ess verify conform synthesize --target go --out impl (55 scenarios, 0 refusals); go vet ./... (exit 0); go test -count=1 -v ./... (ok; TestConformance 17 PASS / 38 SKIP / 0 FAIL, 3 unit tests PASS); go run cmd/gofmt -l impl (empty, exit 0) (aee0d43), observed 2026-09-28T14:09:35Z, admitted at revision 6 + next: archived needs no record +``` + +Every move is there with what the store admitted before it: the adversary's two recorded outcomes, +the before/after verification, and the gate that ran on the merge. + +```shell-session +$ aep plan artifact board --kind story +``` + +```text +draft (1) + story:borrow-member-check-tested The registered-member check on Borrow is exercised by a test + +active (5) + story:cancel-reservation A librarian cancels a reservation while the book is still on loan + story:collect-held-book The member a book is held for collects it + story:release-hold A librarian releases a hold and the book goes back on the shelf + story:reserve-book-on-loan A librarian reserves a book on loan for a member + story:return-reserved-book-to-hold A returned reserved book is held for the member who reserved it + +implemented (1) + story:reservations-suite-baseline Existing lending behaviour passes the regenerated reservations suite +``` + +## 7. Keep going + +Five stories are `active`, and `aep plan artifact waves` already says which comes next. Ask for the +next wave the same way. Each wave ends with the suite a little greener: + +```shell-session +$ cd impl && ESS_REPORT_FORMAT=2 go test ./... +``` + +```text +ok example.com/library 0.043s +? example.com/library/essconform [no test files] +``` + +The open points from steps 2 and 4 stay in the store until somebody answers them: a store keeps an +unanswered question as a record instead of a guess. + +## Next + +- **The longer walk.** The [golden path](../golden-path.md) goes through the same steps on a larger + repository, and ends by handing one story to `metaharness aep drive`, where an engine rather than + the agent decides every step. +- **Test the suite itself.** `ess:hardening` runs `ess verify conform mutate` and the concurrent + explorer against your implementation. +- **Go deeper into AEP.** The [AEP documentation](https://beyond10x.github.io/docs/aep/) covers + lifecycles, evidence and the planning commands. diff --git a/website/sidebars.ts b/website/sidebars.ts index 0810439..e26f0c9 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -8,7 +8,7 @@ const sidebars: SidebarsConfig = { { type: 'category', label: 'Tutorials', - items: ['tutorials/first-ess-specification'], + items: ['tutorials/first-ess-specification', 'tutorials/first-governed-plan'], }, 'golden-path', 'structure', From 55edeb28e88edb7396559fda8b319a35b8f6216d Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:17:04 +0200 Subject: [PATCH 09/10] test(trials): aep-tutorial trial; the page fixed where a fresh agent's run differed - trials/aep-tutorial: a fresh isolated agent gets the ESS tutorial's library (fixture) and a copy of first-governed-plan.md and works steps 2-6. First complete run (aep 0.64.0, ess 0.39.0, plugins at this branch): /5 store, spec 17 -> 55 scenarios with 0 refusals, epic and six stories, four critic approvals (the scope critic sent back once under the new line-1 rule), reservations-suite-baseline implemented, adversary-reviewed and merged, final go test ok; 474 tool calls, $12.41. Baseline: tool calls, synthesis, outputs. validate and go_test are not measured: the wave's adversary makes red runs on purpose, and the report reads the last run it saw. - The page, from that run's list of differences: ids, counts and wording differ between runs (said once at the top); the validate output is labelled as taken after the wave; the `serves no objective` refusal is this recording's, the decomposer now prevents it; who declares a story's scope (aep:story-scoper) and what to do when `waves` finds none; the plan's branch becomes the wave's base; the worktree plugin as the alternative to plain git worktrees. - trials.rs: trial kind aep-tutorial; the page-copy test covers both tutorials. - Taskfile trial:sandbox makes a sandbox writable before removing it: aep's protocol snapshots are read-only, so a sandbox that ran aep could not be recreated ("rm: cannot remove ... Permission denied"). --- Taskfile.yml | 3 +- crates/agentplugins-check/src/trials.rs | 28 +- trials/aep-tutorial/fixture/.gitignore | 2 + .../fixture/impl/conformance_test.go | 191 +++++++++ trials/aep-tutorial/fixture/impl/go.mod | 3 + trials/aep-tutorial/fixture/impl/library.go | 156 +++++++ .../aep-tutorial/fixture/spec/components.yaml | 21 + .../fixture/spec/domains/lending.yaml | 314 ++++++++++++++ .../aep-tutorial/fixture/spec/ess-inputs.yaml | 7 + trials/aep-tutorial/fixture/spec/system.yaml | 6 + trials/aep-tutorial/fixture/tutorial.md | 402 ++++++++++++++++++ trials/aep-tutorial/trial.yaml | 25 ++ trials/baseline.json | 13 + website/docs/tutorials/first-governed-plan.md | 22 +- 14 files changed, 1176 insertions(+), 17 deletions(-) create mode 100644 trials/aep-tutorial/fixture/.gitignore create mode 100644 trials/aep-tutorial/fixture/impl/conformance_test.go create mode 100644 trials/aep-tutorial/fixture/impl/go.mod create mode 100644 trials/aep-tutorial/fixture/impl/library.go create mode 100644 trials/aep-tutorial/fixture/spec/components.yaml create mode 100644 trials/aep-tutorial/fixture/spec/domains/lending.yaml create mode 100644 trials/aep-tutorial/fixture/spec/ess-inputs.yaml create mode 100644 trials/aep-tutorial/fixture/spec/system.yaml create mode 100644 trials/aep-tutorial/fixture/tutorial.md create mode 100644 trials/aep-tutorial/trial.yaml diff --git a/Taskfile.yml b/Taskfile.yml index 7df2984..a5ef24b 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -42,7 +42,8 @@ tasks: fi done done - - rm -rf '{{.T}}' + # aep makes its protocol snapshots read-only; a sandbox that ran aep cannot be removed without this. + - if [ -d '{{.T}}' ]; then chmod -R u+w '{{.T}}'; fi; rm -rf '{{.T}}' - mkdir -p -m 700 '{{.TRIALS}}' - mkdir -p '{{.T}}/home/.local/bin' '{{.T}}/work' '{{.T}}/tools' - for t in claude codex go gofmt; do p="$(command -v $t)" && ln -sf "$(readlink -f "$p")" '{{.T}}/tools/'$t; done; true diff --git a/crates/agentplugins-check/src/trials.rs b/crates/agentplugins-check/src/trials.rs index 36f9d30..8cdbdce 100644 --- a/crates/agentplugins-check/src/trials.rs +++ b/crates/agentplugins-check/src/trials.rs @@ -33,6 +33,8 @@ pub enum Kind { EssFullPackage, /// A fresh agent follows the public ESS tutorial, given only the page. EssTutorial, + /// A fresh agent follows the public AEP tutorial on the ESS tutorial's library. + AepTutorial, /// AEP planning from an existing backlog. AepBacklog, /// Setting up `worktree` in a repository. @@ -406,6 +408,7 @@ mod tests { "ess-pipeline", "ess-full-package", "ess-tutorial", + "aep-tutorial", "aep-backlog", "worktree-onboarding", "upgrade-seeded", @@ -419,16 +422,21 @@ mod tests { #[test] fn the_tutorial_trial_carries_the_published_page() { let root = repository(); - let page = - std::fs::read_to_string(root.join("website/docs/tutorials/first-ess-specification.md")) - .expect("the tutorial page exists"); - let copy = - std::fs::read_to_string(root.join(TRIALS).join("ess-tutorial/fixture/tutorial.md")) - .expect("the trial fixture carries the page"); - assert!( - page == copy, - "trials/ess-tutorial/fixture/tutorial.md differs from the tutorial page; copy the page again" - ); + for (trial, page) in [ + ("ess-tutorial", "first-ess-specification"), + ("aep-tutorial", "first-governed-plan"), + ] { + let published = + std::fs::read_to_string(root.join(format!("website/docs/tutorials/{page}.md"))) + .expect("the tutorial page exists"); + let copy = + std::fs::read_to_string(root.join(TRIALS).join(trial).join("fixture/tutorial.md")) + .expect("the trial fixture carries the page"); + assert!( + published == copy, + "trials/{trial}/fixture/tutorial.md differs from the {page} page; copy the page again" + ); + } } #[test] diff --git a/trials/aep-tutorial/fixture/.gitignore b/trials/aep-tutorial/fixture/.gitignore new file mode 100644 index 0000000..6194a80 --- /dev/null +++ b/trials/aep-tutorial/fixture/.gitignore @@ -0,0 +1,2 @@ +impl/essconform/ +impl/.ess-output/ diff --git a/trials/aep-tutorial/fixture/impl/conformance_test.go b/trials/aep-tutorial/fixture/impl/conformance_test.go new file mode 100644 index 0000000..2242c10 --- /dev/null +++ b/trials/aep-tutorial/fixture/impl/conformance_test.go @@ -0,0 +1,191 @@ +package library_test + +import ( + "errors" + "fmt" + "os" + "testing" + + "example.com/library" + "example.com/library/essconform" +) + +func TestConformance(t *testing.T) { + // The suite runs only once a report format is chosen; choose it here so plain `go test ./...` + // runs it. ESS_REPORT_OUT, when set, still decides where the report goes. + if os.Getenv("ESS_REPORT_FORMAT") == "" { + t.Setenv("ESS_REPORT_FORMAT", "2") + } + essconform.Run(t, func() essconform.Target { return newTarget() }) +} + +// target drives one in-memory Library through the suite's Target interface. +type target struct { + lib *library.Library +} + +func newTarget() *target { return &target{lib: library.New()} } + +func (t *target) Identity() (essconform.Identity, error) { + return essconform.Identity{Name: "example.com/library", Version: "v1"}, nil +} + +func (t *target) BeginScenario(essconform.ScenarioContext) error { return nil } +func (t *target) EndScenario(essconform.ScenarioContext) error { return nil } + +func (t *target) ExecuteCommand(req essconform.CommandRequest) (essconform.CommandResult, error) { + in := func(name string) (string, error) { + v, ok := req.Input[name].(string) + if !ok { + return "", fmt.Errorf("%s: input %q is not text", req.Command, name) + } + return v, nil + } + event := func(name string, payload map[string]essconform.Node) []essconform.ObservedEvent { + return []essconform.ObservedEvent{{Event: name, Payload: payload}} + } + + switch req.Command { + case "library.lending.AddBook": + title, err := in("title") + if err != nil { + return essconform.CommandResult{}, err + } + author, err := in("author") + if err != nil { + return essconform.CommandResult{}, err + } + b := t.lib.AddBook(title, author) + return essconform.CommandResult{ + Outcome: "added", + DirectEvents: event("library.lending.BookAdded", map[string]essconform.Node{ + "book_id": b.ID, "title": b.Title, "author": b.Author, + }), + }, nil + + case "library.lending.RegisterMember": + name, err := in("name") + if err != nil { + return essconform.CommandResult{}, err + } + m := t.lib.RegisterMember(name) + return essconform.CommandResult{ + Outcome: "registered", + DirectEvents: event("library.lending.MemberRegistered", map[string]essconform.Node{ + "member_id": m.ID, "name": m.Name, + }), + }, nil + + case "library.lending.BorrowBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + memberID, err := in("member_id") + if err != nil { + return essconform.CommandResult{}, err + } + // The member check is the implementation's own rule, outside the specification; the + // suite borrows for members it never registered, so it is off here. + if err := t.lib.Borrow(bookID, memberID, false); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "borrowed", + DirectEvents: event("library.lending.BookBorrowed", map[string]essconform.Node{ + "book_id": bookID, "member_id": memberID, + }), + }, nil + + case "library.lending.ReturnBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + if err := t.lib.Return(bookID); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "returned", + DirectEvents: event("library.lending.BookReturned", map[string]essconform.Node{"book_id": bookID}), + }, nil + + case "library.lending.WithdrawBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + if err := t.lib.Withdraw(bookID); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "withdrawn", + DirectEvents: event("library.lending.BookWithdrawn", map[string]essconform.Node{"book_id": bookID}), + }, nil + } + return essconform.CommandResult{}, fmt.Errorf("unknown command %q: %w", req.Command, essconform.ErrUnsupported) +} + +// refusal maps a library error onto the declared outcome and error. +func refusal(err error) (essconform.CommandResult, error) { + var conflict *library.StateConflictError + switch { + case errors.As(err, &conflict): + return essconform.CommandResult{Outcome: "wrong-state", Error: "library.lending.BookStateConflict"}, nil + case errors.Is(err, library.ErrBookNotFound): + return essconform.CommandResult{Outcome: "no-such-book", Error: "library.lending.BookNotFound"}, nil + } + return essconform.CommandResult{}, err +} + +func (t *target) QueryView(req essconform.ViewRequest) (essconform.ViewResult, error) { + var rows []essconform.Row + switch req.View { + case "library.lending.Catalogue": + for _, b := range t.lib.Books() { + rows = append(rows, essconform.Row{ + "book_id": b.ID, "title": b.Title, "author": b.Author, + "state": string(b.State), "borrower_id": borrower(b), + }) + } + case "library.lending.BooksOnLoan": + for _, b := range t.lib.Books() { + if b.State == library.OnLoan { + rows = append(rows, essconform.Row{ + "book_id": b.ID, "title": b.Title, "borrower_id": borrower(b), + }) + } + } + case "library.lending.Members": + for _, m := range t.lib.Members() { + rows = append(rows, essconform.Row{"member_id": m.ID, "name": m.Name}) + } + default: + return essconform.ViewResult{}, fmt.Errorf("unknown view %q: %w", req.View, essconform.ErrUnsupported) + } + return essconform.ViewResult{Rows: rows}, nil +} + +func borrower(b library.Book) essconform.Node { + if b.Borrower == nil { + return nil + } + return *b.Borrower +} + +// Every event is returned directly by the command that emits it; there is nothing to observe apart. +func (t *target) ObserveEvents(essconform.EventObservationRequest) ([]essconform.ObservedEvent, error) { + return nil, essconform.ErrUnsupported +} + +func (t *target) ConfigureExternalOutcome(essconform.ExternalOutcomeControl) error { + return essconform.ErrUnsupported +} + +func (t *target) RedeliverEvent(essconform.RedeliveryRequest) error { + return essconform.ErrUnsupported +} + +func (t *target) ObserveInvocations(essconform.InvocationObservationRequest) ([]essconform.Invocation, error) { + return nil, essconform.ErrUnsupported +} diff --git a/trials/aep-tutorial/fixture/impl/go.mod b/trials/aep-tutorial/fixture/impl/go.mod new file mode 100644 index 0000000..4b2c9c4 --- /dev/null +++ b/trials/aep-tutorial/fixture/impl/go.mod @@ -0,0 +1,3 @@ +module example.com/library + +go 1.23 diff --git a/trials/aep-tutorial/fixture/impl/library.go b/trials/aep-tutorial/fixture/impl/library.go new file mode 100644 index 0000000..db6e0a3 --- /dev/null +++ b/trials/aep-tutorial/fixture/impl/library.go @@ -0,0 +1,156 @@ +// Package library is a minimal in-memory lending library: books are added and withdrawn, members +// are registered, and a member borrows a book and returns it. +package library + +import ( + "crypto/rand" + "errors" + "fmt" +) + +// BookState is where a book is in its lifecycle. +type BookState string + +const ( + OnShelf BookState = "OnShelf" + OnLoan BookState = "OnLoan" + Withdrawn BookState = "Withdrawn" +) + +// Book is one physical book. Borrower is set only while the book is OnLoan. +type Book struct { + ID string + Title string + Author string + State BookState + Borrower *string +} + +// Member is a registered member. +type Member struct { + ID string + Name string +} + +// ErrBookNotFound is returned when no book has the given identity. +var ErrBookNotFound = errors.New("book not found") + +// ErrUnknownMember is returned when a borrow names a member nobody registered. +var ErrUnknownMember = errors.New("member not registered") + +// StateConflictError is returned when a book is not in a state the operation acts from. +type StateConflictError struct { + State BookState +} + +func (e *StateConflictError) Error() string { + return fmt.Sprintf("book is %s", e.State) +} + +// Library holds the collection and the members in memory. It is not safe for concurrent use. +type Library struct { + books map[string]*Book + members map[string]*Member + // order keeps listings stable in insertion order. + bookOrder []string + memberOrder []string +} + +// New returns an empty library. +func New() *Library { + return &Library{books: map[string]*Book{}, members: map[string]*Member{}} +} + +// AddBook puts a new book on the shelf. +func (l *Library) AddBook(title, author string) Book { + b := &Book{ID: newID(), Title: title, Author: author, State: OnShelf} + l.books[b.ID] = b + l.bookOrder = append(l.bookOrder, b.ID) + return *b +} + +// RegisterMember registers a new member. +func (l *Library) RegisterMember(name string) Member { + m := &Member{ID: newID(), Name: name} + l.members[m.ID] = m + l.memberOrder = append(l.memberOrder, m.ID) + return *m +} + +// Borrow lends a book on the shelf to a member. +// +// requireMember makes an unregistered member a refusal. The specification leaves that check to the +// implementation and its conformance suite borrows for members it never registered, so the +// conformance target turns it off. +func (l *Library) Borrow(bookID, memberID string, requireMember bool) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnShelf { + return &StateConflictError{State: b.State} + } + if requireMember { + if _, ok := l.members[memberID]; !ok { + return ErrUnknownMember + } + } + b.State = OnLoan + b.Borrower = &memberID + return nil +} + +// Return puts a book on loan back on the shelf. +func (l *Library) Return(bookID string) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnLoan { + return &StateConflictError{State: b.State} + } + b.State = OnShelf + b.Borrower = nil + return nil +} + +// Withdraw takes a book on the shelf out of the collection for good. +func (l *Library) Withdraw(bookID string) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnShelf { + return &StateConflictError{State: b.State} + } + b.State = Withdrawn + return nil +} + +// Books lists every book, in every state. +func (l *Library) Books() []Book { + out := make([]Book, 0, len(l.bookOrder)) + for _, id := range l.bookOrder { + out = append(out, *l.books[id]) + } + return out +} + +// Members lists every member. +func (l *Library) Members() []Member { + out := make([]Member, 0, len(l.memberOrder)) + for _, id := range l.memberOrder { + out = append(out, *l.members[id]) + } + return out +} + +func newID() string { + var b [16]byte + if _, err := rand.Read(b[:]); err != nil { + panic(err) + } + b[6] = b[6]&0x0f | 0x40 + b[8] = b[8]&0x3f | 0x80 + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) +} diff --git a/trials/aep-tutorial/fixture/spec/components.yaml b/trials/aep-tutorial/fixture/spec/components.yaml new file mode 100644 index 0000000..16b2f14 --- /dev/null +++ b/trials/aep-tutorial/fixture/spec/components.yaml @@ -0,0 +1,21 @@ +components: + - component: library-service + summary: Holds the collection and the members, and lends books to members. + owns: + domains: + - library.lending + accepts: + commands: + - library.lending.AddBook + - library.lending.RegisterMember + - library.lending.BorrowBook + - library.lending.ReturnBook + - library.lending.WithdrawBook + publishes: + events: + - library.lending.BookAdded + - library.lending.MemberRegistered + - library.lending.BookBorrowed + - library.lending.BookReturned + - library.lending.BookWithdrawn + reached_by: network diff --git a/trials/aep-tutorial/fixture/spec/domains/lending.yaml b/trials/aep-tutorial/fixture/spec/domains/lending.yaml new file mode 100644 index 0000000..0a332f3 --- /dev/null +++ b/trials/aep-tutorial/fixture/spec/domains/lending.yaml @@ -0,0 +1,314 @@ +domain: library.lending + +summary: A small lending library. Books are added to the collection, members are registered, and a + member borrows a book and later returns it. + +naming: + wire: lending + display: Lending + +types: + - name: library.lending.BookId + kind: newtype + of: Uuid + + - name: library.lending.MemberId + kind: newtype + of: Uuid + +entities: + # One physical book. There is no loan record: while the book is OnLoan it records which member + # has it, and the record is cleared when the book comes back. + - name: library.lending.Book + identity: + name: book_id + type: library.lending.BookId + fields: + - name: title + type: String + - name: author + type: String + - name: borrower_id + type: Optional + relations: + - name: borrower + kind: references + target: library.lending.Member + cardinality: one + via: borrower_id + lifecycle: + initial: OnShelf + states: [OnShelf, OnLoan, Withdrawn] + terminal: [Withdrawn] + transitions: + - name: lend + from: [OnShelf] + to: OnLoan + - name: return + from: [OnLoan] + to: OnShelf + - name: withdraw + from: [OnShelf] + to: Withdrawn + + # Members are registered and never leave. + - name: library.lending.Member + identity: + name: member_id + type: library.lending.MemberId + fields: + - name: name + type: String + lifecycle: + initial: Active + states: [Active] + terminal: [Active] + +actors: + - name: library.lending.Librarian + may: + - library.lending.AddBook + - library.lending.RegisterMember + - library.lending.BorrowBook + - library.lending.ReturnBook + - library.lending.WithdrawBook + naming: + display: Librarian + +errors: + - name: library.lending.BookStateConflict + summary: The book is not in a state this command acts from, so nothing changed. + fields: + - name: state + type: library.lending.Book.State + + - name: library.lending.BookNotFound + summary: No book in the collection has this identity. + fields: + - name: book_id + type: library.lending.BookId + +commands: + - name: library.lending.AddBook + naming: + wire: add-book + display: Add a book + input: + - name: title + type: String + - name: author + type: String + outcomes: + - name: added + creates: library.lending.Book + instance: book_id + sets: + title: input.title + author: input.author + borrower_id: {cleared: true} + emits: + - library.lending.BookAdded + payload: + library.lending.BookAdded: + book_id: {generated: true} + title: input.title + author: input.author + summary: The book is in the collection, on the shelf. + + - name: library.lending.RegisterMember + naming: + wire: register-member + display: Register a member + input: + - name: name + type: String + outcomes: + - name: registered + creates: library.lending.Member + instance: member_id + sets: + name: input.name + emits: + - library.lending.MemberRegistered + payload: + library.lending.MemberRegistered: + member_id: {generated: true} + name: input.name + summary: The member is registered and may borrow. + + - name: library.lending.BorrowBook + naming: + wire: borrow-book + display: Borrow a book + input: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + # The borrowing member must be a registered member. That is a condition on another entity + # (Member), which a command outcome cannot guard on. Decided: the implementation checks it; + # this specification does not, and no scenario covers it. + outcomes: + - name: borrowed + moves: library.lending.Book.lend + instance: book_id + sets: + borrower_id: input.member_id + emits: + - library.lending.BookBorrowed + payload: + library.lending.BookBorrowed: + book_id: input.book_id + member_id: input.member_id + summary: The book is on loan to the member. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is on loan or withdrawn, so it was not lent. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was lent. + + # A return names only the book; the member who brings it back is not checked. + - name: library.lending.ReturnBook + naming: + wire: return-book + display: Return a book + input: + - name: book_id + type: library.lending.BookId + outcomes: + - name: returned + moves: library.lending.Book.return + instance: book_id + sets: + borrower_id: {cleared: true} + emits: + - library.lending.BookReturned + payload: + library.lending.BookReturned: + book_id: input.book_id + summary: The book is back on the shelf and no member has it. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is not on loan, so nothing was returned. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was returned. + + - name: library.lending.WithdrawBook + naming: + wire: withdraw-book + display: Withdraw a book + input: + - name: book_id + type: library.lending.BookId + outcomes: + - name: withdrawn + moves: library.lending.Book.withdraw + instance: book_id + emits: + - library.lending.BookWithdrawn + payload: + library.lending.BookWithdrawn: + book_id: input.book_id + summary: The book has left the collection for good. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is on loan or already withdrawn, so it was not withdrawn. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was withdrawn. + +events: + - name: library.lending.BookAdded + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: author + type: String + + - name: library.lending.MemberRegistered + fields: + - name: member_id + type: library.lending.MemberId + - name: name + type: String + + - name: library.lending.BookBorrowed + fields: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + + - name: library.lending.BookReturned + fields: + - name: book_id + type: library.lending.BookId + + - name: library.lending.BookWithdrawn + fields: + - name: book_id + type: library.lending.BookId + +views: + # The whole collection, in every state, with who has each book. + - name: library.lending.Catalogue + source: library.lending.Book + consistency: read_your_writes + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: author + type: String + - name: state + type: library.lending.Book.State + - name: borrower_id + type: Optional + naming: + wire: catalogue + display: Catalogue + + - name: library.lending.Members + source: library.lending.Member + consistency: read_your_writes + fields: + - name: member_id + type: library.lending.MemberId + - name: name + type: String + naming: + wire: members + display: Members + + # The loans in force: every book on loan and the member who has it. + - name: library.lending.BooksOnLoan + source: library.lending.Book + consistency: read_your_writes + filter: state == OnLoan + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: borrower_id + type: Optional + naming: + wire: books-on-loan + display: Books on loan diff --git a/trials/aep-tutorial/fixture/spec/ess-inputs.yaml b/trials/aep-tutorial/fixture/spec/ess-inputs.yaml new file mode 100644 index 0000000..c404043 --- /dev/null +++ b/trials/aep-tutorial/fixture/spec/ess-inputs.yaml @@ -0,0 +1,7 @@ +format: ess-inputs/2 +requires: ess 0.38.0 +specification: + - system.yaml + - components.yaml + - domains/lending.yaml +scenarios: [] diff --git a/trials/aep-tutorial/fixture/spec/system.yaml b/trials/aep-tutorial/fixture/spec/system.yaml new file mode 100644 index 0000000..82586ba --- /dev/null +++ b/trials/aep-tutorial/fixture/spec/system.yaml @@ -0,0 +1,6 @@ +format: ess/15 +system: library +version: v1 + +domains: + - library.lending diff --git a/trials/aep-tutorial/fixture/tutorial.md b/trials/aep-tutorial/fixture/tutorial.md new file mode 100644 index 0000000..0f32c2d --- /dev/null +++ b/trials/aep-tutorial/fixture/tutorial.md @@ -0,0 +1,402 @@ +--- +title: Your first governed plan +sidebar_label: Your first governed plan +description: Continue the ESS tutorial's lending library with AEP. Adopt a planning store, plan a feature that adds a new state, have four critics review the plan, and implement the first story in a reviewed wave. +--- + +# Your first governed plan + +[Your first ESS specification](./first-ess-specification.md) ended with a lending library: a +specification in `spec/` and a Go implementation that passes its 17 conformance scenarios. This +tutorial continues from there with **AEP**, which keeps the plan for changing that library inside the +repository as files the `aep` command validates: an epic, the stories under it, the critics' +verdicts, the evidence that moved each story, and what is still open. + +In this tutorial you ask for one feature, *members can reserve a book that is on loan*, and follow +it from a question to a merged, reviewed change. It takes about an hour, most of it the agent's. + +**What you end with:** + +- a planning store in `.engineering/`, which `aep plan artifact validate` accepts; +- the specification extended by the agent before any story was written, with the suite grown from + 17 to 55 scenarios; +- an epic and six stories, each naming the scenarios it turns from skipped to passing, and four + critic verdicts recorded as immutable review records; +- the first story implemented in a wave, attacked by an adversary agent, merged, and moved to + `implemented` on the evidence it recorded. + +Every output block on this page is what the command printed when this page was recorded, on +2026-09-28, with `aep` 0.64.0, `ess` 0.39.0, the `aep` and `ess` plugins 0.17.0 and Claude Code. +Your run will differ in ids, counts and wording (another agent names a vision differently, or asks +six questions instead of five); the steps and what each one records are what repeat. Paths are shortened to `~`. The recorded session cost $5.48 for the plan and critics, $6.34 for +accepting the stories, and $13.19 for the wave. + +## What you need + +- The `library/` directory from [the ESS tutorial](./first-ess-specification.md), committed to Git. +- Claude Code or Codex, and Go 1.23 or newer. + +## 1. Install `aep` and its plugin + +If you did the ESS tutorial through `b10x`, add the `aep` product: + +```shell-session +$ b10x init aep,ess --host claude --out plan.json +``` + +The actions it plans (the rest of the plan lists what is already installed): + +```text +Actions (6): + 1. register marketplace `b10x`: claude plugin marketplace add beyond10x/agentplugins + 2. install `b10x@b10x`: claude plugin install b10x@b10x --scope user + 3. install `aep@b10x`: claude plugin install aep@b10x --scope user + 4. install `ess@b10x`: claude plugin install ess@b10x --scope user + 5. install `aep`: install aep 0.64.0 into ~/.local/bin (prebuilt archive) + 6. install `ess`: install ess 0.39.0 into ~/.local/bin (prebuilt archive) + +``` + +```shell-session +$ b10x setup apply --plan plan.json --yes +``` + +Restart Claude Code so it loads the plugin. `aep@b10x` brings the `aep:planning` skill, which owns +the store, and `aep:implementing`, which runs waves. + +## 2. Adopt a planning store + +Ask: + +```text +This repository has an ESS specification in spec/ and a Go implementation in impl/. Set up AEP +planning here so the work on it is planned in the repository. Tell me what you did. +``` + +The agent ran `aep plan reverse init`, which wrote `.engineering/project.yaml` in the current store +format (`aep.project/5`) and pinned the shared planning rules to the exact commit of the `aep` +release. Then its `aep:reverse-engineer` agent read the repository and its history and drafted the +first artifacts, each citing the line it came from: + +```shell-session +$ aep plan artifact list +``` + +```text +executable-system-specification:library executable-system-specification draft library v1 — ESS specification of the lending domain +story:borrow-member-check-tested story draft The registered-member check on Borrow is exercised by a test +vision:lending-library vision draft A small lending library, specified in ESS and implemented in Go +``` + +It also found a real gap: the check that a borrower is a registered member is never exercised +(`impl/conformance_test.go:90` always passes `false`), and filed it as a story instead of fixing it +unasked. It ended with five questions the code could not answer, such as whether a network service +is owed (your run may ask a different number). Commit the store before you go on: it is a plan, and a plan belongs in version control. + +## 3. Ask for a feature + +```text +Members should be able to reserve a book that is on loan, so that when it comes back it is held +for them instead of going back on the shelf. Plan this change. Before you write anything, ask me the +decisions you need from me, then stop and wait for my answers. +``` + +A reservation changes the book's lifecycle, so `aep:planning` will not write a story around it +until the specification says what a reservation is. It asked first, each question with the answer +it would take: + +
+The agent's questions (9) + +I read `spec/domains/lending.yaml` and `impl/library.go`. Right now a Book can only be `OnShelf → OnLoan → OnShelf` (or `Withdrawn`). The borrower is stored on the Book itself, there's no loan record, and the only actor in the spec is the Librarian. Reservations change the Book's lifecycle, so I need these decided before I model it. I've put a suggested default on each, so you can answer "defaults, except 3b" if that's easier. + +**The model** + +1. **How many reservations can a book have?** Just one at a time, or a queue served first come, first served? This decides whether a reservation is a field on Book (`reserved_for_id`) or its own entity with a one-to-many relation to Book. I won't guess this one. *Default: one at a time; a second attempt is refused.* +2. **What does a returned, reserved book become?** I'd add a new Book state, `OnHold`, alongside `OnShelf`. The alternative is to keep it `OnShelf` with a hold marker on it. *Default: a new state `OnHold`.* + +**The lifecycle** + +3. **How does a held book stop being held?** + a. The member who reserved it borrows it: `OnHold → OnLoan`. When anyone else tries to borrow it, should that be a `BookStateConflict` or a new error such as `BookHeldForAnotherMember`? *Default: a new error, since it tells the librarian why.* + b. Is there a way to release the hold, putting the book back on the shelf (`OnHold → OnShelf`), for when the member never comes to collect it? If so, who does it? Expiring holds after N days would need a clock, and the spec has none. *Default: a manual `ReleaseHold` command for the Librarian; no time-based expiry.* +4. **Can a reservation be cancelled while the book is still on loan?** *Default: yes, with a `CancelReservation` command.* +5. **Can a held book be withdrawn?** *Default: no. Release the hold first; withdraw keeps acting only from `OnShelf`.* + +**The rules** + +6. **Who places a reservation?** The only actor in the spec is the Librarian. Do they reserve on a member's behalf, or should I add a Member actor who can reserve for themselves? *Default: the Librarian, as with borrowing.* +7. **Which reservations are refused?** The member already has the book on loan? The book is on the shelf? (Your wording suggests on-loan books only, so the book would just be borrowed instead.) *Default: refuse both.* +8. **Must the reserving member be registered?** For borrowing, that check was left to the implementation and is off in the conformance target, which is the untested gap the draft story covers. Should reservations do the same, or should the spec model the check this time? *Default: the same as borrowing, for consistency.* + +**What people can see** + +9. **Views and events.** Should the Catalogue show `reserved_for_id`? Should there be a new "held awaiting pickup" view? And when a reserved book comes back, should the return emit `BookReturned` plus `BookHeld`, or have its own outcome? *Default: add the field to the Catalogue, add a `BooksOnHold` view, and give the return a separate `held` outcome that emits `BookHeld`.* + +Once you've answered, I'll update the ESS domain and validate it. Then I'll create an epic that cites the updated spec and break it down into stories. A panel of four review agents will check the stories for testability, overlap, scope and parallel work, and I'll write up anything they find that I didn't fix. I won't touch any file until you reply. + +
+ +This page answered: + +```text +Defaults for all nine. Go ahead: model it, plan it, and have the critics review the plan. Tell me +what you did and what is still open. +``` + +## 4. What it planned + +**The specification first.** The agent extended `spec/domains/lending.yaml` before drafting a +single story: two new book states, a `reserved_for_id` field, four commands, two errors, a +`BookHeld` event and a `BooksOnHold` view. The book's lifecycle now reads: + +```yaml + lifecycle: + initial: OnShelf + states: [OnShelf, OnLoan, OnLoanReserved, OnHold, Withdrawn] + terminal: [Withdrawn] + transitions: + - name: lend + from: [OnShelf] + to: OnLoan + - name: return + from: [OnLoan] + to: OnShelf + - name: reserve + from: [OnLoan] + to: OnLoanReserved + - name: cancel_reservation + from: [OnLoanReserved] + to: OnLoan + - name: return_to_hold + from: [OnLoanReserved] + to: OnHold + - name: collect + from: [OnHold] + to: OnLoan + - name: release_hold + from: [OnHold] + to: OnShelf + - name: withdraw + from: [OnShelf] + to: Withdrawn +``` + +It reported one deviation from the answers it had proposed: taking a held book out is its own +command, `CollectHold`, because one ESS command cannot both depend on the book's state and check who +the book is held for. It wrote that into the epic instead of bending the model. + +```shell-session +$ ess verify conform synthesize --path spec --target go --out impl +``` + +```text +warning: spec/ess-inputs.yaml requires ess 0.38.0 and this is ess 0.39.0, which is newer; continuing (--strict-requires refuses) +55 scenario(s) (0 authored), 0 refusal(s), 7 file(s) written to impl +``` + +**Then the plan.** An epic, `epic:book-reservations`, and six stories drafted by the +`aep:decomposer` agent. Each story's acceptance names the exact scenarios it turns from skipped to +passing; together they cover all 55 once. + +**Then the critics.** Four agents read the drafted set at once, none seeing another's verdict: +acceptance (can each story be checked?), design (coupling, cycles, a split abstraction), scope +(everything the epic promised is claimed, nothing else), and parallel safety (which stories land on +one file). All four approved in the first round, and each verdict is stored word for word as a +`review-result` that cannot be edited afterwards. + +```shell-session +$ aep plan artifact validate +``` + +This output was taken at the end of the recording, after the wave had added two adversary records +(14 artifacts at this point, 16 below): + +```text +16 file(s) in ~/library/.engineering/planning: 16 artifact(s) +4 review(s) recorded no findings block: + - review-result:acceptance-round-1 states its findings as prose only — nothing can enumerate what it found, so the next review starts from nowhere + - review-result:design-round-1 states its findings as prose only — nothing can enumerate what it found, so the next review starts from nowhere + - review-result:parallel-safety-round-1 states its findings as prose only — nothing can enumerate what it found, so the next review starts from nowhere + - review-result:scope-round-1 states its findings as prose only — nothing can enumerate what it found, so the next review starts from nowhere +valid +``` + +`valid` is the verdict. The four warnings above it come from `aep` itself: it counts a review that +ended with an empty findings block as having none (beyond10x/aep#60). + +## 5. Accept the stories and propose a wave + +```text +Leave the open points as they are for now. Commit what you did. Then accept the six reservation +stories and propose the first wave with aep:implementing: tell me which stories are in it and why, +and stop there until I approve. +``` + +Accepting a story is a lifecycle move through `aep plan artifact move`, which the store validates. +In this recording, moving the first story to `active` was refused, +`story:reservations-suite-baseline is proposed and serves no objective`, so the agent linked every +story to the vision it serves and moved them again. The decomposer now draws that link when it drafts +a story, so your store will not refuse the move. The agent committed the plan on a branch of its +own, `book-reservations`, which becomes the wave's base. + +A **wave** is the set of stories that can be built at the same time without touching the same file. +The store derives it from each story's declared scope: the files a story lands on, which the +`aep:story-scoper` agent works out and records. If `waves` answers `nothing selected declares a scope`, +ask the agent to scope the stories first. + +```shell-session +$ aep plan artifact waves --kind story --status active +``` + +```text +wave 1 + story:reserve-book-on-loan +wave 2 + story:return-reserved-book-to-hold +wave 3 + story:cancel-reservation +wave 4 + story:collect-held-book +wave 5 + story:release-hold +collision: story:cancel-reservation story:collect-held-book impl/conformance_test.go +collision: story:cancel-reservation story:collect-held-book impl/library.go +collision: story:cancel-reservation story:release-hold impl/conformance_test.go +collision: story:cancel-reservation story:release-hold impl/library.go +collision: story:cancel-reservation story:reserve-book-on-loan impl/conformance_test.go +collision: story:cancel-reservation story:reserve-book-on-loan impl/library.go +collision: story:cancel-reservation story:return-reserved-book-to-hold impl/conformance_test.go +collision: story:cancel-reservation story:return-reserved-book-to-hold impl/library.go +collision: story:collect-held-book story:release-hold impl/conformance_test.go +collision: story:collect-held-book story:release-hold impl/library.go +collision: story:collect-held-book story:reserve-book-on-loan impl/conformance_test.go +collision: story:collect-held-book story:reserve-book-on-loan impl/library.go +collision: story:collect-held-book story:return-reserved-book-to-hold impl/conformance_test.go +collision: story:collect-held-book story:return-reserved-book-to-hold impl/library.go +collision: story:release-hold story:reserve-book-on-loan impl/conformance_test.go +collision: story:release-hold story:reserve-book-on-loan impl/library.go +collision: story:release-hold story:return-reserved-book-to-hold impl/conformance_test.go +collision: story:release-hold story:return-reserved-book-to-hold impl/library.go +collision: story:reserve-book-on-loan story:return-reserved-book-to-hold impl/conformance_test.go +collision: story:reserve-book-on-loan story:return-reserved-book-to-hold impl/library.go +5 wave(s), 20 collision(s), 0 unassessed +``` + +Every pair of stories lands on the same two Go files, so each wave holds one story. (This listing +was taken after wave 1, which held the sixth story, `reservations-suite-baseline`.) The agent +proposed that wave, named every commit your approval would authorise and nothing more (no push, no +tag, no second wave), and asked three things: which branch to use as the base, whether plain +`git worktree` is acceptable (the `worktree` plugin, `b10x init worktree`, gives it managed +worktrees instead), and how much model budget is left. + +## 6. Run the wave + +```text +Approved. 1: use book-reservations as the base. 2: plain `git worktree add` is fine here. 3: the +budget is not a limit for this wave. Implement the first wave, with the adversary review, and stop +when it is merged. Tell me what happened, including anything the adversary found. +``` + +What the wave did, in order: + +1. The `aep:implementor` agent built the story in its own worktree: the smallest change that turns + its 17 scenarios from skipped to passing. +2. The claim was checked against the base: the same test command before and after. 10 of 17 passed + before, 17 of 17 after, none failed. That comparison is recorded as `verification` evidence. +3. The `aep:adversary` agent tried to break the change, for up to two passes. It found no failing + behaviour, but three gaps in the tests: the new view code was never exercised, a doc comment + misdescribed `Return`, and nothing tested that `Return` refuses the two new states. Each was + fixed by a test, and each finding's outcome was recorded. +4. The unit merged into the wave's integration branch, the gate ran there, and the integration + branch merged into the base. + +```shell-session +$ git log --oneline --graph +``` + +```text +* 9dce7df Merge wave-1/integration into book-reservations +|\ +| * 950900a Wave 1 closed: story:reservations-suite-baseline implemented +| * aee0d43 Merge impl/reservations-suite-baseline into wave-1/integration +|/| +| * c3bebc2 Pin Return's refusal of the two new states +| * e7658b7 Pin the reservation views with tests that force the new states +| * f0cc3e2 Add reservation states and answer the reservation views +|/ +* 3163f1b Wave 1: accept reservation stories, open wave page +* 7d2a962 Plan book reservations: ESS model, epic, six stories, critic round 1 +* e89b41c Adopt AEP planning +* 30e5ba2 Lending library: ESS specification and Go implementation +``` + +Ask the store why the story is where it is: + +```shell-session +$ aep plan artifact explain story:reservations-suite-baseline +``` + +```text +story:reservations-suite-baseline in ~/library/.engineering/planning: implemented, revision 7 + draft -> proposed 2026-09-28T13:54:57Z (revision 3) + no record: nothing was recorded about how this was decided + proposed -> active 2026-09-28T13:54:57Z (revision 4) + no record: nothing was recorded about how this was decided + active -> implemented 2026-09-28T14:09:35Z (revision 7) + review_outcome from review-result:adversary-wave-1-u1-pass-1, observed 2026-09-28T14:04:39Z, admitted at revision 4 + review_outcome from review-result:adversary-wave-1-u1-pass-1, observed 2026-09-28T14:04:40Z, admitted at revision 4 + verification from go test -count=1 -v ./... in impl/, impl/essconform regenerated from spec/ (55 scenarios): acceptance scenarios PASS 10/17 at baseline 3163f1b, 17/17 at c3bebc2; 0 FAIL both (.engineering/drafts/wave-1/integration/scratch/baseline.txt vs .engineering/drafts/wave-1/u1/scratch/treatment.txt), observed 2026-09-28T14:09:02Z, admitted at revision 4 + review_outcome from review-result:adversary-wave-1-u1-pass-2, observed 2026-09-28T14:09:02Z, admitted at revision 4 + test_result from gate on wave-1/integration at merge aee0d43: aep plan artifact validate (valid); ess specify validate (valid); ess verify conform synthesize --target go --out impl (55 scenarios, 0 refusals); go vet ./... (exit 0); go test -count=1 -v ./... (ok; TestConformance 17 PASS / 38 SKIP / 0 FAIL, 3 unit tests PASS); go run cmd/gofmt -l impl (empty, exit 0) (aee0d43), observed 2026-09-28T14:09:35Z, admitted at revision 6 + next: archived needs no record +``` + +Every move is there with what the store admitted before it: the adversary's two recorded outcomes, +the before/after verification, and the gate that ran on the merge. + +```shell-session +$ aep plan artifact board --kind story +``` + +```text +draft (1) + story:borrow-member-check-tested The registered-member check on Borrow is exercised by a test + +active (5) + story:cancel-reservation A librarian cancels a reservation while the book is still on loan + story:collect-held-book The member a book is held for collects it + story:release-hold A librarian releases a hold and the book goes back on the shelf + story:reserve-book-on-loan A librarian reserves a book on loan for a member + story:return-reserved-book-to-hold A returned reserved book is held for the member who reserved it + +implemented (1) + story:reservations-suite-baseline Existing lending behaviour passes the regenerated reservations suite +``` + +## 7. Keep going + +Five stories are `active`, and `aep plan artifact waves` already says which comes next. Ask for the +next wave the same way. Each wave ends with the suite a little greener: + +```shell-session +$ cd impl && ESS_REPORT_FORMAT=2 go test ./... +``` + +```text +ok example.com/library 0.043s +? example.com/library/essconform [no test files] +``` + +The open points from steps 2 and 4 stay in the store until somebody answers them: a store keeps an +unanswered question as a record instead of a guess. + +## Next + +- **The longer walk.** The [golden path](../golden-path.md) goes through the same steps on a larger + repository, and ends by handing one story to `metaharness aep drive`, where an engine rather than + the agent decides every step. +- **Test the suite itself.** `ess:hardening` runs `ess verify conform mutate` and the concurrent + explorer against your implementation. +- **Go deeper into AEP.** The [AEP documentation](https://beyond10x.github.io/docs/aep/) covers + lifecycles, evidence and the planning commands. diff --git a/trials/aep-tutorial/trial.yaml b/trials/aep-tutorial/trial.yaml new file mode 100644 index 0000000..daf8ddf --- /dev/null +++ b/trials/aep-tutorial/trial.yaml @@ -0,0 +1,25 @@ +name: aep-tutorial +kind: aep-tutorial +prompt: | + I'm following the tutorial in `tutorial.md`. This repository is the lending library from the ESS + tutorial it starts from, and step 1 (setup) is done. Work through steps 2 to 6 here as the page + describes: adopt a planning store, plan the reservation feature with the specification first, + have the four critics review the plan, accept the stories, and run the first wave through to the + merge. Where the page shows the questions and the answers it gave, take the same answers instead + of asking me; where it approves a wave, approve it the same way. + + When you're done, tell me which story the wave implemented and what the adversary found, quote any + text on the page that confused you or did not match what you saw, and paste the last + `aep plan artifact validate` output, the last synthesis output and the last `go test` summary + verbatim. +dir: library +fixture: fixture +setup: + - b10x init aep,ess --host claude --out plan.json + - b10x setup apply --plan plan.json --yes +measures: [tool_calls, synthesis, outputs] +outputs: + project: .engineering/project.yaml + epic: .engineering/planning/epic + reviews: .engineering/planning/review-result + suite: impl/essconform diff --git a/trials/baseline.json b/trials/baseline.json index 8f21ae0..14b038c 100644 --- a/trials/baseline.json +++ b/trials/baseline.json @@ -2,6 +2,19 @@ "aep-backlog": { "tool_calls": 278 }, + "aep-tutorial": { + "tool_calls": 474, + "synthesis": { + "scenarios": 55, + "refusals": 0 + }, + "outputs": [ + "epic", + "project", + "reviews", + "suite" + ] + }, "ess-full-package": { "tool_calls": 53, "validate": "valid", diff --git a/website/docs/tutorials/first-governed-plan.md b/website/docs/tutorials/first-governed-plan.md index d16658b..0f32c2d 100644 --- a/website/docs/tutorials/first-governed-plan.md +++ b/website/docs/tutorials/first-governed-plan.md @@ -27,7 +27,8 @@ it from a question to a merged, reviewed change. It takes about an hour, most of Every output block on this page is what the command printed when this page was recorded, on 2026-09-28, with `aep` 0.64.0, `ess` 0.39.0, the `aep` and `ess` plugins 0.17.0 and Claude Code. -Paths are shortened to `~`. The recorded session cost $5.48 for the plan and critics, $6.34 for +Your run will differ in ids, counts and wording (another agent names a vision differently, or asks +six questions instead of five); the steps and what each one records are what repeat. Paths are shortened to `~`. The recorded session cost $5.48 for the plan and critics, $6.34 for accepting the stories, and $13.19 for the wave. ## What you need @@ -90,7 +91,7 @@ vision:lending-library vision draft It also found a real gap: the check that a borrower is a registered member is never exercised (`impl/conformance_test.go:90` always passes `false`), and filed it as a story instead of fixing it unasked. It ended with five questions the code could not answer, such as whether a network service -is owed. Commit the store before you go on: it is a plan, and a plan belongs in version control. +is owed (your run may ask a different number). Commit the store before you go on: it is a plan, and a plan belongs in version control. ## 3. Ask for a feature @@ -208,6 +209,9 @@ one file). All four approved in the first round, and each verdict is stored word $ aep plan artifact validate ``` +This output was taken at the end of the recording, after the wave had added two adversary records +(14 artifacts at this point, 16 below): + ```text 16 file(s) in ~/library/.engineering/planning: 16 artifact(s) 4 review(s) recorded no findings block: @@ -230,11 +234,16 @@ and stop there until I approve. ``` Accepting a story is a lifecycle move through `aep plan artifact move`, which the store validates. -Moving the first story to `active` was refused, `story:reservations-suite-baseline is proposed and -serves no objective`, so the agent linked every story to the vision it serves and moved them again. +In this recording, moving the first story to `active` was refused, +`story:reservations-suite-baseline is proposed and serves no objective`, so the agent linked every +story to the vision it serves and moved them again. The decomposer now draws that link when it drafts +a story, so your store will not refuse the move. The agent committed the plan on a branch of its +own, `book-reservations`, which becomes the wave's base. A **wave** is the set of stories that can be built at the same time without touching the same file. -The store derives it from each story's declared scope: +The store derives it from each story's declared scope: the files a story lands on, which the +`aep:story-scoper` agent works out and records. If `waves` answers `nothing selected declares a scope`, +ask the agent to scope the stories first. ```shell-session $ aep plan artifact waves --kind story --status active @@ -278,7 +287,8 @@ Every pair of stories lands on the same two Go files, so each wave holds one sto was taken after wave 1, which held the sixth story, `reservations-suite-baseline`.) The agent proposed that wave, named every commit your approval would authorise and nothing more (no push, no tag, no second wave), and asked three things: which branch to use as the base, whether plain -`git worktree` is acceptable, and how much model budget is left. +`git worktree` is acceptable (the `worktree` plugin, `b10x init worktree`, gives it managed +worktrees instead), and how much model budget is left. ## 6. Run the wave From 836d63a302eef7bac77b4b24200fa5a9f475b936 Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:17:42 +0200 Subject: [PATCH 10/10] chore: release 0.18.0 - Workspace, all ten plugin manifests and the Skill version lines move to 0.18.0; CHANGELOG gains the 0.18.0 section. task check: exit 0. agentplugins-check tools: exit 0 (aep 0.64.0: 149, ess 0.39.0: 45, worktree 0.8.2: 19 spelled commands; the ESS tutorial's spec, suite and go test pass). Trials: ess-new, ess-retrofit (after the identity-view fix), ess-pipeline, ess-full-package, ess-tutorial and aep-backlog within the baseline; aep-tutorial complete, baseline recorded. --- CHANGELOG.md | 29 +++++++++++++++++++ Cargo.lock | 4 +-- Cargo.toml | 2 +- plugins/aep/.claude-plugin/plugin.json | 2 +- plugins/aep/.codex-plugin/plugin.json | 2 +- plugins/aep/skills/diagnosing/SKILL.md | 2 +- plugins/aep/skills/implementing/SKILL.md | 2 +- plugins/aep/skills/migrating/SKILL.md | 2 +- plugins/aep/skills/planning/SKILL.md | 2 +- plugins/b10x/.claude-plugin/plugin.json | 2 +- plugins/b10x/.codex-plugin/plugin.json | 2 +- plugins/connectors/.claude-plugin/plugin.json | 2 +- plugins/connectors/.codex-plugin/plugin.json | 2 +- plugins/ess/.claude-plugin/plugin.json | 2 +- plugins/ess/.codex-plugin/plugin.json | 2 +- plugins/worktree/.claude-plugin/plugin.json | 2 +- plugins/worktree/.codex-plugin/plugin.json | 2 +- 17 files changed, 46 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd34ee5..ce7455e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## [0.18.0] — 2026-09-28 + +A second tutorial continues the first: the ESS tutorial's library gets an AEP plan, four critics +review it, and the first story is built in a reviewed wave. The skills are verified against aep +0.64.0 and ess 0.39.0, and a new report and skill keep this repository in step with the releases +it depends on. + +- New page: [Your first governed plan](website/docs/tutorials/first-governed-plan.md), recorded with + aep 0.64.0 and ess 0.39.0: adopt an `aep.project/5` store, model a new state in ESS before any + story is written (17 → 55 scenarios), six stories, four critic verdicts, a wave proposal, and one + story implemented, attacked by the adversary and merged. The `aep-tutorial` trial has a fresh + agent follow it. +- `agentplugins-check upstream` reports what moved in every repository this one depends on: the + releases its plugins drive with their changelog sections, the `beyond10x/*` workflow pins, and + the issues its text cites. `.agents/skills/following-upstream` is the loop that acts on it. +- `agentplugins-check` refuses a `b10x.docs.yaml` link that names no page. One such link + (`plugins/beyond10x/`, renamed in 0.14.0) failed every organization website publication from + 2026-09-24 to 2026-09-28. +- `verified.json` pins aep 0.64.0 and ess 0.39.0. `ess:retrofitting` and `syntax.md`: a read by + identity is the unfiltered view; `filter: id == param.id` leaves every outcome it observes + unsynthesized (beyond10x/ess#193). `ess:hardening` covers the concurrent explorer and + `ess verify conform check-history`. +- `aep:planning` checks each critic's verdict line before recording it; the decomposer relates each + story to the epic's objective, which `development.standard` requires before a story is accepted. +- Eval recording moved to `metaharness aep drive eval run` with aep 0.64.0; it is blocked until + metaharness links a current aep (beyond10x/metaharness#10). The four plan-critic cases have a + working tree, `fixtures/library-reservations-drafted`. +- `task trial:sandbox` can recreate a sandbox that ran `aep`. + ## [0.17.0] — 2026-09-28 A tutorial takes a developer from an empty directory to a validated ESS specification and a Go diff --git a/Cargo.lock b/Cargo.lock index 4ee420d..1e7ead1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "agentplugins-check" -version = "0.17.0" +version = "0.18.0" dependencies = [ "clap", "serde", @@ -76,7 +76,7 @@ dependencies = [ [[package]] name = "b10x" -version = "0.17.0" +version = "0.18.0" dependencies = [ "clap", "serde", diff --git a/Cargo.toml b/Cargo.toml index 3f62033..5ce4e99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/agentplugins-check", "crates/b10x"] [workspace.package] -version = "0.17.0" +version = "0.18.0" edition = "2021" rust-version = "1.85" license = "Apache-2.0" diff --git a/plugins/aep/.claude-plugin/plugin.json b/plugins/aep/.claude-plugin/plugin.json index 59e1031..c8bfb3d 100644 --- a/plugins/aep/.claude-plugin/plugin.json +++ b/plugins/aep/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "aep", "displayName": "AEP", "description": "Plan governed work in the AEP artifact store and deliver it in reviewed waves: decomposition, plan critique, reverse engineering, story scoping, implementation and adversarial review.", - "version": "0.17.0", + "version": "0.18.0", "author": { "name": "Beyond10x" }, diff --git a/plugins/aep/.codex-plugin/plugin.json b/plugins/aep/.codex-plugin/plugin.json index bd31949..64c3b24 100644 --- a/plugins/aep/.codex-plugin/plugin.json +++ b/plugins/aep/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "aep", - "version": "0.17.0", + "version": "0.18.0", "description": "Plan governed work in the AEP artifact store and deliver it in reviewed waves.", "author": { "name": "Beyond10x" diff --git a/plugins/aep/skills/diagnosing/SKILL.md b/plugins/aep/skills/diagnosing/SKILL.md index 896024a..e646c4f 100644 --- a/plugins/aep/skills/diagnosing/SKILL.md +++ b/plugins/aep/skills/diagnosing/SKILL.md @@ -3,7 +3,7 @@ name: diagnosing description: Diagnose a hard bug or a performance regression by building a red-capable feedback loop before any hypothesis, then ranked falsifiable hypotheses, one-variable probes, a regression test at the right seam, and evidence recorded in the AEP store. Use when the user says diagnose, debug, "why is this failing", "this is slow", or reports something broken, throwing, flaky or slower than before. Not for a failing CI job whose cause is already named in its log, and not for raising conformance coverage, which is `ess:testing-conformance`. --- -**Skill version 0.17.0** — the version in `.claude-plugin/plugin.json`. +**Skill version 0.18.0** — the version in `.claude-plugin/plugin.json`. # Diagnosing a failure diff --git a/plugins/aep/skills/implementing/SKILL.md b/plugins/aep/skills/implementing/SKILL.md index 00d3fc4..ba5e690 100644 --- a/plugins/aep/skills/implementing/SKILL.md +++ b/plugins/aep/skills/implementing/SKILL.md @@ -3,7 +3,7 @@ name: implementing description: Implement accepted AEP work, in one of two modes. A wave picks the stories that can be implemented at once, proposes the wave for approval, dispatches one implementor per story into its own worktree, sends each result to the adversary and merges what goes green. A drive hands one story to a governed `metaharness aep drive` run and reports the run id. Use when the operator asks to implement, build or deliver planned stories, to pick or start the next wave, to implement several stories in parallel or fan out across sub-agents, to drive a story or start a governed run, or asks why a wave's rules are instructions and a drive's are enforced. A wave proposes first and stops; a drive starts one run and reports; neither moves an artifact itself. --- -**Skill version 0.17.0** — the version in `.claude-plugin/plugin.json`; a wave's stage-1 proposal quotes it. +**Skill version 0.18.0** — the version in `.claude-plugin/plugin.json`; a wave's stage-1 proposal quotes it. # Implementing accepted work diff --git a/plugins/aep/skills/migrating/SKILL.md b/plugins/aep/skills/migrating/SKILL.md index 37ea32d..1aaadfd 100644 --- a/plugins/aep/skills/migrating/SKILL.md +++ b/plugins/aep/skills/migrating/SKILL.md @@ -3,7 +3,7 @@ name: migrating description: Migrate a repository's legacy work tracking — story trees, TODO.md, plan and issue documents — into the governed AEP planning store, without deleting or rewriting the sources. Use when the user asks to migrate, import, port or convert an existing backlog into AEP, when a repository is adopting AEP and already has work written down somewhere, or when a store has been adopted beside a legacy backlog nobody retired. Read it before creating the first artifact in a repository that already tracks work in markdown. --- -**Skill version 0.17.0** — the version in `.claude-plugin/plugin.json`. +**Skill version 0.18.0** — the version in `.claude-plugin/plugin.json`. # Migrating legacy tracking into the store diff --git a/plugins/aep/skills/planning/SKILL.md b/plugins/aep/skills/planning/SKILL.md index 7384035..9e6b716 100644 --- a/plugins/aep/skills/planning/SKILL.md +++ b/plugins/aep/skills/planning/SKILL.md @@ -3,7 +3,7 @@ name: planning description: Plan engineering work in a governed markdown artifact store — create, relate, move and validate epics, stories, tasks and initiatives through the `aep` CLI. Use when the user mentions planning, a backlog, an epic, a story, a task, decomposing or breaking down work, an artifact's status ("move this to active", "what is still in draft?", "why can't this be implemented?"), or when the project contains a `.engineering/planning/` directory. Use it at adoption too — the user asks to adopt AEP, to migrate from or replace the track plugin, to start a first backlog, or works in a repository with no `.engineering/` directory at all — because § 5 says how a first store is populated and it is worth nothing after one has been hand-written. Also use before editing any file under `.engineering/planning/`. --- -**Skill version 0.17.0** — the version in `.claude-plugin/plugin.json`. +**Skill version 0.18.0** — the version in `.claude-plugin/plugin.json`. # Planning in a governed artifact store diff --git a/plugins/b10x/.claude-plugin/plugin.json b/plugins/b10x/.claude-plugin/plugin.json index bc55130..d5860c3 100644 --- a/plugins/b10x/.claude-plugin/plugin.json +++ b/plugins/b10x/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "b10x", "displayName": "Beyond10x", "description": "Set up, upgrade and check the Beyond10x plugins and binaries, route work to them, and create portable plugins.", - "version": "0.17.0", + "version": "0.18.0", "author": { "name": "Beyond10x" }, diff --git a/plugins/b10x/.codex-plugin/plugin.json b/plugins/b10x/.codex-plugin/plugin.json index 5bdc349..6051b9f 100644 --- a/plugins/b10x/.codex-plugin/plugin.json +++ b/plugins/b10x/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "b10x", - "version": "0.17.0", + "version": "0.18.0", "description": "Set up, upgrade and check the Beyond10x plugins and binaries, route work to them, and create portable plugins.", "author": { "name": "Beyond10x" diff --git a/plugins/connectors/.claude-plugin/plugin.json b/plugins/connectors/.claude-plugin/plugin.json index a0a6cc3..8e69e9f 100644 --- a/plugins/connectors/.claude-plugin/plugin.json +++ b/plugins/connectors/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "connectors", - "version": "0.17.0", + "version": "0.18.0", "description": "Set up, inspect, and invoke governed integrations through the connectors CLI.", "author": { "name": "Beyond10x" }, "license": "Apache-2.0", diff --git a/plugins/connectors/.codex-plugin/plugin.json b/plugins/connectors/.codex-plugin/plugin.json index a314214..54d03e8 100644 --- a/plugins/connectors/.codex-plugin/plugin.json +++ b/plugins/connectors/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "connectors", - "version": "0.17.0", + "version": "0.18.0", "description": "Set up, inspect, and invoke governed integrations through the connectors CLI.", "author": { "name": "Beyond10x" }, "license": "Apache-2.0", diff --git a/plugins/ess/.claude-plugin/plugin.json b/plugins/ess/.claude-plugin/plugin.json index ca7b9e5..e29228c 100644 --- a/plugins/ess/.claude-plugin/plugin.json +++ b/plugins/ess/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "ess", "displayName": "ESS", "description": "Write, retrofit, validate and project Executable System Specifications, and hold implementations to them with conformance suites.", - "version": "0.17.0", + "version": "0.18.0", "author": { "name": "Beyond10x" }, diff --git a/plugins/ess/.codex-plugin/plugin.json b/plugins/ess/.codex-plugin/plugin.json index e509402..4aef5ea 100644 --- a/plugins/ess/.codex-plugin/plugin.json +++ b/plugins/ess/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ess", - "version": "0.17.0", + "version": "0.18.0", "description": "Write, retrofit, validate and project Executable System Specifications, and hold implementations to them with conformance suites.", "author": { "name": "Beyond10x" diff --git a/plugins/worktree/.claude-plugin/plugin.json b/plugins/worktree/.claude-plugin/plugin.json index 8835160..8e080f5 100644 --- a/plugins/worktree/.claude-plugin/plugin.json +++ b/plugins/worktree/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "worktree", "displayName": "Worktree", "description": "Create, lease, finish, audit and safely clean isolated Git worktrees through the worktree CLI.", - "version": "0.17.0", + "version": "0.18.0", "author": { "name": "Beyond10x" }, diff --git a/plugins/worktree/.codex-plugin/plugin.json b/plugins/worktree/.codex-plugin/plugin.json index 23599a9..68883f3 100644 --- a/plugins/worktree/.codex-plugin/plugin.json +++ b/plugins/worktree/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "worktree", - "version": "0.17.0", + "version": "0.18.0", "description": "Create, lease, finish, audit and safely clean isolated Git worktrees through the worktree CLI.", "author": { "name": "Beyond10x"