diff --git a/CHANGELOG.md b/CHANGELOG.md index c57d115..d5a430e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## [0.16.0] — 2026-09-28 + +The `ess`, `aep` and `worktree` plugins describe the newest CLI releases: ess 0.37.0, aep 0.63.1 +and worktree 0.8.2. `verified.json` pins all three. + +- `aep` describes the Git-native store, `aep.project/5`. The artifact files are the store, a move + appends to `transitions`, and each evidence record is one file under `.engineering/evidence/`. + `aep:planning` and `aep:implementing` read `.engineering/project.yaml` before the first write, and + tell the user once, with the exact upgrade, when the store is not `/5`: `/1` migrates with + `aep plan store migrate git --verify`, `/2`–`/4` through the pinned build named in + `aep:upgrade`. `aep:init`, adoption and the reverse engineer start new stores at `/5`. Reasons + that rested on the old append-only journal now rest on one writer and the `revision:` conflict. +- `ess:hardening` uses `ess verify conform mutate`, with `--emit` and `--collect` for a project's + own runner, and the Go and TypeScript explorers. It no longer says the audit is unshipped. A + surviving mutant is answered by making the rule observable or filing a synthesis gap, never by + an authored scenario. +- `ess:specifying` gains `references/later-formats.md`: fixtures, value expressions, the outcome + shapes (`unknown_instance:`, `deletes:`, `into:`, `accepts: nothing`, `preconditions:`), + `presence:`, `Json`, `prefix:`, `skip_absent`, case-insensitive guards, `when_subject_state:` + with its limits, and a per-holder limit ("at most five per member"). Each example validates and + synthesizes under ess 0.37.0. `syntax.md` gives the unknown-instance order, `instance:` on a + create and on a move, `params:` on a view, and the one-default rule for outcome `when`s. +- `ess:retrofitting` maps a command that ignores in one state and refuses in another. `ess:init` + and `ess:upgrade` cover `ess specify toolchain` and an exact `requires: ess` pin. + `ess:testing-conformance` covers fixture providers and the suite versions the Go and TypeScript + runtimes accept. +- The specification interview skips its questions when the request asks for a finished + specification. Without an AEP store it lists the decisions it took in the report. +- `worktree:managing-worktrees` covers what `archive` includes and refuses, and how `gc` finishes an + interrupted removal. `worktree:init` covers `activate --install-agent-guidance`. +- The `ess-pipeline` trial fixture sets the fields its invariant reads, which ess 0.37.0 requires + (`ESS-COMMAND-018`). The four ESS trials' baselines are re-recorded. `ess-new` now takes 38 tool + calls, not 12, because it models the per-member limit it used to leave unmapped; unmapped + markers fell from 7 to 4. + ## [0.15.0] — 2026-09-28 Eight methods from two public MIT skill collections, rewritten into the `aep`, `ess` and `b10x` diff --git a/Cargo.lock b/Cargo.lock index abd7a19..5eac945 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "agentplugins-check" -version = "0.15.0" +version = "0.16.0" dependencies = [ "clap", "serde", @@ -76,7 +76,7 @@ dependencies = [ [[package]] name = "b10x" -version = "0.15.0" +version = "0.16.0" dependencies = [ "clap", "serde", diff --git a/Cargo.toml b/Cargo.toml index 587b231..9201641 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/agentplugins-check", "crates/b10x"] [workspace.package] -version = "0.15.0" +version = "0.16.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 e578d0d..de1484c 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.15.0", + "version": "0.16.0", "author": { "name": "Beyond10x" }, diff --git a/plugins/aep/.codex-plugin/plugin.json b/plugins/aep/.codex-plugin/plugin.json index c5c8040..ab95013 100644 --- a/plugins/aep/.codex-plugin/plugin.json +++ b/plugins/aep/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "aep", - "version": "0.15.0", + "version": "0.16.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 863f184..279223c 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.15.0** — the version in `.claude-plugin/plugin.json`. +**Skill version 0.16.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 39035c4..5750f17 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.15.0** — the version in `.claude-plugin/plugin.json`; a wave's stage-1 proposal quotes it. +**Skill version 0.16.0** — the version in `.claude-plugin/plugin.json`; a wave's stage-1 proposal quotes it. # Implementing accepted work @@ -22,6 +22,19 @@ question that names both. Then read that mode's reference in full; this page onl The operator can also name the mode directly: `/aep:wave [story-id…]` (`aep:wave`) or `/aep:drive ` (`aep:drive`), two commands that load this skill in that mode. +## The store's version, first + +Before reading or writing the store, read `version` in `.engineering/project.yaml`. On anything but +`aep.project/5`, tell the user once, before any store write, which version it is and its upgrade; +do not wait for `aep` to print a notice: + +| the store | upgrade | +|---|---| +| `aep.project/1`, or `.engineering/planning/` with no `project.yaml` | `aep plan store migrate git --verify` on a clean `.engineering` (no `project.yaml`: add `--protocols` and `--profile`, `aep:planning` § 5), then commit. Every verb still works (with no `project.yaml`, only given `--store `); carry on | +| `aep.project/2`, `/3` or `/4` | `cargo install --git https://github.com/beyond10x/aep --rev 9c0f1da44429ff935fa0b2d743457945d51e1c51 aep-cli`, then `aep plan store migrate git --verify` on a clean `.engineering`, commit, then install the current release (`aep:upgrade`). Every planning verb refuses the store: stop and report | + +`aep:upgrade` carries the steps; the user decides when the migration runs. + ## Rules for both modes - Implement only stories the store shows as accepted; neither mode moves an artifact itself. diff --git a/plugins/aep/skills/implementing/references/adversary.md b/plugins/aep/skills/implementing/references/adversary.md index 41e057f..67826a0 100644 --- a/plugins/aep/skills/implementing/references/adversary.md +++ b/plugins/aep/skills/implementing/references/adversary.md @@ -173,16 +173,16 @@ Only the residue — what could not be made into a failing case. **You return th report. You do not write them to the planning store, and you run no `aep plan artifact` command at all.** -The reason is mechanical, not stylistic. You work in a worktree, and the store's journal is -append-only and committed. A record you write there is a second tail on a branch nobody merges, and -when the coordinator's tree and yours both append, the textual merge produces a document whose -revision no event supports — which the store's own validator reports as forgery. One agent, one -surface; the store is the coordinator's surface and never yours. +The reason is mechanical, not stylistic. You work in a worktree, on a branch. Every store write +bumps the artifact's `revision:` line, so a record you write there and the coordinator's write to +the same artifact conflict at merge, and a resolution that drops either side's move is refused by +`aep plan artifact validate`. One agent, one surface; the store is the coordinator's surface and +never yours. This was measured, not feared: on the wave of 2026-08-30 two adversaries were given the same -charter, one declined and said why, the other complied and wrote into its worktree's store. Its -journal was 564 lines against the main tree's 568 — forked, and a merge away from the failure the -rule exists to prevent. +charter, one declined and said why, the other complied and wrote into its worktree's store. That +store's journal (the layout of the time) was 564 lines against the main tree's 568 — forked, and a +merge away from the failure the rule exists to prevent. So the findings arrive as a table in your report, one row per finding, each carrying a `file:line`, one verdict and one origin: diff --git a/plugins/aep/skills/implementing/references/security-reviewer.md b/plugins/aep/skills/implementing/references/security-reviewer.md index d4d572d..ae00181 100644 --- a/plugins/aep/skills/implementing/references/security-reviewer.md +++ b/plugins/aep/skills/implementing/references/security-reviewer.md @@ -167,11 +167,11 @@ Only the residue — what could not be made into a failing case. **You return th report. You do not write them to the planning store, and you run no `aep plan artifact` command at all.** -The reason is mechanical, not stylistic. You work in a worktree, and the store's journal is -append-only and committed. A record you write there is a second tail on a branch nobody merges, and -when the coordinator's tree and yours both append, the textual merge produces a document whose -revision no event supports — which the store's own validator reports as forgery. One agent, one -surface; the store is the coordinator's surface and never yours. +The reason is mechanical, not stylistic. You work in a worktree, on a branch. Every store write +bumps the artifact's `revision:` line, so a record you write there and the coordinator's write to +the same artifact conflict at merge, and a resolution that drops either side's move is refused by +`aep plan artifact validate`. One agent, one surface; the store is the coordinator's surface and +never yours. So the findings arrive as a table in your report, one row per finding, each carrying a `file:line`, one verdict and one origin: diff --git a/plugins/aep/skills/implementing/references/story-scoper.md b/plugins/aep/skills/implementing/references/story-scoper.md index 29bf920..4fd8529 100644 --- a/plugins/aep/skills/implementing/references/story-scoper.md +++ b/plugins/aep/skills/implementing/references/story-scoper.md @@ -20,9 +20,8 @@ exactly where it is weakest. ## You change nothing -Read-only, and for a reason beyond caution: many of you run at once. The planning store's journal is -append-only and one file, so N agents writing it concurrently is a race. You return the section; the -one session that called you writes it, in order. +Read-only, and for a reason beyond caution: many of you run at once, and the planning store takes +one writer at a time. You return the section; the one session that called you writes it, in order. * **Bash is for reading** — `aep plan artifact show`, `list`, `graph`, `git log`, `git grep`, `rg`, and nothing that writes. diff --git a/plugins/aep/skills/implementing/references/wave.md b/plugins/aep/skills/implementing/references/wave.md index 280b38f..e69a318 100644 --- a/plugins/aep/skills/implementing/references/wave.md +++ b/plugins/aep/skills/implementing/references/wave.md @@ -79,11 +79,11 @@ matters* is the load-bearing half. Almost none of a healthy wave matters. A sub-agent's report is **input to you, never output to the operator.** Its register is not yours to pass on: take the findings, drop the voice. -**Why you own every store write.** The planning store's journal is append-only and committed, and -nothing merges it. Two branches that each move their own story both append to the tail, and the -textual merge produces a document whose revision no event supports — which the store's own -validator reports as forgery. Implementors touching only source files makes that impossible. It is -also the division that works: one agent, one surface; the shared files are yours. +**Why you own every store write.** Every store write bumps the artifact's `revision:` line, and a +move appends to its `transitions`. A unit branch that writes an artifact the coordinator also +writes conflicts on those lines at merge, and a resolution that drops either side's move is +refused by `aep plan artifact validate`. Implementors touching only source files makes that +impossible. It is also the division that works: one agent, one surface; the shared files are yours. --- @@ -113,8 +113,8 @@ a real backlog most bodies cite no path at all, so the disjointness a wave rests unless somebody establishes it. Fan out `story-scoper`, one agent per candidate, and run them at once. They are read-only by -charter — which is what makes running many safe: the planning journal is append-only and one file, -so N agents writing it would race. **They return `## Scope` sections; you write them**, one at a +charter — which is what makes running many safe: the store takes one writer at a time, and that is +you. **They return `## Scope` sections; you write them**, one at a time, through `aep plan artifact body` with the complete body. Each section says where the work lands and marks every line `cited` or `inferred`. Read the diff --git a/plugins/aep/skills/init/SKILL.md b/plugins/aep/skills/init/SKILL.md index 16da6e8..c5e9ef5 100644 --- a/plugins/aep/skills/init/SKILL.md +++ b/plugins/aep/skills/init/SKILL.md @@ -35,8 +35,10 @@ aep plan artifact list ``` A store answers with its artifacts. No store: `aep:planning` § 5 (*Starting from a repository that -has no store*) says how a first one is created; an existing backlog in markdown moves in with -`aep:migrating`. +has no store*) says how a first one is created — `aep plan reverse init`, which writes an +`aep.project/5` project with `store: {git: {}}`; an existing backlog in markdown moves in with +`aep:migrating`. A store whose `.engineering/project.yaml` names another `version` is upgraded +first: `aep:upgrade` (*An older planning store*). ## 3. Pick the work diff --git a/plugins/aep/skills/migrating/SKILL.md b/plugins/aep/skills/migrating/SKILL.md index 92ac117..dfaa064 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.15.0** — the version in `.claude-plugin/plugin.json`. +**Skill version 0.16.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 1bd44b9..cfe3f49 100644 --- a/plugins/aep/skills/planning/SKILL.md +++ b/plugins/aep/skills/planning/SKILL.md @@ -3,10 +3,27 @@ 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.15.0** — the version in `.claude-plugin/plugin.json`. +**Skill version 0.16.0** — the version in `.claude-plugin/plugin.json`. # Planning in a governed artifact store +## The store's version, before the first write + +Read `version` in `.engineering/project.yaml` (YAML or JSON; the key is the same). The current store +is `aep.project/5`, with `store: {git: {}}`. On any other version, tell the user once, before any +store write, which version the store is on and its upgrade, from this table — `aep` prints a notice +only for some of them, and a caller may have hidden it: + +| the store | what works | upgrade | +|---|---|---| +| `aep.project/5` | everything | none | +| `aep.project/1`, or `.engineering/planning/` with no `project.yaml` | every verb (with no `project.yaml`, only given `--store `) | `aep plan store migrate git --verify` on a clean `.engineering` (no `project.yaml`: add `--protocols` and `--profile`, § 5), then commit | +| `aep.project/2`, `/3` or `/4` | nothing: every planning verb refuses it | `cargo install --git https://github.com/beyond10x/aep --rev 9c0f1da44429ff935fa0b2d743457945d51e1c51 aep-cli`, then `aep plan store migrate git --verify` on a clean `.engineering`, commit, then install the current release (`aep:upgrade`) | + +The migration rewrites every artifact file, so it runs when the user says so; `aep:upgrade` carries +the steps. On `/1`, carry on with the task after telling them. On `/2`–`/4`, stop store work and +report the refusal. Done when the user has been told, or `version` is `aep.project/5`. + ## 0. When the record is required In a Beyond10x repository, use this skill before implementation when the work is non-trivial, @@ -26,7 +43,9 @@ present, follow section 5 rather than inventing one. ## 1. The model Artifacts are markdown files under `.engineering/planning//.md`: YAML frontmatter the -CLI owns, and a body you and the operator own. Which kinds exist, which statuses each kind may hold +CLI owns, and a body you and the operator own. Those files are the store: a move appends one line to +the artifact's `transitions`, each evidence record is one file under +`.engineering/evidence///`, and one CLI write changes one file. Which kinds exist, which statuses each kind may hold and which moves between them are legal come from validated lifecycle documents, not from convention and not from this file. The `aep` CLI is the authority on both, so every question about vocabulary has a command that answers it. @@ -77,7 +96,8 @@ These are inlined because they hold whatever the store's vocabulary is. **1. A status changes only through `aep plan artifact move`.** Never edit the `status:` field in frontmatter, and never write it into a file with `Edit` or a heredoc. The CLI validates the move against the kind's lifecycle; a hand-edited status is an unvalidated one, indistinguishable in the -file from a legal one and wrong in exactly the cases that matter. +file from a legal one and wrong in exactly the cases that matter. The same holds for `revision` +and `transitions`; `validate` refuses a `status` that disagrees with the last transition. ```console $ aep plan artifact move story:credential-store --to proposed @@ -330,8 +350,10 @@ $ aep plan reverse init --protocols --profile $ aep plan reverse scan --format json ``` -`reverse init` writes `.engineering/project.yaml` and refuses the two things that quietly break -later — an absolute path, and a `git+` source pinned to a branch rather than a commit. +`reverse init` writes `.engineering/project.yaml` as `aep.project/5` with `store: {git: {}}`, and +refuses the two things that quietly break later — an absolute path, and a `git+` source pinned to a +branch rather than a commit. A `.engineering/planning/` that already holds a plan is refused too, +naming `aep plan store migrate git`. The two values, for a project that follows the published protocols: @@ -444,8 +466,8 @@ verdict before starting the next, and say in the report that the four were not i **They run at once, and none of them sees another's findings.** That is the mechanism, not a scheduling convenience: four independent readings are worth more than four agents converging on the -first one's framing. It is also why they are read-only — the journal is append-only and -single-writer, so N critics writing it is a race. They return text; you write. +first one's framing. They are read-only as well: the store takes one writer at a time, and that +writer is you. They return text; you write. Each returns a first line that is exactly `approve` or exactly `needs-revision`, then one line per finding in the form `artifact — reason — citation`. The rules they work to are in @@ -552,7 +574,8 @@ decides what can be worked at once — and in most stores nothing holds it. The `story-scoper` agent answers it for one artifact: it reads the body, its edges, the symbols it names and the tree, and returns a `## Scope` section marking every line **cited** or **inferred**. It is read-only, so run one per story and run them at once; write what they return through -`aep plan artifact body`, one at a time, because the journal is append-only and N writers race. +`aep plan artifact body`, one at a time, because each write bumps the story's `revision` and the +store takes one writer at a time. Read the confidence line, not just the surface. A scope that mixes what was read with what was guessed is worse than none — it gets trusted exactly where it is weakest — which is why the section diff --git a/plugins/aep/skills/planning/references/critic-rubric.md b/plugins/aep/skills/planning/references/critic-rubric.md index 7c563f6..7fe7ce2 100644 --- a/plugins/aep/skills/planning/references/critic-rubric.md +++ b/plugins/aep/skills/planning/references/critic-rubric.md @@ -130,8 +130,7 @@ settles the question faster than an argument about a schema. ## You change nothing Read-only, and for a mechanical reason rather than caution: several of you run at once, and the -store's journal is append-only and single-writer. N critics writing it concurrently is a race that -produces a document whose revision no event supports. +store takes one writer, the caller, who records each verdict in order. * **The shell is for reading** — the CLI's own read verbs, `git log`, `git grep`, `rg`, `cat`. Never a write verb, never `sed -i`, `mv`, `rm`, `git` anything that moves the tree, never a redirection diff --git a/plugins/aep/skills/planning/references/reverse-engineer.md b/plugins/aep/skills/planning/references/reverse-engineer.md index 859b875..282d3dc 100644 --- a/plugins/aep/skills/planning/references/reverse-engineer.md +++ b/plugins/aep/skills/planning/references/reverse-engineer.md @@ -19,6 +19,12 @@ something to *ask about*, not something to file. ## Read before you write +0. **The store.** No `.engineering/project.yaml`: create it with `aep plan reverse init --protocols + --profile ` (values in `aep:planning` § 5). It writes an `aep.project/5` + project, `store: {git: {}}`, where each artifact file is the authority. A `project.yaml` whose + `version` is not `aep.project/5`: report the version and its upgrade (`aep:planning`, *The + store's version*) as the first line of your report, and write nothing to a `/2`–`/4` store, + which every planning verb refuses. 1. **`aep plan reverse scan --format json`** from the repository root. This is your evidence and it is the only thing that produces citations. Everything below is read against it. 2. **`aep plan reverse history --format json`**, when the repository is a Git working tree. It joins diff --git a/plugins/aep/skills/planning/references/store-conventions.md b/plugins/aep/skills/planning/references/store-conventions.md index ee41f0d..27ff31f 100644 --- a/plugins/aep/skills/planning/references/store-conventions.md +++ b/plugins/aep/skills/planning/references/store-conventions.md @@ -7,16 +7,27 @@ relations) all have commands, and those commands are the authority. See `SKILL.m ## Layout ``` -.engineering/planning/ -├── epic/ -│ └── passkey-login.md -├── story/ -│ ├── credential-store.md -│ └── registration-ceremony.md -└── task/ - └── ceremony-fixtures.md +.engineering/ +├── project.yaml version: aep.project/5, store: {git: {}} +├── planning/ +│ ├── epic/ +│ │ └── passkey-login.md +│ ├── story/ +│ │ ├── credential-store.md +│ │ └── registration-ceremony.md +│ └── task/ +│ └── ceremony-fixtures.md +└── evidence/ + └── story/ + └── credential-store/ + └── 20260830T140200Z-8477cd9d6ad8.json ``` +The artifact files are the store's authority: there is no journal, state directory or event log +beside them, and no file is a projection of another. Each evidence record is one JSON file written +once by `aep plan artifact evidence` and never edited. Status history lives in each artifact's +`transitions` list, and every other version of a file in Git history. + One directory per kind, one file per artifact, no nesting below the kind directory. The store root defaults to `.engineering/planning/` and moves with `--store ` — a repository may keep more than one, and nothing in a file records which store it belongs to. @@ -59,8 +70,11 @@ No planning-store file is edited directly. One command surface owns every change | record which surfaces a story lands on, read (`cited`) or worked out (`--inferred`) | `aep plan artifact scope --add ` | | record an observation a later move rests on, by kind or from a conformance report | `aep plan artifact evidence` | -This makes the store a single-writer system: every mutation loads and validates it before writing, -and every changed document receives the same revision semantics. +This makes the store a single-writer system: a write takes the store's writer lock, validates, and +changes exactly one artifact file (`evidence` adds one evidence file), bumping its `revision` once. +Two branches that write the same artifact therefore conflict on its `revision:` line in Git; resolve +by keeping one side and re-applying the other side's change through the CLI, then run `validate`, +which refuses a merge whose `transitions` do not end in its `status`. ## Frontmatter @@ -70,12 +84,13 @@ Everything above the `---` is structured. Ownership is what decides whether you |---|---|---| | `id` | machine | set at creation, never edited; equals `:` | | `kind` | machine | fixed at creation; changing a kind means a new artifact | -| `status` | machine | **only** `aep plan artifact move` writes this — see guardrail 1 | -| `revision` | machine | bumped by the CLI; a review is bound to the revision it saw | +| `status` | machine | **only** `aep plan artifact move` writes this — see guardrail 1; `validate` refuses one that disagrees with the last transition | +| `revision` | machine | bumped by the CLI once per write; a review is bound to the revision it saw | +| `transitions` | machine | append-only; `move` adds one line `{from, to, at, actor, revision, decided_on?}`, and a move carried over by a migration is marked `imported: true` | | `relations` | machine | written by `aep plan artifact new --relate` and `aep plan artifact relate` | | `title` | descriptive | set at creation by `aep plan artifact new --title`; no verb changes one afterwards | | `summary` | descriptive | one or two sentences; optional | -| `format` | machine | optional; defaults to `aep.planning-md/1` when absent — a file may omit it | +| `format` | machine | `aep.planning-md/3` in an `aep.project/5` store, written by the CLI | | `withholds` | machine | optional; set by `aep plan artifact new --withholds `. The evidence kind this artifact is stopping anybody from producing, and only meaningful beside a `blocks:` relation — `validate` reports it otherwise | | `scope` | machine | story only; written by `aep plan artifact scope`, each entry a `path` and a `confidence` of `cited` or `inferred`. `aep plan artifact waves` reads nothing else | | `model_digest` | machine | `executable-system-specification` only; written by `aep plan artifact set --model-digest`, refused by name on any other kind | @@ -101,7 +116,7 @@ sentence, in the form of an observable outcome. It is what a reviewer checks aga ```markdown --- -format: aep.planning-md/1 +format: aep.planning-md/3 id: story:credential-store kind: story status: proposed @@ -110,6 +125,8 @@ summary: Persist WebAuthn credential ids and public keys, and look them up at as relations: - decomposes: epic:passkey-login revision: 2 +transitions: +- {from: "draft", to: "proposed", at: "2026-08-30T13:40:02Z", actor: "human:operator", revision: 2} --- ## Context @@ -129,6 +146,6 @@ Storage backend is settled (the existing Postgres schema); the open question is key is stored COSE-encoded or normalised on write. Raised in `epic:passkey-login`. ``` -The frontmatter is six machine-owned lines and two descriptive ones. Everything that took thought is +Two frontmatter lines are descriptive; the rest are the CLI's. Everything that took thought is below the fence, which is the intended shape: the CLI keeps the graph honest, and the file stays a document a person can read. diff --git a/plugins/aep/skills/upgrade/SKILL.md b/plugins/aep/skills/upgrade/SKILL.md index b6d0e2a..632cbcc 100644 --- a/plugins/aep/skills/upgrade/SKILL.md +++ b/plugins/aep/skills/upgrade/SKILL.md @@ -1,6 +1,6 @@ --- name: upgrade -description: Check whether the AEP plugin and the `aep` CLI are current, and upgrade them with the user's confirmation. Use when the user asks whether AEP is up to date or to upgrade or update it, when a session-start line starting with `b10x:` names `aep`, or when a `aep` command behaves differently from what a AEP skill describes. +description: Check whether the AEP plugin and the `aep` CLI are current, and upgrade them with the user's confirmation. Use when the user asks whether AEP is up to date or to upgrade or update it, when a session-start line starting with `b10x:` names `aep`, or when a `aep` command behaves differently from what a AEP skill describes or names the planning store's `aep.project` version. --- # Upgrade AEP @@ -20,6 +20,34 @@ changed yet. No `b10x`? Follow https://github.com/beyond10x/agentplugins/releases/latest/download/SETUP.md first. +## An older planning store + +The current `aep` reads `aep.project/5` (`store: {git: {}}`). Read `version` in the repository's +`.engineering/project.yaml`, tell the user which it is, and after a clear yes run the steps for it. +Each migration refuses a dirty `.engineering`, so commit what is there first. + +**`aep.project/1`, or `.engineering/planning/` with no `project.yaml`.** The current release +migrates it; add `--protocols --profile ` when there is no `project.yaml` +(values in `aep:planning` § 5). `--dry-run` first shows what it would write. + +```bash +aep plan store migrate git --verify +git add .engineering && git commit # message names the migration +``` + +**`aep.project/2`, `/3` or `/4`.** The current release refuses these stores. Migrate with the pinned +build that still reads them, then return to the current release: + +```bash +cargo install --git https://github.com/beyond10x/aep --rev 9c0f1da44429ff935fa0b2d743457945d51e1c51 aep-cli +aep plan store migrate git --verify +git add .engineering && git commit # message names the migration +b10x install aep --method cargo # replaces the pinned build in ~/.cargo/bin with the newest release +``` + +Done when `aep --version` prints the newest release, `version` reads `aep.project/5`, and +`aep plan artifact validate` exits 0. A `--verify` difference exits non-zero: relay it and stop. + ## Next - Continue the work that prompted the check; `aep:init` lists the skills. diff --git a/plugins/b10x/.claude-plugin/plugin.json b/plugins/b10x/.claude-plugin/plugin.json index 881b617..5709eec 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.15.0", + "version": "0.16.0", "author": { "name": "Beyond10x" }, diff --git a/plugins/b10x/.codex-plugin/plugin.json b/plugins/b10x/.codex-plugin/plugin.json index 1fb1196..1de3b70 100644 --- a/plugins/b10x/.codex-plugin/plugin.json +++ b/plugins/b10x/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "b10x", - "version": "0.15.0", + "version": "0.16.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 0156c08..4405013 100644 --- a/plugins/connectors/.claude-plugin/plugin.json +++ b/plugins/connectors/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "connectors", - "version": "0.15.0", + "version": "0.16.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 aa56e3e..bb383fe 100644 --- a/plugins/connectors/.codex-plugin/plugin.json +++ b/plugins/connectors/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "connectors", - "version": "0.15.0", + "version": "0.16.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 a7eaab6..cd0e402 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.15.0", + "version": "0.16.0", "author": { "name": "Beyond10x" }, diff --git a/plugins/ess/.codex-plugin/plugin.json b/plugins/ess/.codex-plugin/plugin.json index 1a4aac3..3cd2139 100644 --- a/plugins/ess/.codex-plugin/plugin.json +++ b/plugins/ess/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ess", - "version": "0.15.0", + "version": "0.16.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/ess/skills/hardening/SKILL.md b/plugins/ess/skills/hardening/SKILL.md index 91f1879..133a84f 100644 --- a/plugins/ess/skills/hardening/SKILL.md +++ b/plugins/ess/skills/hardening/SKILL.md @@ -49,18 +49,19 @@ every later technique is then run against the spec that will stay. 1. **Design review** (8) — no tooling, one agent, the [brief](references/design-review.md). 2. **Spec diff in the gate** (7) — one command and a [classification](references/spec-diff.md); it protects everything after it. -3. **Mutation audit** (1) — reuses the suite you already have. +3. **Mutation audit** (1) — `ess verify conform mutate`, run through your own suite runner. 4. **Guard analysis** (6) — reads the IR only; no implementation runs. -5. **Reference model and random sequences** (2) — the one piece of code the catalogue needs; build it - once. -6. **Determinism** (4), **metamorphic relations** (5) and **caller replay** (3) — each reuses the - runner from step 5. +5. **Random sequences** (2) — the explorer in the generated Go or TypeScript package; a reference + model of your own only where the explorer excludes what you need. +6. **Determinism** (4) — the explorer twice with one seed. **Metamorphic relations** (5) and + **caller replay** (3) — a reference model you drive with your own traces. Stop where the cost exceeds what the spec is worth, and say which techniques were not run. ## What the IR is for -Techniques 1–6 read the canonical IR, not the YAML: +Anything you build for techniques 2–6 reads the canonical IR, not the YAML (`ess` and the explorer +already do): ```console ess specify compile --path --format json --out @@ -70,13 +71,18 @@ The IR is the resolved model: every name qualified, every guard in structured fo applied. A technique that reads the YAML re-implements resolution and disagrees with the compiler somewhere nobody looked. -## The reference model - -Techniques 2, 3 and 5 rest on one small interpreter over the IR — about 150 lines — that answers, -for any command in any state, what the spec says happens. `ess` does not ship it yet; -[beyond10x/ess#114](https://github.com/beyond10x/ess/issues/114) proposes shipping the mutation audit -and the sequence runner as `ess` features, and carries a draft for the TypeScript target. Until it -ships, build it from the pattern: [references/reference-model.md](references/reference-model.md). +## What `ess` ships for techniques 1 and 2 + +- **Mutation audit:** `ess verify conform mutate` mutates the specification in nine + classes and writes `ess-mutation-report/1`. Against your own implementation, `--emit DIR` writes + the baseline's and every mutant's suite, your runner writes `report.json` beside each, and + `--collect DIR` scores them. [references/techniques.md](references/techniques.md) § 1. +- **Reference model and random sequences:** the Go and TypeScript packages + `ess verify conform synthesize --target go|typescript` writes carry a seeded explorer + (`Explore`/`explore`) that walks your `Target` against a model interpreted from the IR, shrinks a + 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). ## Reporting diff --git a/plugins/ess/skills/hardening/references/reference-model.md b/plugins/ess/skills/hardening/references/reference-model.md index 42b3655..d3f47a3 100644 --- a/plugins/ess/skills/hardening/references/reference-model.md +++ b/plugins/ess/skills/hardening/references/reference-model.md @@ -4,12 +4,12 @@ Techniques 2, 3 and 5 compare an implementation with what the spec says happens. **reference model**: a small interpreter over the compiled IR that, given a state and a command, returns the outcome the spec declares. On one adopter it was about 150 lines of TypeScript. -**Status.** `ess` does not ship this. [beyond10x/ess#114](https://github.com/beyond10x/ess/issues/114) -proposes shipping the mutation audit and a seeded sequence runner built on this model as `ess` -features, emitted beside the suite for `--target typescript` and `--target go` and driven through the -same `Target` interface; the issue carries a draft (`explore.ts`) for the TypeScript target. Until it -ships, write the model yourself from the pattern below, and check that issue before starting — once -it ships, use what it emits instead. +**Status.** The Go and TypeScript packages that +`ess verify conform synthesize --target go|typescript` writes carry this model and a seeded +sequence runner over it (`explore`/`Explore`, [techniques.md](techniques.md) § 2), bound to the +suite's `spec_digest` through `ir.json`. Use that for technique 2. Write the model yourself from the +pattern below when the runner's `excluded` list holds what you need, or for techniques 3 and 5, +which drive the model with traces the explorer does not generate. ## Input @@ -38,19 +38,25 @@ does not have. For `execute(command, input)`: 1. **Pick the outcome.** Evaluate each outcome's `when:` predicate over the input — comparisons over - `input_field` and `literal` values, combined with and/or/not. Exactly one non-`wrong_state` - outcome must match; zero or two is a spec defect, report it rather than choosing. -2. **Check the lifecycle.** For an outcome that `moves` an instance through a transition, look the - instance up by the outcome's `instance` field. Missing instance, or current state not in the - transition's `from`: answer with the command's `wrong_state` outcome instead, and **change - nothing** — no `sets`, no events. (That a `wrong_state` answer still applied `sets` was one of the - defects this found.) + `input_field` and `literal` values, combined with and/or/not — and a `when_subject` predicate + over the addressed instance's stored fields (with `input.` operands from `ess/15`). + Exactly one non-`wrong_state` outcome must match; zero or two is a spec defect, report it rather + than choosing. +2. **Check the lifecycle.** For an outcome that `moves`, `updates` or `deletes` an instance, look it + up by the outcome's `instance` field. A missing instance answers, in order, the command's + `unknown_instance:` outcome, its not-found outcome (an `external:` refusal whose `error:` carries + a field of the identity's type), or its `wrong_state` outcome. A current state not in the + transition's `from` answers `wrong_state`. Either way **change nothing** — no `sets`, no events. + (That a `wrong_state` answer still applied `sets` was one of the defects this found.) 3. **Apply.** - - `creates`: a new identity (refuse to reuse one in `issued`), the lifecycle's `initial` state, the - fields the outcome sets. + - `creates`: a new identity (refuse to reuse one in `issued`), the `into:` state or else the + lifecycle's `initial`, the fields the outcome sets. - `moves`: set the state to the transition's `to`. - - `updates` / `sets`: write each field from its value expression (`input.` or a literal). - - `error`: change nothing. + - `deletes`: remove the instance; its identity stays in `issued`. + - `updates` / `sets`: write each field from its value expression — `input.`, a literal, + `{subject: f}` (the value before this step), `{increment: n}`, or `{generated: true}` (unknown + to the model: see Views). + - `error`, `preserves`, `accepts: nothing`: change nothing. 4. **Emit.** For each event in `emits`, build its payload from the outcome's `payload` mapping. 5. **Check invariants** of every instance the step touched, over its fields after the step. diff --git a/plugins/ess/skills/hardening/references/techniques.md b/plugins/ess/skills/hardening/references/techniques.md index 6955d33..190488d 100644 --- a/plugins/ess/skills/hardening/references/techniques.md +++ b/plugins/ess/skills/hardening/references/techniques.md @@ -15,6 +15,32 @@ ess specify compile --path --format json --out **Question:** would the suite notice if a declared rule broke? +`ess verify conform mutate` derives one mutant per site in nine classes (`from-drop`, +`transition-to`, `guard-boundary`, `sets-retarget`, `guard-negate`, `guard-connective`, +`error-swap`, `emit-drop`, `order-flip`; `--class` selects), synthesizes each mutant's suite and +scores it against an implementation of the unchanged specification. `--target` runs only the +built-in `billing`, `oracle-fixture` and `interpreted` targets, so for your own implementation: + +1. `ess verify conform mutate --path --emit ` writes `/baseline/suite.json`, + one `//suite.json` (with `ir.json`) per mutant and `/manifest.json`, and + runs nothing. `` must be new or empty. +2. Run your conformance runner over every emitted suite and write its report as `report.json` + beside that suite (the generated Go and TypeScript packages take `ESS_REPORT_OUT=`). This + step is done when every mutant directory holds a `report.json`; a missing one scores + `inconclusive`. +3. `ess verify conform mutate --collect --report-out ` scores them into + `ess-mutation-report/1`: exit 0 every mutant killed, 1 a survivor, 3 the baseline did not pass + (`ESS-MUTATE-001`), no site (`ESS-MUTATE-003`) or only inconclusive or stillborn mutants. + A `stillborn` mutant is one the model itself refuses; it says nothing about the suite. +4. Answer each survivor by declaring what makes the rule observable — a view publishing the field a + `sets` entry writes, a `wrong_state:` outcome for a dropped `from` state — then re-run and see + it killed; or file a synthesis gap on beyond10x/ess. An authored scenario cannot answer a + survivor: its expectations are its author's, so it runs identically in every mutant's suite and + `mutate` runs none. + +**By hand**, where `mutate` has no class for the rule you need (a mutant of the implementation's +own rule table, or a class outside the nine): + 1. From the IR, list one mutant per declared rule. The classes that find gaps: | class | example | @@ -30,20 +56,34 @@ ess specify compile --path --format json --out suite is re-synthesized and run against the unchanged implementation — one at a time. 3. Record, per mutant, the scenarios that failed. A mutant no scenario kills is a declared rule the suite does not pin down. -4. For each survivor, decide: a missing scenario (author one, then re-run that mutant and see it - killed), a view that does not expose the field (see `ess:testing-conformance`, "does a green run - mean anything?"), or a generator gap (an issue on beyond10x/ess). +4. For each survivor, decide: a view that does not expose the field (see `ess:testing-conformance`, + "does a green run mean anything?"), a generator gap (an issue on beyond10x/ess), or — for a + mutant of the implementation, not of the specification — a missing scenario (author one, then + re-run that mutant and see it killed). **Planted defect:** the audit is its own plant — but confirm the harness first with one mutant you -know a scenario kills. If it survives, the harness is not applying mutants. +know a scenario kills. If it survives, the harness is not applying mutants. With `mutate`, the +baseline must pass first (`ESS-MUTATE-001` otherwise), which checks the runner. -**Found:** 8 of 35 single-rule mutants passed every scenario; 6 were declared behaviour. After 6 -authored scenarios, 1 survived, and it rested on a value the spec did not declare. +**Found** (by hand, before `mutate` shipped): 8 of 35 single-rule mutants passed every scenario; 6 +were declared behaviour. After 6 authored scenarios, 1 survived, and it rested on a value the spec +did not declare. ## 2. Random command sequences against a reference model **Question:** does the implementation agree with the spec along paths nobody wrote? +The generated Go and TypeScript packages carry this runner. Call it from a test beside the suite's: +`explore(() => newTarget(), { seeds: 200, steps: 60 })` then `assertExplored(result)` in +TypeScript, `essconform.Explore(…, essconform.ExploreOptions{Seeds: 200, Steps: 60})` then +`essconform.AssertExplored(t, result, essconform.AssertOptions{})` in Go. It compares outcome, +error, events, payloads, every parameterless view and every invariant after each step, polls +`eventual` views, shrinks a failure, names its seed, and fails on a declared outcome no sequence +reached. It models a subset; what it leaves out is listed in `excluded`, and passing with +`allowExcluded` is a claim to report. Two guards that both hold are reported in `ambiguous`, a view +or invariant over a field no command set in `undetermined`. Go on to the steps below only for what +it excludes. + 1. Build the [reference model](reference-model.md) over the IR. 2. Drive a seeded random walk: at each step pick an actor, a command and an input (valid, boundary and invalid), send it to the implementation through the suite's own `Target` interface diff --git a/plugins/ess/skills/init/SKILL.md b/plugins/ess/skills/init/SKILL.md index 4b0dd34..d037aba 100644 --- a/plugins/ess/skills/init/SKILL.md +++ b/plugins/ess/skills/init/SKILL.md @@ -41,6 +41,10 @@ then the repository's own conformance command, read from its `AGENTS.md` or its (`Taskfile.yml`, `Makefile`, `package.json` scripts, the CI job that runs the suite). Run that command as the repository spells it; do not assemble a runner invocation of your own. +Where `ess-inputs.yaml` says `requires: ess X.Y.Z`, that exact release runs, not the one on `PATH`: +`ess` fetches it into a checksummed cache and hands over. `ess specify toolchain which`, run in +that directory, prints which release ran and why; include its first line in the report. + Report both results as the tools printed them: the validation line (` v — file(s), valid`, or every refusal verbatim) and the suite's `passed`, `skipped` and `failed` counts with the command and its exit status. No conformance command found: say so, and name where you looked. diff --git a/plugins/ess/skills/retrofitting/SKILL.md b/plugins/ess/skills/retrofitting/SKILL.md index c62d0f6..dfcdd91 100644 --- a/plugins/ess/skills/retrofitting/SKILL.md +++ b/plugins/ess/skills/retrofitting/SKILL.md @@ -24,7 +24,9 @@ Use every source that exists. Where two disagree, the draft records both and mar `ess infra import openapi` reads OpenAPI 3.0 and 3.1. A nullable field (`nullable: true`, or a 3.1 `type: [T, "null"]`) imports as a coverage gap at its pointer, because the interface has no null: -carry each gap into the draft as an `UNMAPPED:` marker. An object schema must be closed with +carry each gap into the draft as an `UNMAPPED:` marker, or, where the contract says the key is +always sent and may be `null`, declare it `Optional` with `presence: null_when_absent` +(`ess/15`), which publishes exactly that. An object schema must be closed with `additionalProperties: false`, or the import refuses it; that refusal is a finding about the contract, so report it rather than editing the contract to pass. @@ -60,23 +62,61 @@ 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 command the code ignores in some state is a synthesis refusal, and that is correct.** When - the code does nothing (no error) for a command in a state its transition does not start from, - declare no `wrong_state:` outcome and no error. `synthesize` then reports `ESS-SYNTH-012` for - that state and still writes the scenario, which requires that nothing happened: the code's - behaviour. Report the refusals; do not add an error the code never raises. -- **A service that publishes no events still needs one per success outcome.** `validate` refuses an - outcome that neither emits nor names an error (`empty_change`); a view does not count. Declare an - event for the fact the outcome produces (`OrderPaid`), leave it out of the component's - `publishes:` list because the service does not publish it, and say so in a comment and the report. +- **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 + `wrong_state:` outcome, so when it ignores in one state and refuses in another, write no + `wrong_state:` at all (`format: ess/7` or later): guard each branch by the held state, and let + one effect-free error answer the rest. + + ```yaml + - name: retired + when_subject_state: Active + moves: shop.tools.Tool.retire + instance: id + emits: [shop.tools.ToolRetired] + payload: + shop.tools.ToolRetired: {tool_id: input.id} + - name: already-retired # ignored: success, nothing changes + when_subject_state: Retired + preserves: shop.tools.Tool + instance: id + - name: in-maintenance # refused in every other state + error: shop.tools.InMaintenance + ``` + + `when_subject_state:` goes only on a `moves:`, `updates:` or `preserves:` branch with `instance:`, + never beside `wrong_state:` in the same command (`conflicting_declaration`). A `preserves:` + branch carries no error, event or `sets:` (`refusal_mutated_state`); a refusal names no subject. +- **A service that publishes no events still needs one per success outcome that changes a + record.** `validate` refuses an outcome that neither emits nor names an error (`empty_change`); a + view does not count. Declare an event for the fact the outcome produces (`OrderPaid`), leave it + out of the component's `publishes:` list because the service does not publish it, and say so in a + comment and the report. A success that changes nothing is not this case: `preserves:` with a + subject, `accepts: nothing` without one (`ess/15`). +- **The code's own shapes have constructs; use them rather than `UNMAPPED:`** + ([later formats](../specifying/references/later-formats.md); raise `format:` to the one named): + + | the code | declare | + |---|---| + | deletes the row at the end of its life | `deletes:` (`ess/15`), not a terminal state no row holds | + | inserts a row already past the first status | `into: ` beside `creates:` (`ess/15`) | + | answers an unknown id differently from a wrong state (a `404`) | `unknown_instance: true` with its error (`ess/15`) | + | runs every request inside a session or tenant that must exist first | system `preconditions:` (`ess/15`) | + | needs a deployed id a generator cannot invent | `fixture_inputs:` on the command (`ess/13`) | + | writes a stored value plus one, or echoes the stored value | `{increment: 1}`, `{subject: }` (`ess/14`) | + | sends a JSON key under another spelling, a key starting `_`, an opaque JSON body | `naming: {wire: }` on the field, the name `_key` as is, `Json` (`ess/15`) | + | compares text ignoring case | `equals_ignore_case` / `in_ignore_case` (`ess/15`, ASCII only; Unicode folding in the code is a disagreement to report) | - **An entity the code gives no status still needs a lifecycle.** The language requires one, so declare a single state that is both `initial` and `terminal` (`Stocked`), and say in a comment that it is structural, not read from the code. That is the one state a retrofit may name. - **Wire values keep their spelling in a comment.** A stored status `in_transit` becomes the state `InTransit`; state names must start upper-case. -- **A rule the language cannot hold stays in the code, and is named.** A limit read from stored - state, or a constraint across records, is `UNMAPPED:` with its source line - ([syntax reference](../specifying/references/syntax.md), "What `when` can and cannot say"). +- **A rule the language cannot hold stays in the code, and is named.** A limit read from the + addressed record's stored fields is `when_subject: {predicate: …}`, compared with the request as + `input.` from `ess/15`; a constraint across records, or on another entity, is `UNMAPPED:` + with its source line ([syntax reference](../specifying/references/syntax.md), "What `when` can + and cannot say"). ## 3. Prove the draft describes the system diff --git a/plugins/ess/skills/specifying/SKILL.md b/plugins/ess/skills/specifying/SKILL.md index a0c2658..04d9bda 100644 --- a/plugins/ess/skills/specifying/SKILL.md +++ b/plugins/ess/skills/specifying/SKILL.md @@ -16,9 +16,10 @@ job: the domain is drafted first, so the noun has a typed home before stories ar When the request does not say what the noun is, what it relates to and how it changes state, and no source in the repository answers that, interview first: ask the open decisions in numbered rounds, -each with the answer you would take, and draft once none is open. -[references/interview.md](references/interview.md) is the procedure, including what to do when no -operator is there to answer. +each with the answer you would take, and draft once none is open. When the request asks for a +finished specification, or no operator is there, that takes precedence: do not ask; take each +recommended answer and list the decisions in your report. +[references/interview.md](references/interview.md) is the procedure for both. Where an OpenAPI document already describes it, do not hand-write the domain. Draft it from the contract, and read the decisions the draft says it could not take: @@ -121,7 +122,9 @@ narrows that question without answering it. Where you cannot say which kind it i Grow it from there — types, commands, events, views, and a component that owns the domain — running `ess specify validate` after each addition rather than at the end. [references/syntax.md](references/syntax.md) shows every one of those sections in a small specification that validates; read it before writing -the first command. +the first command. [references/later-formats.md](references/later-formats.md) lists what formats up +to `ess/15` add — a stored-field guard, a delete, a create into a state, an unknown-id answer, value +expressions, wire presence — read it before marking a rule `UNMAPPED:` as not expressible. `--path` takes one ESS file or a directory. Without an `ess-inputs.yaml`, a directory is read as every YAML file below it — so generated output written inside it is read back as specification and @@ -142,6 +145,14 @@ scenarios: [] lists authored scenario files, `[]` when there are none. `--path ` then reads exactly that list. A directory with only `system.yaml` still validates, read whole. +To pin the `ess` release the specification is maintained with, write `format: ess-inputs/2` and +`requires: ess X.Y.Z` (exact) or `requires: ess X.Y` (a minor line). An older `ess` then refuses, +a newer one warns, and `--strict-requires` makes the warning a refusal (the CI spelling). An exact +pin makes every `ess` run below that manifest execute the pinned release from a checksummed cache: +`ess specify toolchain install X.Y.Z --pin` installs it and writes the pin, and +`ess specify toolchain which` prints which release runs here and why. Report that line whenever a +refusal names a release. + **A draft is a proposal, never a silent completion.** Every relation you could not read from code, an OpenAPI document or an existing artifact is written with an `UNMAPPED:` marker beside the place it would go, and named again in the report: @@ -167,7 +178,7 @@ type, a lifecycle state or an edge to make a document validate — leave the mar would settle it. An entity no source names, added so that a command type-checks, is an invention even when it validates. -A marker records the question; it does not ask it. In an interactive session, put the open markers +A marker records the question; it does not ask it. In an interactive session where the request did not ask for a finished specification, put the open markers to the user as one interview round at the end ([references/interview.md](references/interview.md); Claude Code: `AskUserQuestion`), one question per marker, and write the answers into the specification. Headless, or as the `author` agent, leave the markers and list them @@ -233,7 +244,12 @@ An entity `invariant` is checked after every outcome that leaves the entity in s needs a view that holds instances in that state and publishes the fields the invariant reads. Without one, `synthesize` refuses the check (`no view of holds an instance in `, or ` is published by no view of the entity`); a view over the entity with no `filter` that -publishes those fields covers every state (the `Copies` view in the syntax reference). `--target go` or `--target typescript` writes the suite as a test +publishes those fields and `state` covers every state (the `Copies` view in the syntax reference); a guard over a stored count is refused as `requires an immediate unfiltered identity/state/fact view` until the view publishes `state` too. A `note:` +from `synthesize` is not a refusal but names a check the suite does not make: `observes the refused +subject through what its views publish; no view publishes …` means a wrong-state refusal that +changed those fields would pass, and a view publishing them closes it; `declares no not-found and +no wrong_state outcome` means an unknown id has no declared answer (`unknown_instance:` in +[references/later-formats.md](references/later-formats.md)). Relay every note with the refusals. `--target go` or `--target typescript` writes the suite as a test package your implementation runs; `ess:testing-conformance` says what to run it against. `run` holds a built-in reference implementation to it; `--target` lists the ones this binary carries. `evidence --from` reads the kind, the source and the instant out of the report and refuses diff --git a/plugins/ess/skills/specifying/references/interview.md b/plugins/ess/skills/specifying/references/interview.md index 183454a..a773878 100644 --- a/plugins/ess/skills/specifying/references/interview.md +++ b/plugins/ess/skills/specifying/references/interview.md @@ -6,7 +6,8 @@ so the draft records decisions instead of gaps. Run it when a noun has no typed home yet and the request does not already say what the noun is, what it relates to, and how it changes state. Skip it when an OpenAPI document or existing code -answers those — read the source instead. +answers those — read the source instead. When the request asks for a finished specification, or +nobody is there to answer, go straight to *Without an operator* below. ## The design tree @@ -53,11 +54,17 @@ of the domain. Any decision still open is an `UNMAPPED:` marker, as the skill sa ## Without an operator -In a non-interactive run nobody answers. Do not hold the interview. For each question you would have -asked, take the recommended answer, write it into the specification, and record the question and -the answer taken in an `approval-record` as the `aep:planning` skill's § 4 *When there is no -operator* describes. A decision nobody could make — an ownership with no evidence either way — stays -an `UNMAPPED:` marker rather than a recommended guess. +In a non-interactive run, or when the request asks for a finished specification, do not hold the +interview. For each question you would have asked, take the recommended answer and write it into the +specification. Then record the question and the answer taken: + +- where the repository has an AEP store and `aep` is on `PATH`, as an `approval-record`, as the + `aep:planning` skill's § 4 *When there is no operator* describes; +- otherwise, as a numbered list in your report — the question, the answer taken, and the file and + entity it changed — so the user can overrule each one. + +A decision nobody could make — an ownership with no evidence either way — stays an `UNMAPPED:` +marker rather than a recommended guess. --- diff --git a/plugins/ess/skills/specifying/references/later-formats.md b/plugins/ess/skills/specifying/references/later-formats.md new file mode 100644 index 0000000..87b5827 --- /dev/null +++ b/plugins/ess/skills/specifying/references/later-formats.md @@ -0,0 +1,231 @@ +# ESS syntax beyond `ess/1` + +What formats after `ess/1` add to the lending library of [syntax.md](syntax.md). Read it when a +rule needs one of the constructs below, or when `validate` refuses one as +`unsupported_format_version`. + +A construct needs the `format:` the table names, in `system.yaml`; a higher format admits every +lower one, and under a lower header `validate` refuses the construct as `unsupported_format_version` +at its key. Raising the header past `ess/3` also brings one rule the `ess/1` library does not meet: +every field of an emitted event needs a `payload:` source, so `BranchOpened.branch_id` and +`CopyAdded.copy_id` get `{generated: true}` (the implementation mints the identity). + +| to say | format | how | +|---|---|---| +| a text's characters, its length, the value synthesis starts from | `ess/11` | `alphabet:` on a `String` newtype, `.count` on text, `example:` on a scalar input | +| one external refusal for many commands | `ess/12` | top-level `outcome_groups:` selecting by `commands:`, `actor:` or `domain:` | +| an input a provider supplies (a deployed id) | `ess/13` | `fixture_inputs: {borrower_id: current-borrower}` on the command | +| a value read from the stored row, added to it, or minted | `ess/14` | `{subject: title}` (the row before this outcome, on `moves:`/`updates:`), `{increment: 1}` (`sets:` only), `{generated: true}` (now also in `sets:`), `{cleared: true}` (an `Optional` field holds nothing), `{input: shelf_mark, else: {generated: true}}` for an optional input. The text `subject.title` is refused as `misspelled_reference` | +| an unknown id answered apart from a wrong state | `ess/15` | `unknown_instance: true` with an `error:` (or `refuses: false`), below | +| a record removed | `ess/15` | `deletes: ` with `instance:`, no `sets:`, below | +| a record created past `initial` | `ess/15` | `into: OnLoan` beside `creates:` | +| success with no subject and no effect | `ess/15` | `accepts: nothing`, below; with a subject it is `preserves:` | +| a command every scenario runs inside | `ess/15` | `preconditions:` in `system.yaml`, below | +| a text prefix, an unstructured JSON value | `ess/15` | `prefix: "SM-"` on a `String` newtype; `of: Json` (a payload fills it from an input) | +| how an absent `Optional` travels | `ess/15` | `presence: omitted_when_absent` or `null_when_absent` on the field | +| an aggregate over an `Optional` field | `ess/15` | `aggregate: {sum: replacement_cents, skip_absent: true}`, result `Optional`; an `Optional` group key makes absent one group | +| a field's own wire name, a leading underscore | any | `naming: {wire: copyId}` (same as flat `wire:`) on a field of an entity, struct, event or command input; a view field refuses it (`unknown field naming`). `_receipt` is a valid field name | +| a branch chosen by the subject's held state | `ess/3`; `ess/7` for the error default | `when_subject_state: `, below | + +```yaml +# system.yaml: every scenario, and every explorer sequence, first opens this branch +format: ess/15 +system: library +version: v1 + +domains: + - library.lending + +preconditions: + - command: library.lending.OpenBranch + as: library.lending.Librarian + input: {name: Central} +``` + +```yaml + - name: library.lending.ReturnCopy + naming: + wire: return-copy + display: Return a copy + input: + - name: copy_id + type: library.lending.CopyId + - name: title + type: library.lending.Title + - name: note + type: Optional + outcomes: + - name: wrong-title + when_subject: + predicate: title != input.title + error: library.lending.TitleMismatch + + - name: returned + moves: library.lending.Copy.return + instance: copy_id + sets: + loans: {increment: 1} + emits: + - library.lending.CopyReturned + payload: + library.lending.CopyReturned: + copy_id: input.copy_id + title: {subject: title} + note: input.note + _receipt: {generated: true} + summary: The copy is back on the shelf. + + - name: wrong-state + wrong_state: true + error: library.lending.CopyStateConflict + summary: The copy is not on loan, so nothing was returned. + + - name: library.lending.WithdrawCopy + input: + - name: copy_id + type: library.lending.CopyId + outcomes: + - name: withdrawn + deletes: library.lending.Copy + instance: copy_id + emits: [library.lending.CopyWithdrawn] + payload: + library.lending.CopyWithdrawn: + copy_id: input.copy_id + + - name: no-such-copy + unknown_instance: true + error: library.lending.CopyNotFound + + - name: library.lending.SuggestTitle + input: + - name: title + type: library.lending.Title + outcomes: + - name: placeholder + when: + title: {in_ignore_case: ["untitled", "tbd"]} + error: library.lending.ReservedTitle + + - name: noted + accepts: nothing +``` + +```yaml + - name: library.lending.CopyReturned + fields: + - name: copy_id + type: library.lending.CopyId + naming: {wire: copyId} + - name: title + type: library.lending.Title + - name: note + type: Optional + presence: omitted_when_absent + - name: _receipt + type: Uuid +``` + +These blocks, and every row of the table, are excerpts of the library raised to `ess/15` with the +fields, errors, events and views they name added (`loans`, `CopyNotFound`, `CopyWithdrawn`, a +`Copies` view publishing `state`, `title` and `loans`); that whole validates and synthesizes 23 +scenarios with 0 refusals. + +What synthesis does with them: an `unknown_instance:` scenario sends an id nothing holds; after a +`deletes:` it requires no `read_your_writes` view to hold the row, then re-sends for the unknown- +instance answer; `accepts: nothing` requires every parameterless immediate view unchanged; a +`when_subject` with `input.` gets one scenario with the input equal to the stored value and one +different. Two limits observed on this library: comparing or grouping by +`branch_id`, the `via:` of `Branch owns Copy`, left those scenarios unbuilt (`ESS-SYNTH-003`, +`ESS-SYNTH-017`), so the examples use `title`. + +**A limit per holder** ("a member can have five packets out at once"). Keep the count on the entity + +the command addresses, and guard on it: + +```yaml +entities: + - name: seeds.lending.Member + identity: {name: member_id, type: Uuid} + fields: + - {name: name, type: String} + - {name: packets_out, type: Integer} + invariants: + - packets_out >= 0 + - packets_out <= 5 + lifecycle: + initial: Active + states: [Active] + terminal: [Active] +``` + +```yaml + - name: seeds.lending.BorrowPacket + input: + - {name: member_id, type: Uuid} + outcomes: + - name: at-limit + when_subject: + predicate: packets_out >= 5 + error: seeds.lending.LimitReached + - name: borrowed + updates: seeds.lending.Member + instance: member_id + sets: {packets_out: {increment: 1}} + emits: [seeds.lending.PacketBorrowed] + payload: + seeds.lending.PacketBorrowed: {member_id: input.member_id} + - name: seeds.lending.ReturnPacket + input: + - {name: member_id, type: Uuid} + outcomes: + - name: nothing-out + when_subject: + predicate: packets_out <= 0 + error: seeds.lending.NothingOut + - name: returned + updates: seeds.lending.Member + instance: member_id + sets: {packets_out: {increment: -1}} + emits: [seeds.lending.PacketReturned] + payload: + seeds.lending.PacketReturned: {member_id: input.member_id} +``` + +The creating command (`Join`) sets `packets_out` from its input under `when: {packets_out: {gte: 0, +lte: 5}}`. Synthesis arranges a member only through that `sets:` and does not repeat `BorrowPacket`, +so with `packets_out: 0` fixed at creation the `at-limit` scenario is refused (`ESS-SYNTH-003`). +One outcome changes one record: a `Packet` moving to `OnLoan` is a second command the caller sends, +and the specification cannot require both to happen together. + +**A branch chosen by the held state.** When one command succeeds from one state, does nothing in a +second and refuses in the rest, guard each branch with `when_subject_state:` and let one +effect-free error answer every other state (`format: ess/7` or later): + +```yaml + - name: shop.tools.RetireTool + input: [{name: id, type: Uuid}] + outcomes: + - name: retired + when_subject_state: Active + moves: shop.tools.Tool.retire + instance: id + emits: [shop.tools.ToolRetired] + payload: + shop.tools.ToolRetired: {tool_id: input.id} + - name: already-retired + when_subject_state: Retired + preserves: shop.tools.Tool + instance: id + - name: in-maintenance + error: shop.tools.InMaintenance +``` + +Two limits, both `conflicting_declaration`: `when_subject_state:` goes only on a `moves:`, +`updates:` or `preserves:` branch with `instance:` (not on an `error:`, `external:` or `creates:` +branch), and a command that uses it declares no `wrong_state:` outcome. A `preserves:` branch +carries no error, event or `sets:` (`refusal_mutated_state`). + +A specification lowered to Entity Runtime is refused there, not at `validate`, for value +expressions (`ValueExpressionUnsupported`), the `ess/15` outcome shapes (`OutcomeShapeUnsupported`) +and the case-insensitive operators (`CaseFoldUnsupported`); keep those out of a model that must lower. diff --git a/plugins/ess/skills/specifying/references/syntax.md b/plugins/ess/skills/specifying/references/syntax.md index 74e4166..7dada20 100644 --- a/plugins/ess/skills/specifying/references/syntax.md +++ b/plugins/ess/skills/specifying/references/syntax.md @@ -3,6 +3,7 @@ A small lending library in three files, with every section a specification usually needs. It validates as written (`ess specify validate --path ` → `library v1 — 3 file(s), valid`). Copy the shape, not the domain: name your own entities, commands and events after your system. +It is written in `format: ess/1`; [later-formats.md](later-formats.md) adds what formats up to `ess/15` say. ## `system.yaml` @@ -54,7 +55,8 @@ naming: # Types: `newtype` over a primitive, `enum`, `struct` (with `fields`, optional `invariants`), # `union` (tagged: `tag:` plus `variants:` name → type). Primitives include Uuid, String, Integer, -# Decimal, Boolean, Bytes, Timestamp, Duration; wrappers Optional, List, Map. +# Decimal, Boolean, Bytes, Timestamp, Duration, Binary64 (ess/2) and Json (ess/15); wrappers +# Optional, List, Map. types: - name: library.lending.BranchId kind: newtype @@ -147,8 +149,11 @@ errors: - name: state type: library.lending.Copy.State -# Commands: input, then outcomes. An outcome `creates` an entity or `moves` one through a -# transition, `emits` events with a `payload` built from `input.`, or returns an `error`. +# Commands: input, then outcomes. An outcome `creates` an entity, `moves` one through a +# transition or `updates` its fields, `emits` events with a `payload` built from `input.`, +# or returns an `error`. `instance:` on `creates:` names a field of an emitted event that carries +# the new identity (`branch_id` of BranchOpened); on `moves:`/`updates:` it names the input field +# holding the identity (`copy_id` of LendCopy). Either other way is `undeclared_reference`. commands: - name: library.lending.OpenBranch naming: @@ -168,8 +173,8 @@ commands: name: input.name summary: The branch exists and holds no copies. - # Two outcomes of one command: all but one need a `when` over the command's input, or the result - # is not determined by the input and `validate` refuses it as `conflicting_declaration`. Error + # Two outcomes of one command: exactly one has no `when` (the default) and every other needs a `when` + # over the input. Two without a `when` is `conflicting_declaration`; every branch with a `when` is `non_exhaustive_branches`. Error # outcomes count; `wrong_state: true` outcomes do not (LendCopy below has two without a `when`). - name: library.lending.AddCopy naming: @@ -205,7 +210,10 @@ commands: summary: The page count was not positive, and nothing was added. # A command that moves an entity: `wrong_state: true` answers from every state the transition does - # not start from, so the `from:` list lives in one place. + # not start from, so the `from:` list lives in one place. A `copy_id` no record carries gets, in + # order: the command's `unknown_instance:` outcome (ess/15, later-formats.md); else its not-found + # outcome, an `external: ` refusal whose `error:` carries a field typed `CopyId`; else this + # `wrong_state` outcome, which is what LendCopy answers here. - name: library.lending.LendCopy naming: wire: lend-copy @@ -279,7 +287,9 @@ events: - name: copy_id type: library.lending.CopyId -# Views: read models over an entity. `read_your_writes` or `eventual`; an optional `filter`. +# 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. views: - name: library.lending.AvailableCopies source: library.lending.Copy @@ -334,6 +344,8 @@ inside that string are refused; write a conjunction, a disjunction or a set in t | present or absent | `when: {note: {exists: false}}` (= `not defined(note)`), `{defined: true}`, `{truthy: true}` | | every or some element of a list | `forall: {in: tags, as: t, that: t != ""}`, `exists: {in: tags, as: t, that: …}` — no compact form | | a list's length | `tags.count >= 0` | +| text starts with, ends with, contains (`ess/8`, case-sensitive) | `when: {title: {starts_with: "Draft: "}}`; `ends_with`, `contains` alike | +| text equal to one of a set, ignoring ASCII case (`ess/15`) | `when: {title: {in_ignore_case: ["untitled", "tbd"]}}`; one literal is `equals_ignore_case` | `validate` accepting a form does not mean `ess verify conform synthesize` can witness it. Two refusals to expect: an invariant over a list field (`tags.count`, a `forall` over `tags`) validates @@ -341,17 +353,20 @@ and is then refused as reading what no view publishes, even with the field in a guard over a required input (`{defined: true}`, `{exists: false}`) is refused because no candidate input leaves the field out. Relay such a refusal verbatim rather than reshaping the rule around it. -A `when` reads only the command's own input. A condition on another entity — "the branch must be -open", "the customer is active" — is not an input guard. Express it as a transition of that entity (a +A `when` reads only the command's own input. A value stored on the entity the command addresses is +read by `when_subject:` beside it (next table). A condition on **another** entity — "the branch +must be open", "the customer is active" — is neither. Express it as a transition of that entity (a command that `moves` it, answered by `wrong_state` from states it does not start from), or leave an `UNMAPPED:` marker naming the rule and report it; never invent an outcome the compiler cannot decide. -Two cases trials hit: +Four cases trials hit: | rule | how to write it | |---|---| -| a value stored on the entity decides the outcome ("express parcels over 20 kg are refused at dispatch", with the weight given at create) | not expressible: a `when` sees only the dispatch input. Mark it `UNMAPPED:` at the outcome, citing the source line. A guard over stored fields is proposed in ESS's [design note](https://github.com/beyond10x/ess/blob/main/docs/design/cross-record-and-stored-field-guards.md) | +| a value stored on the addressed entity decides the outcome ("express parcels over 20 kg are refused at dispatch", with the weight given at create) | `when_subject: {predicate: {all: [service == Express, weight_kg > 20]}}` on the refusing outcome (`ess/9`). It reads the entity's stored fields only, not `state`; a branch chosen by the held state is `when_subject_state:` ([later-formats.md](later-formats.md) shows it and its two limits); an open comparison needs a default branch, and a view must publish every guarded field | +| a stored value compared with the request ("a return scanned with another title is refused") | `when_subject: {predicate: title != input.title}` (`ess/15`); the input side is always `input.` on the right. ReturnCopy in [later-formats.md](later-formats.md) | | two records must not overlap ("a room cannot be booked twice for one hour") | make the contested unit an entity with its own lifecycle (a `Slot` that is `Free` or `Booked`); a second booking is then `wrong_state` on that slot. Overlap between arbitrary time ranges is not expressible; mark it `UNMAPPED:` | +| a holder may hold at most N ("a member can have five packets out at once") | keep the count on the holder and address the holder: `packets_out: Integer` on `Member`, a borrow command that `updates:` the member with `sets: {packets_out: {increment: 1}}` (`ess/14`), refused by `when_subject: {predicate: packets_out >= 5}` (`ess/9`), and a return with `{increment: -1}`. One outcome changes one record, so the packet's own move to `OnLoan` is a second command the caller sends, and nothing ties the two. Validated form: [later-formats.md](later-formats.md) | **Compare two fields through one struct.** A right-hand side without a dot is a literal, so `ends_at > starts_at` compares `ends_at` with the text `"starts_at"` and `validate` refuses it. diff --git a/plugins/ess/skills/testing-conformance/SKILL.md b/plugins/ess/skills/testing-conformance/SKILL.md index bd726a5..e0e36ee 100644 --- a/plugins/ess/skills/testing-conformance/SKILL.md +++ b/plugins/ess/skills/testing-conformance/SKILL.md @@ -49,7 +49,10 @@ target; the same scenario fails there once it also asserts that the `bookings` v `state: Cancelled` for the booking it cancelled. Add the observed-state assertion before running any mutation, because a mutation can only turn a scenario red when the scenario reads what changed. -Mutation is one of eight hardening techniques. Once the suite is green, `ess:hardening` carries the +Breaking the implementation by hand is the check here. `ess verify conform mutate` breaks the +specification instead and scores every mutant's suite against your implementation through +`--emit`/`--collect`; run it once the suite is green (`ess:hardening`, technique 1). Mutation is one +of eight hardening techniques. Once the suite is green, `ess:hardening` carries the rest — random command sequences against a reference model, caller replay, determinism, metamorphic relations, guard analysis, a spec diff in the gate and a design review — and the order to run them in. @@ -272,13 +275,23 @@ ess verify conform synthesize --path --target typescript --out < ``` Generate the Go package **inside the implementation's own module** (`--out /conformance`) so -its test can import it; a copy outside the module has no `go.mod` to import from. Numbers in a -payload you return compare equal only as `float64`: an `int64` or a `json.Number` fails although -the type check accepts it (beyond10x/ess#101, until fixed). The package's `README.md` lists the -methods and the wiring test. A method you cannot answer returns +its test can import it; a copy outside the module has no `go.mod` to import from. The package's +`README.md` lists the methods and the wiring test. A method you cannot answer returns `ErrUnsupported` (a skip), never a made-up result. Keep a suite for a domain nobody has implemented; run it once something answers it. +Three version facts decide whether these packages run a suite at all: + +- A command with `fixture_inputs:` needs the target to supply the values: implement + `FixtureValues` (Go) or `fixtureValues` (TypeScript). Without it every scenario using a fixture is + an explicit skip, so a fixture provider is the first thing to check when skips cluster there. +- A specification using `deletes:` or `accepts: nothing` synthesizes `ess-conformance/22`/`23`, and + one using `presence:` synthesizes `/24`/`25`. The Go and TypeScript runners refuse + those suites by version (the Go runtime admits up to `/21`), so no implementation of yours can be + held to them yet. Report such a suite as not run, never as a pass. +- Both packages also carry the random-sequence explorer (`explore`/`Explore`); `ess:hardening` + technique 2 says how to wire it. + ## Agents - `conformance` — raises or audits a suite's coverage under this skill. diff --git a/plugins/ess/skills/upgrade/SKILL.md b/plugins/ess/skills/upgrade/SKILL.md index fbbdd32..ad9f2b3 100644 --- a/plugins/ess/skills/upgrade/SKILL.md +++ b/plugins/ess/skills/upgrade/SKILL.md @@ -17,6 +17,10 @@ changed yet. - Otherwise show the actions in one list and ask once. After a clear yes: `b10x setup apply --plan ~/.local/state/b10x/plan.json --yes`. - A new plugin version loads in a new session; until then `b10x skill ess:` prints the new text. +- A project whose `ess-inputs.yaml` pins `requires: ess X.Y.Z` keeps running that release after the + upgrade. Moving the project is a separate change to its repository: + `ess specify toolchain install --pin` rewrites the pin, and + `ess specify toolchain which` confirms the release that now runs there. No `b10x`? Follow https://github.com/beyond10x/agentplugins/releases/latest/download/SETUP.md first. diff --git a/plugins/worktree/.claude-plugin/plugin.json b/plugins/worktree/.claude-plugin/plugin.json index 2b7b831..f104d22 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.15.0", + "version": "0.16.0", "author": { "name": "Beyond10x" }, diff --git a/plugins/worktree/.codex-plugin/plugin.json b/plugins/worktree/.codex-plugin/plugin.json index 9cf1b80..4dcfae7 100644 --- a/plugins/worktree/.codex-plugin/plugin.json +++ b/plugins/worktree/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "worktree", - "version": "0.15.0", + "version": "0.16.0", "description": "Create, lease, finish, audit and safely clean isolated Git worktrees through the worktree CLI.", "author": { "name": "Beyond10x" diff --git a/plugins/worktree/skills/init/SKILL.md b/plugins/worktree/skills/init/SKILL.md index 0a98bcf..4ec3a9c 100644 --- a/plugins/worktree/skills/init/SKILL.md +++ b/plugins/worktree/skills/init/SKILL.md @@ -43,6 +43,10 @@ path to the directory that holds their repositories) rather than guessing it. Wi `worktree doctor --check` fails with `no active profile` until `activate` has run. +Add `--install-agent-guidance` to `activate` only when the user asks for it: it writes a managed +guidance block, pointing at `worktree:managing-worktrees`, into `~/.claude/CLAUDE.md` and +`~/.codex/AGENTS.md`, and replaces only the text between its own markers. + **A repository needs a remote before its trees can be cleaned up.** Cleanup proves that each commit reached a remote, so in a repository with no remote (`git remote` prints nothing) `create` works and every later `gc` refuses. Tell the user before they start work there. diff --git a/plugins/worktree/skills/managing-worktrees/SKILL.md b/plugins/worktree/skills/managing-worktrees/SKILL.md index 70ff57f..18e2a68 100644 --- a/plugins/worktree/skills/managing-worktrees/SKILL.md +++ b/plugins/worktree/skills/managing-worktrees/SKILL.md @@ -31,7 +31,7 @@ After verification, preserve the small logs, reports, or deliverables needed for ## Finish and clean up 1. Commit and publish every wanted change. A local-only commit is deliberately not cleanup-safe. Work merged as rebased or cherry-picked copies also qualifies when an advertised ref carries every unique commit's exact patch; GC reports that proof as `patch-equivalent`. - When work must not be published, run `worktree archive ` instead. It never modifies the tree; it writes `commits.bundle` (every commit no advertised ref holds), `dirty.patch` (tracked, untracked and ignored changes over HEAD) and a `worktree.archive/1` `manifest.json` below the state directory's `worktree/archives///`, and verifies them. GC then accepts that archive as `archive` proof while HEAD and every file still match it exactly; any later commit or edit is refused as `archive-stale` until `worktree archive --replace ` writes a new one. `--replace` moves the old archive aside and never deletes it. + When work must not be published, run `worktree archive ` instead. It never modifies the tree; it writes `commits.bundle` (every commit no advertised ref holds), `dirty.patch` (tracked, untracked and ignored changes over HEAD) and a `worktree.archive/1` `manifest.json` below the state directory's `worktree/archives///`, and verifies them. GC then accepts that archive as `archive` proof while HEAD and every file still match it exactly; any later commit or edit is refused as `archive-stale` until `worktree archive --replace ` writes a new one. `--replace` moves the old archive aside and never deletes it. Every ignored file is archived, so remove this task's build output first (see *Bound disposable storage*); submodules, nested repositories, special files and paths containing a newline are refused as `archive-unsupported-entry`. GC apply returns an archived dirty tree to HEAD, removes it without force, and keeps the archive. 2. Preserve required evidence and remove this task's disposable output as described above. Release your own lease, then run `worktree finish `. It refuses locked, unmanaged, live, or mid-operation Git worktrees, and dirty ones unless their archive holds exactly the current state. 3. Run `worktree gc --repo --dry-run --id ` and inspect every result. Always pass `--id`. Without exact ids, `--repo` selects the activated workspace profile, not just the repository: the assessment covers records under that profile's `workspace_root`, including other repositories. 4. Run `worktree gc --repo --apply --id ` with repeated `--id` values only for the exact results intended for removal. The command refreshes remote advertisements, fetches required objects, and revalidates immediately before non-forced removal. Check the result before reporting storage reclaimed. @@ -46,11 +46,11 @@ After verification, preserve the small logs, reports, or deliverables needed for - Apply reconciliation only to ids copied from that immediately preceding dry-run with `worktree reconcile --repo --apply --id ` and repeated `--id` arguments when needed. - If that dry-run explicitly proposes `retire-external`, confirm that destructive action separately by adding `--allow-external-retirement`; never add it for an unrelated migration or missing-record repair. - A finished external legacy tree may supersede a stale migration intent only when the dry-run itself proposes `retire-external`; never reinterpret or bypass a cross-device or ambiguous-relocation refusal. -- If removal is interrupted while the path still exists, rerun GC dry-run and exact-id apply. If the path is already absent, use reconciliation dry-run and exact-id apply; its durable removal intent can safely finish the recorded transition. +- If removal is interrupted while the path still exists, rerun GC dry-run and exact-id apply; it finishes a removal Git already unlinked only when every remaining file is the commit's tracked content, and otherwise retains the tree as `removal-residue-unproven`. If the path is already absent, use reconciliation dry-run and exact-id apply; its durable removal intent can safely finish the recorded transition. - A missing Active record without matching durable removal intent stays refused while its work may still exist. Preserve and investigate its registry evidence; never edit the registry by hand, delete related state, or fabricate recovery proof. If its recorded commit still exists anywhere, publish it and rerun the dry-run. - Only once you have established that such a record's recorded commit is gone for good, abandon it with `worktree reconcile --repo --apply --id --acknowledge-unrecoverable `. That acknowledgement asserts one exact commit named by the immediately preceding dry-run; the command still checks it and refuses while any local branch, tag, remote-tracking ref, or remote advertisement contains it. It deletes nothing from disk or from Git, and records the tombstone with no recovery proof, because there is none to record. - A record whose repository was deleted (its root is gone, or has no `.git`) is reported as `repository-missing`, naming the repository, the tree path and the recorded commit. Git cannot check anything for it, so the dry-run's own `--acknowledge-unrecoverable ` apply, run from any live repository of the same workspace as `--repo`, is the only way to retire it; select it with `--id`. It is refused as `worktree-path-exists` while the tree path, or a relocation or removal intent's path, still exists: deal with that tree yourself first. Never recreate the repository just to make reconciliation run. -- An archive outlives the tree it retired. Restore it from a `--no-checkout` clone that has the advertised refs: first write `* -text -eol -filter -ident -working-tree-encoding` to `.git/info/attributes` so that attributes cannot rewrite the archived bytes, then `git fetch /commits.bundle refs/worktree-archive/head:refs/heads/`, and run both `switch ` and, when the archive has one, `apply --binary --whitespace=nowarn /dirty.patch` as `git -c core.autocrlf=false -c core.fileMode=true -c core.symlinks=true …`. Never use `--attr-source` for this: Git 2.55 `apply` crashes with it. Never delete an archive to make GC pass; `archive-digest-mismatch` and `archive-incomplete` mean it no longer proves recovery. +- An archive outlives the tree it retired. Restore it from a `--no-checkout` clone that has the advertised refs: first write `* -text -eol -filter -ident -working-tree-encoding` to `.git/info/attributes` so that attributes cannot rewrite the archived bytes, then `git fetch /commits.bundle refs/worktree-archive/head:refs/heads/`, and run both `switch ` and, when the archive has one, `apply --binary --whitespace=nowarn /dirty.patch` as `git -c core.autocrlf=false -c core.fileMode=true -c core.symlinks=true …`. Never use `--attr-source` for this: Git 2.55 `apply` crashes with it. Never delete an archive to make GC pass; `archive-digest-mismatch`, `archive-incomplete`, `archive-bundle-invalid` and `archive-invalid` mean it no longer proves recovery, and `archive-stale` means the tree changed after it was written. - `worktree-hidden-state` (assume-unchanged or skip-worktree entries, staged content only the index holds, a nested `.git`) and `worktree-local-refs` (refs under `refs/worktree/`, `refs/bisect/`, `refs/rewritten/`) retain a tree whether or not it is archived, because Git status does not show that state and removal would destroy it. Resolve the named state yourself; never clear it just to make GC pass. - Run `worktree doctor --check` for prerequisites and configuration. It exits non-zero and names each failure, including `no active profile` when no workspace profile is activated. - Only after a human explicitly decides an existing linked tree should become manager-owned, run `worktree repo adopt --repo --path --id --purpose `. Then review `reconcile --dry-run` and use exact-id apply only if migration is intended. diff --git a/trials/baseline.json b/trials/baseline.json index 14cd741..7b99e76 100644 --- a/trials/baseline.json +++ b/trials/baseline.json @@ -1,12 +1,12 @@ { "aep-backlog": { - "tool_calls": 313 + "tool_calls": 278 }, "ess-full-package": { - "tool_calls": 68, + "tool_calls": 53, "validate": "valid", "synthesis": { - "scenarios": 13, + "scenarios": 20, "refusals": 0 }, "unmapped": 0, @@ -22,37 +22,38 @@ "types-rust" ], "go_test": { - "passed": 13, + "passed": 20, "failed": 0, "skipped": 0 } }, "ess-new": { - "tool_calls": 12, + "tool_calls": 38, "validate": "valid", - "unmapped": 7 + "unmapped": 4 }, "ess-pipeline": { - "tool_calls": 19, + "tool_calls": 11, "validate": "valid", "synthesis": { - "scenarios": 12, + "scenarios": 14, "refusals": 0 }, "outputs": [ "conformance", "docs", + "openapi", "schema" ] }, "ess-retrofit": { - "tool_calls": 29, + "tool_calls": 36, "validate": "valid", "synthesis": { - "scenarios": 16, - "refusals": 2 + "scenarios": 20, + "refusals": 0 }, - "unmapped": 7 + "unmapped": 3 }, "worktree-onboarding": { "tool_calls": 19 diff --git a/trials/ess-pipeline/fixture/spec/domains/booking.yaml b/trials/ess-pipeline/fixture/spec/domains/booking.yaml index 1bd9291..3157a9e 100644 --- a/trials/ess-pipeline/fixture/spec/domains/booking.yaml +++ b/trials/ess-pipeline/fixture/spec/domains/booking.yaml @@ -87,6 +87,11 @@ commands: when: hours > 0 creates: rehearsal.booking.Booking instance: booking_id + sets: + band: input.band + room: input.room + starts_at: input.starts_at + hours: input.hours emits: - rehearsal.booking.RoomHeld payload: diff --git a/verified.json b/verified.json index 8149c2d..7b00b05 100644 --- a/verified.json +++ b/verified.json @@ -1,5 +1,5 @@ { - "aep": "0.60.0", - "ess": "0.35.0", + "aep": "0.63.1", + "ess": "0.37.0", "worktree": "0.8.2" } diff --git a/website/docs/golden-path.md b/website/docs/golden-path.md index 521364b..c7ed83b 100644 --- a/website/docs/golden-path.md +++ b/website/docs/golden-path.md @@ -63,7 +63,9 @@ $ aep plan reverse init --protocols 'git+https://github.com/beyond10x/aep#8b4342 `reverse init` refuses the two things that break quietly later — an absolute path, and a `git+` source pinned to a branch rather than a commit — which is why the source above carries a full commit -hash. +hash. A current `aep` writes the project as `aep.project/5` (`store: {git: {}}`) and prints one more +line saying so: the artifact files under `.engineering/planning/` are the store, and a move appends +one line to the artifact's `transitions`. ```shell-session $ aep plan reverse scan @@ -280,8 +282,8 @@ its story's body. ``` The scopers are read-only, so run one per story and run them at once. The write-back is serial — the -store's journal is append-only and parallel writers race — and it goes through the CLI like every -other change to a body: +store takes one writer at a time, and each write bumps the story's revision — and it goes through +the CLI like every other change to a body: ```shell-session $ aep plan artifact body story:commercial-client-record --from record-body.md diff --git a/website/docs/plugins/aep.md b/website/docs/plugins/aep.md index 512d1f4..8dcc6c4 100644 --- a/website/docs/plugins/aep.md +++ b/website/docs/plugins/aep.md @@ -45,6 +45,11 @@ The plugin respects store ownership: machine-owned artifact metadata is changed by editing markdown frontmatter. A refusal from the lifecycle is a result to report, not a guard to route around. +The store is `aep.project/5`: each artifact file under `.engineering/planning/` is the authority, a +move appends one line to its `transitions`, and each evidence record is one file under +`.engineering/evidence/`. When a repository's store is on an older version, the planning and +delivery skills say so before their first write and name the upgrade; `aep:upgrade` runs it. + ## Delivery It provides: diff --git a/website/docs/plugins/ess.md b/website/docs/plugins/ess.md index 640cdf5..49b5ba0 100644 --- a/website/docs/plugins/ess.md +++ b/website/docs/plugins/ess.md @@ -11,10 +11,10 @@ to raise or audit a conformance suite against a real implementation. | skill | for | agent | |---|---|---| | `ess:init` | install the `ess` CLI, learn what ESS is, take the first step | — | -| `ess:specifying` | write or extend a specification; `references/syntax.md` shows every section in one that validates | `author` | +| `ess:specifying` | write or extend a specification; `references/syntax.md` shows every section in one that validates, `references/later-formats.md` what formats up to `ess/15` add | `author` | | `ess:retrofitting` | derive a specification for a system that has none | `retrofitter` | | `ess:testing-conformance` | raise or audit what a conformance suite tests | `conformance` | -| `ess:hardening` | after a green suite, the eight techniques that ask what it cannot; `references/` holds each procedure, the reference-model pattern, a design-review brief and spec-diff classification | — | +| `ess:hardening` | after a green suite, the eight techniques that ask what it cannot, starting from `ess verify conform mutate` and the explorer in the generated Go and TypeScript packages; `references/` holds each procedure, the reference-model pattern, a design-review brief and spec-diff classification | — | | `ess:upgrade` | check the plugin and CLI, offer the upgrade | — | ```text diff --git a/website/docs/plugins/worktree.md b/website/docs/plugins/worktree.md index a84c5fe..dc30f12 100644 --- a/website/docs/plugins/worktree.md +++ b/website/docs/plugins/worktree.md @@ -31,9 +31,10 @@ worktree doctor --check The skill teaches agents to create trees outside the primary repository collection, maintain live session leases, and publish wanted commits before finishing. Cleanup is review-bound: inspect `worktree gc --repo --dry-run`, then pass only ids from that review to -`worktree gc --repo --apply --id `. Dirty, locked, live, local-only, -offline, unmanaged, and out-of-policy trees are retained. Work merged as rebased or cherry-picked -copies is recoverable when an advertised ref carries every unique commit's exact patch. +`worktree gc --repo --apply --id `. Locked, live, offline, unmanaged, and +out-of-policy trees are retained, and so are dirty and local-only trees unless `worktree archive` +holds their exact current state. Work merged as rebased or cherry-picked copies is recoverable when +an advertised ref carries every unique commit's exact patch. `/worktree:cleanup [--repo ] [--id …]` starts that review by hand. It is a command: only you start it, never the model, and it hands off to `worktree:managing-worktrees` for every @@ -48,7 +49,10 @@ or authorize removal. Use `worktree reconcile --repo --dry-run` for interrupted provisioning, adopted legacy paths, finished external trees, and already-missing records. Apply only exact reviewed ids. External retirement additionally requires the explicit `--allow-external-retirement` -acknowledgement. +acknowledgement. A missing record whose recorded commit is gone for good, including one whose +repository was deleted (`repository-missing`), is retired only with +`--acknowledge-unrecoverable `, which the command refuses while any ref still +contains that commit. The plugin contains no cleanup script and no independent policy copy; `agentplugins-check tools` checks every command it spells against the newest `worktree` release.