From 4dbab7408de53390d0e26755666fb6106992f467 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 12:24:22 +0200 Subject: [PATCH 001/120] docs: mark the errata wave-one spec implemented --- docs/specs/2026-08-31-review-loop-errata-wave-one.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs/2026-08-31-review-loop-errata-wave-one.md b/docs/specs/2026-08-31-review-loop-errata-wave-one.md index 2c125e7..1ebbb3e 100644 --- a/docs/specs/2026-08-31-review-loop-errata-wave-one.md +++ b/docs/specs/2026-08-31-review-loop-errata-wave-one.md @@ -1,7 +1,7 @@ --- ticket: none date: 2026-08-31 -status: draft +status: implemented architect: blocking (adjudicated 2026-09-01) revises: ./2026-08-17-autonomous-review-loop-design.md branch: feature/audit-errata From 98de97eebbdc7c232dfcaa6c779c7c184e2a8215 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 12:24:22 +0200 Subject: [PATCH 002/120] docs: recover two stalled working-process design drafts --- ...07-27-working-process-discipline-design.md | 388 ++++++++++++++++++ ...6-07-28-multi-domain-code-review-design.md | 304 ++++++++++++++ 2 files changed, 692 insertions(+) create mode 100644 docs/specs/2026-07-27-working-process-discipline-design.md create mode 100644 docs/specs/2026-07-28-multi-domain-code-review-design.md diff --git a/docs/specs/2026-07-27-working-process-discipline-design.md b/docs/specs/2026-07-27-working-process-discipline-design.md new file mode 100644 index 0000000..c235394 --- /dev/null +++ b/docs/specs/2026-07-27-working-process-discipline-design.md @@ -0,0 +1,388 @@ +--- +ticket: none +date: 2026-07-27 +status: draft +branch: feature/wp-process-discipline +base: develop +--- + +# Working-process discipline — design + +## Overview + +Five process changes drawn from the Private-memory backlog. Four land in +the `working-process` rules and agents; one — the +`marketplace-plan-review` checklist of decision 5 — lives at repo level, +for the domain this repository's own plans touch: + +1. the **implementation-start gate** for committing process documents; +2. **branch-closeout offers** — a code review and a Project memory + review, both offered while the integration decision is still open; +3. **background dispatch** for the architect and plan-adversary agents; +4. two **author duties** discharged before the adversary round; +5. the **`marketplace-plan-review`** checklist skill. + +Every change is a text edit to an existing surface: no new rule file (the +Rules payload stays five files), no new plugin component, no change to +the report contract. Multi-domain review dispatch and the generic +reviewer — designed alongside these and split out on the architect +session's recommendation — live in +`docs/specs/2026-07-28-multi-domain-code-review-design.md`. No version +bump on the topic branch; the release PR sizes it. + +## Motivation + +**The commit-timing clause is too loose.** `spec-plan-lifecycle` says +to suggest committing the work's documents "when implementation is +about to start" — wide enough to read as "any time before code", which +is how documents ended up committed mid-authoring (Private-memory +session, 2026-07-20). The gate the developer actually wants has two +conditions, and neither is "the plan looks finished". + +**The flow has no closeout.** It ends at step 6, Implementation. +Nothing offers a code review of what was just built, and nothing offers +to close out the Project-memory notes the work accumulated. The +obvious-looking hook — `status: implemented` — does not work here: in +this project that stamp often lands only after the PR merges, so an +offer made there arrives after the integration decision it was supposed +to inform, and after the branch where the fixes belong is gone. + +**Verdict rounds still freeze the session.** #10 proved background +dispatch end-to-end for review runs: 9-to-31-minute runs with the +developer working throughout, the summary arriving as a task +notification and nothing lost. Architect and adversary rounds have the +same shape — long, self-contained, one verdict at the end — and today +block the session that dispatches them. + +**Plan authoring wastes adversary rounds.** The #10 plan took three +rounds (blocking, blocking, concerns) and 17 findings, recorded in that +plan's Review rounds section. Zero were against design decisions; all +were against execution precision, and round 1 was avoidable: the plan's +verification greps were written as single-line matches against text the +plan itself wrapped across line breaks, and were never executed; +ownership claims ("X owns this", "no fork") were enumerated from memory +instead of `rg`, missing two live restatement surfaces. + +That failure mode reproduced while authoring *this* spec. Enumerating +the consumers of a phrase this change edits, a fixed-string `rg` for +``notes and `INDEX.md` `` returned `ticket-frontmatter.md` but not +`process-artifacts.md` — which carries the phrase with a line break +inside it. A `-U` multiline retry found both. The class of bug the +duties below prevent cost a false-negative consumer list in the very +work that adds them. + +**The adversary has no checklist for this repo's domain.** During #10 +it scanned for a `*-plan-review` match, found none, and worked from an +ad-hoc dispatch prompt. Seven of the 17 findings turned on knowledge +that exists only here: that a Rules payload is read from the installed +rule file and not the plugin cache (so a dogfooding gate verifying the +cache misattributes failures); that pass criteria can be vacuously +satisfied by a target which never exercises the new paths; that a rerun +must launch through the real surface; that `description:` fields carry +invoker chains; that conditional references degrade under a Standalone +install; that a restatement dropping one clause lets the standalone +shape drift; that changing who performs an act obliges a glossary +update. + +## Decisions + +### 1. The implementation-start gate + +`spec-plan-lifecycle`'s final paragraph is replaced. Proposed wording: + +> Suggest committing the work's documents under `docs/` only at the +> implementation-start gate: the developer has decided that +> implementation begins. Authoring is never that moment — not while a +> spec is drafted, not between review rounds, not while the plan is +> written. The suggestion never becomes the act: this rule governs when +> committing may be *suggested*, never whether it may happen, and an +> open gate authorizes nothing. Only paths git tracks or would track; +> deliberately ignored documents are skipped silently. + +The prohibition on authoring-time suggestions is stated rather than left +implied by "only": it is the behavior the change exists to stop, and an +implication is not greppable. + +The gate carries **one** condition, deliberately. The developer's intent +was two — the decision to start plus commit authorization — and the +substance of the second is kept, as an absolute constraint on the act +rather than a condition on the suggestion. The reason is that +`working-process` defines no observable signal for "authorized", while +its sibling clauses are built on signals that are checkable (the +first-create question turns on a `.gitignore` containing exactly `*`, a +git-tracked file, or a declared instruction). The rule that governs +commit authorization lives outside this plugin — in the developer's own +environment — so a distributed rule naming it as a gate condition would +hand every other consumer an untestable term, which reads as an escape +hatch. Phrasing it as "this rule never authorizes; authorization is never +inferred" is the stronger of the two forms: it binds the act +unconditionally instead of binding the suggestion conditionally. + +### 2. Branch closeout owns both offers — `workflow.md` step 7 + +> 7. **Implementation → branch closeout.** When implementation on the +> topic branch is complete and the integration decision is still +> open — where superpowers:finishing-a-development-branch runs, when +> available — offer, each independently: +> - a code review, naming each available `*-code-review` skill whose +> domain the changed files touch — plugin-provided or project-level +> alike, discovery mirroring the `*-plan-review` convention; the +> dispatch mechanics stay with their owners, this step only makes +> the offer; +> - a Project memory review, when `memory-review-session` is +> available and the project keeps a store (`docs/memory/INDEX.md` +> or `.claude/memory/INDEX.md` — no store, no offer). +> +> The hook is branch completion, not the `status: implemented` stamp: +> that stamp often lands after the PR merges, too late to act on +> either offer. + +Three consequences worth stating: + +- **One owner.** Step 7 is the only surface that makes these offers. + `spec-plan-lifecycle`'s "Lifecycle offers" paragraph gains nothing — + not even a pointer — because a second mention of a branch-time offer + in a document-lifecycle rule is exactly the fork #10 spent a round + removing. +- **The memory-review offer moves off the `implemented` hook** its + backlog entry proposed, for the same reason as the code review, and + because one closeout conversation beats two. +- **No project-memory edit.** `memory-review-session` stays + explicit-ask-only; an offer the developer accepts *is* the explicit + ask. Recorded here so a later reader does not mistake step 7 for a + second trigger on that skill. + +The store probe copies the candidate-gap offer's probe in +`review-reports` — the same two paths, the same no-store-no-offer rule. + +### 3. Background dispatch for both verdict agents + +`workflow.md` gains a paragraph beside the existing model-selection one, +which already governs "these dispatches"; steps 3 and 5 are untouched, +so the statement has one home: + +> Dispatch mode for these reviews: always in the background — the +> dispatching session stays interactive, and the verdict arrives as a +> task notification the dispatcher relays when it lands. The step the +> verdict gates still waits for it: background dispatch buys the +> developer's time back, not a reordered flow — a plan is not written +> while the architect round runs, and implementation does not start +> while the adversary round runs. Stamping happens after the relay, +> never before: the self-reported model is compared against the +> dispatched and prescribed tiers at that point. + +The gate is deliberately preserved. Writing a plan against a design the +architect may reject wastes exactly the work the round exists to +prevent; what background dispatch recovers is the developer's time, not +the process's ordering. + +**A round in flight is recorded before it starts.** Background dispatch +opens a hole synchronous dispatch did not have: the verdict lives only in +the notification, and a session that ends before the relay takes the +round with it — the verdict agents write nothing, by design. So +`spec-plan-lifecycle` gains a pending marker, exactly as it already +carries one for grilling (`grilled: grilling`): + +- the dispatcher stamps `architect: dispatched` / `adversary: dispatched` + BEFORE dispatching, and the relayed verdict replaces it. This is not a + verdict stamp and does not weaken the after-the-relay rule above: it + records that a round is owed, never its outcome; +- the value set of both fields therefore extends to + `dispatched | LGTM | concerns | blocking`, with `dispatched` documented + as the in-flight marker; +- unfinished process work stays greppable — the rule's own promise — + with one more pattern beside the existing two: + `rg -l '^(architect|adversary): dispatched$' docs/`; +- a fresh dispatch overwrites the marker; a round the developer abandons + is cleared by removing the field, the same explicit act that resolves + any other pending marker; +- one existing sentence of the same rule is a consumer and must move with + it: "`grilled`, `architect`, and `adversary` appear only once the + corresponding step has run" becomes true of the step having *started* + for the two verdict fields. + +The findings of a lost round are still lost — recovering those would mean +a durable round artifact written by the agent, which is a new artifact +kind (location, frontmatter, directory mode, lifecycle) and belongs to +its own work. What this decision refuses is the silent version, where an +interrupted round leaves the document indistinguishable from one never +reviewed. + +Both agents' `description:` fields note that they are dispatched in the +background and that the verdict arrives as a task notification, and each +`## Stamping` section gains one clause: the dispatcher stamps after +relaying the run's reply. The cap-refusal flow is unaffected — a refusal +happens at dispatch time, while the session is still there to be asked. + +`grilling-session` and `architect-session` are in-session consultations +with no verdict and no dispatch; they are out of this decision. + +### 4. Author duties before the adversary round — `workflow.md` step 4 + +> 4. **Spec → plan.** Write the implementation plan with +> superpowers:writing-plans when available; plans live in +> `docs/plans/`. Before the adversary round, the author verifies the +> plan against itself: +> - every verification command the plan names is RUN against the text +> it is meant to match — the plan's replacement texts written to +> scratchpad files outside version control first, never into the +> repository, whose tree stays clean and whose Process directories +> stay undisturbed — and fixed where it misses; a command that has +> never been executed is not a verification step; +> - every ownership claim ("X owns this", "supersedes", "no fork") +> carries a whole-repo `rg` result for the superseded phrase, +> quoted in the plan — never a recollection. + +Both duties are domain-agnostic: any plan that names a check can execute +it first, and any plan that claims sole ownership of a statement can +enumerate the alternatives. They sit in the flow step that produces the +plan, not in the checklist that reviews it — the point is to spend the +author's cheap minute instead of the reviewer's expensive round. + +### 5. `marketplace-plan-review` at repo level + +`.claude/skills/marketplace-plan-review/SKILL.md`, shaped like +`python-plan-review`: frontmatter `name` and `description` ending +"invoked by the plan-adversary agent", then dimensions carrying their +own severities, each finding citing a plan quote or file path. + +Dimensions, each drawn from a #10 finding rather than invented: + +1. **Rules payload and drift** — a plan that changes rule content + verifies the *installed* rule file, not the plugin cache; content-hash + sync and the SessionStart drift nudge are accounted for. +2. **Dogfooding gate design** — pass criteria cannot be vacuously + satisfied; the target must exercise the new paths; runs launch through + the real surface (the command, not a hand-rolled dispatch); every + changed surface is exercised at least once. +3. **Version channels** — no bumps on topic branches; dogfooding + unreleased content needs `-dev.` because the plugin cache keys + by version; prerelease grammar respected. +4. **Restatement and conditional-reference discipline** — restatements + carry the full clause; `description:` invoker chains stay accurate; + conditional references degrade correctly under a Standalone install; + the authoring rubric appears only where its owning rule permits, and + the checklist cites that rule for the count instead of restating it — + a second statement of the same arithmetic is the very fork the rule + forbids. +5. **Glossary and ADR upkeep** — a plan that changes who performs an act, + or introduces a term, updates `docs/domain/glossary.md`. +6. **Commit and hygiene rules** — one-line conventional-commit subjects; + no machine paths, no company names, English only. + +The name is kept as the backlog entry wrote it. The prefix names this +marketplace as the domain; the alternative considered and rejected was +`plugin-authoring-plan-review`, which describes the work more narrowly +than the checklist's dimensions do (channels, catalog sync and hygiene +are marketplace-level, not per-plugin). + +The skill lives at repo level, next to the `.claude/rules/` files whose +conventions it checks, so consumers of `working-process` in other +repositories do not receive a checklist about this marketplace. + +**One assumption to verify, not assume:** that a dispatched +`plan-adversary` — a subagent — actually discovers project-level skills +in `.claude/skills/` when it scans for `*-plan-review`. If it does not, +the dispatch prompt names the checklist path explicitly. The plan owns +this as a named check with both outcomes handled; nothing here depends +on the discovery working. + +## Surfaces and changes + +| Surface | Change | +| --- | --- | +| `rules/workflow.md` | step 4 author duties; new step 7; new dispatch-mode paragraph | +| `rules/spec-plan-lifecycle.md` | final paragraph replaced by the implementation-start gate; `dispatched` added to both verdict fields' value set, its frontmatter example and the unfinished-work greps | +| `rules/process-artifacts.md` | `ARCHIVE.md` added to the registry-file exemption | +| `rules/ticket-frontmatter.md` | `ARCHIVE.md` added to the Project-memory bullet | +| `agents/architect.md` | `description:` dispatch mode; stamping-after-relay clause | +| `agents/plan-adversary.md` | same two edits | +| `plugins/working-process/README.md` | architect and plan-adversary bullets and the Model selection section note background dispatch | +| `.claude/skills/marketplace-plan-review/SKILL.md` | new file | +| `.claude/rules/plugin-authoring.md` | one line pointing at the new checklist (reference, not restatement) | + +`ARCHIVE.md` rides along because both enumerations read as closed lists +that omit it while the exemption already holds by delegation — a +one-line clarification each, flagged by the Fable architect re-review of +2026-07-22. + +No plugin component is added or renamed, so the three component +enumerations the marketplace-sync rule keeps in step — the manifest's +`description`, the catalog entry, and the root README row — stay +untouched. They are named here because the companion spec does add +components and must edit all three. + +Also unchanged and deliberately so: `rules/review-reports.md` (its +background wording is about review runs, a different subject from a +verdict agent's dispatch mode, and this spec adds no report semantics); +both standards plugins and their READMEs (`*-plan-review` discovery is +unaffected); `ARCHITECT_PERSONA.md`; and every project-memory surface. + +## Invariants + +- Every step in the flow stays an offer the developer may decline. +- One statement, one owner: step 7 owns the closeout offers; the + dispatch-mode paragraph owns background dispatch; the agents' bodies + carry only the stamping clause. +- A declined offer is never repeated uninvited — every step of the flow + offers, and none nags. +- The Rules payload stays five files, so the README's count stays true. +- No behavior depends on a check that has not been run — the duties in + decision 4 bind this work's own plan. + +Candidate glossary terms for the grilling session, not decided here: +**branch closeout** (implementation complete on the topic branch, +integration decision still open), **implementation-start gate**, +**author duty**, **round in flight** (a dispatched review round whose +verdict has not been relayed, marked `dispatched`). + +## Out of scope + +- **Multi-domain review dispatch and the generic reviewer.** Designed in + the same session and split out on the architect session's + recommendation (2026-07-28): they add two plugin components, two report + contract additions and two canonical-term changes, which would make + five cheap text edits hostage to the release's riskiest part. They live + in `docs/specs/2026-07-28-multi-domain-code-review-design.md`. Step 7's + offer needs no change when they land — only what the offer dispatches + does. +- The **finding router** (review → local fix / plan / spec seed) stays + parked in Private memory: what to *do* with findings — fix locally, + plan a sweep, or seed a spec — is its own design problem with its own + provenance requirements. +- Encoding a verification or smoke-test step in the flow. Step 7 says + "before the integration decision" and claims no ordering against + project-specific verification. +- A new rule file for plan authoring. +- Ticket numbers: the authoring session had no network access to the + issue tracker, so this spec carries `ticket: none` and the branch is + `feature/wp-process-discipline`. Both are backfilled per + `ticket-frontmatter`'s backfill clause once an issue exists; the branch + is renamed to `feature/-wp-process-discipline` at that point. + +## Versioning and validation + +No version bump on the topic branch (plugin-versioning rule). The +accumulated change is **minor** for `working-process` — a new flow step, +a new paragraph and added author duties, all backward-compatible — sized +in the release PR. + +Every change here is rule and agent text, so nothing in this spec can be +falsified by running code; what can be checked is that each edit lands +where it is meant to and forks nothing. Validation is therefore the +adversary round plus the plan's own named checks: per-phrase `rg -c` +verifications executed against the final text (decision 4's first duty +applied to this work's own plan), the whole-repo `rg` consumer lists +behind every ownership claim (its second duty), and the +checklist-discovery probe of decision 5, whose two outcomes are both +handled. + +A dogfooding round would still be the only way to see the new offers +fire in a live session. It runs against the *installed* payload, which +the plugin cache keys by version, so it needs a `-dev.` string and +waits for an issue number — and it is worth one round precisely because +step 7 fires at a moment (branch complete, integration open) that no +grep can observe. + +After the rule edits, installed copies emit the SessionStart drift nudge +until re-synced. That is the engine working, not a regression. diff --git a/docs/specs/2026-07-28-multi-domain-code-review-design.md b/docs/specs/2026-07-28-multi-domain-code-review-design.md new file mode 100644 index 0000000..f5591d9 --- /dev/null +++ b/docs/specs/2026-07-28-multi-domain-code-review-design.md @@ -0,0 +1,304 @@ +--- +ticket: none +date: 2026-07-28 +status: draft +--- + +# Multi-domain code review — design + +## Overview + +Two components that turn the branch-closeout code-review offer from a +list of commands the developer runs by hand into one resolved, offered +and dispatched review: + +1. the **`code-review-dispatch`** skill — resolve which domains the + changed files touch, offer the resolved targets, run one shared + first-create check, fan out the domain reviewers in the background, + relay their replies; +2. the **`code-reviewer`** agent — a generic review for changed files no + installed standards plugin claims. + +Split out of `docs/specs/2026-07-27-working-process-discipline-design.md` +on the architect session's recommendation (2026-07-28): that spec's five +changes are text edits to existing surfaces, while these two add plugin +components, report-contract semantics and canonical-term changes. Its +step 7 needs no edit when this lands — only what its offer dispatches +changes. + +**This spec is not ready for an architect round.** Six open design +questions are recorded below; three of them (the two term changes and the +dispatch-discipline owner) came from the architect session that split the +work and must be answered before the design is coherent. + +## Motivation + +A repository holding two domains — Python services next to Salesforce +metadata is the live case — has two review commands, each reviewing its +own files and declaring the rest out of scope. The closeout offer that +names them one by one leaves the developer to work out which domains the +changed files touch, run each command, and reconcile the replies. + +The knowledge needed to do that automatically is already published: each +`*-code-review` skill declares its domain's file set in a `## Run scope` +section (`python-code-review/SKILL.md:13` names `*.py` and +`pyproject.toml`; `salesforce-code-review/SKILL.md:15-27` names the Apex, +Visualforce, Flow and declarative-metadata sets), and the plan-adversary +already demonstrates the pattern of reading sibling plugins' skills to +resolve domains. + +And whatever no domain claims is reviewed by nobody: in a mixed +repository that is most of the shell scripts, infrastructure definitions +and glue. + +## Decisions + +### 1. Multi-domain dispatch — the `code-review-dispatch` skill + +A `working-process` skill. Deliberately neither of the other two shapes: + +- **not an agent** — the contract requires the *dispatcher* to run the + first-create check before dispatch ("so the agent never meets an + undecided directory"), and a nested agent has nobody to ask; the + contract classifies such a run as one that defers the decision; +- **not a command** — `/code-review` already carries a meaning in the + harness, and `working-process` ships no commands today. + +What it does, in order: + +1. Resolve the scope: named files when given, otherwise the current diff + — staged plus unstaged, or the branch against its base on a clean + tree. +2. Resolve the targets: for every available `*-code-review` skill — + plugin-provided or project-level — read its `## Run scope` section and + match the scope's files against the file set it declares. A domain + with no matching file is not a target. Files no domain claims become + one further target, the generic review of decision 2. +3. **Offer the resolved targets; never impose them.** List each target + with its file count and the paths it claims, and let the developer + choose: all of them, a subset, or **none** — declining the whole + review is a listed choice, not an inference from silence. A review the + developer did not choose is not dispatched. Invoked from the closeout + step, this single offer serves as both the consent to review and the + selection of targets; invoked directly, it is the skill's first + interaction. A declined offer is not repeated: the developer invokes + the skill whenever they want it, and an offer that returns uninvited + is nagging, which no step of this flow does. +4. Run the `docs/code-review/` first-create check ONCE — gated on the + contract probe, exactly as the review commands gate it, so a + Standalone install skips the question entirely — after the selection + and before any dispatch, and pass the resulting decision to every + dispatched run. Declining every target ends the run without asking it: + a directory nothing will write to stays undecided. In tracked mode the + local pocket (`docs/code-review/.gitignore` containing `local-*`) is + written here, once, by the dispatcher — never by the fanned-out runs, + which would race on one registry file. +5. Dispatch each selected domain's reviewer in the background with the + scope narrowed to that domain. This skill adds target resolution, the + offer, the one shared first-create check, the fan-out and the relay — + nothing else. It never restates a domain's dispatch discipline and + never adds a report-shaping instruction of its own; aggregation and + counting policy, layout and severity policy stay where they are + defined. **Where that discipline lives is open question 3.** +6. Relay each run's reply as its notification arrives — report path, + findings by severity, candidate-gap offers verbatim — and close with a + rollup: one line per dispatched target, plus anything left unreviewed + (a dropped target, or a file no target could take). + +The offer's shape, illustrated on a mixed repository — the exact wording +belongs to the skill, and this block is here for shape only, never to be +matched against or copied by a verification step: + +``` +Code review — 61 changed files on feature/1234-payment-retries (vs develop). + + 1. python-standards 23 files src/payments/**, tests/** + 2. salesforce-standards 29 files force-app/main/default/{classes,objects}/** + 3. generic (no plugin) 7 files infra/main.tf, scripts/deploy.sh, + .github/workflows/release.yml, +4 more + skipped 2 files uv.lock, docs/architecture.png + +Each target runs as its own background review and writes its own report +under docs/code-review/. + +Which targets — all three, a subset, or none? Nothing runs unless you pick it. +``` + +Paths accompany the counts because a mis-resolved domain is visible there +and nowhere else before dispatch; the generic target names its files +individually (up to a few, then a count) because it is the one target +whose composition nobody knows in advance. `skipped` is a line, not a +target: non-text and generated files cannot be chosen, but hiding them +would read as an oversight. Declining every target reports one line — no +review dispatched, `docs/code-review/` untouched. + +**Report shape: N runs, N reports**, one per target. Each report's +`standards:` names that single plugin; the inline-list form stays +reserved for one run that loaded several standards plugins, which this is +not. `rerun-of` resolves per target, among reports of the same scope, +because a runid is unique only within that pairing — which makes the +scope slug's derivation load-bearing (open question 4). + +This is not the shape the backlog entry assumed (one mixed report). +Independent runs are what makes the fan-out parallel and keeps each +reviewer's context to its own domain, and the contract already grants +each run exactly one report. Aggregation belongs in the reply, where the +candidate-gap offers already live — the same division the contract draws +for those offers. + +The `## Run scope` section becomes load-bearing, so the contract names +it: one sentence in `review-reports` stating that a `*-code-review` skill +declares its domain's file set in a `## Run scope` section, which a +dispatcher reads to resolve targets. Both shipped skills already comply, +so the sentence records an existing convention rather than requesting a +change to either plugin. + +### 2. Generic review for files no domain claims — the `code-reviewer` agent + +**A `working-process` agent, and deliberately no skill.** The domain +plugins split procedure (a `*-code-review` skill) from run-owner (a +`*-code-reviewer` agent) because the procedure is where their standards +live. This reviewer has no standards plugin to load, so its procedure +sits inline in the agent file. A WP skill named `*-code-review` would be +worse than redundant: decision 1's resolution would match it, and its +`## Run scope` — everything — would swallow every domain's files. + +What it reviews: language-agnostic defects — broken correctness, data +loss, security and secret handling, unhandled errors, dead or duplicated +logic, absent tests for new behavior. Non-text and generated files are +skipped and named as skipped rather than guessed at. + +What it grades by: the authoring rubric, which the contract already +carries verbatim — **plus the reviewed project's own instructions** +(`CLAUDE.md`, project-level rules), read before grading. That addition +comes from the architect session: in a repository whose conventions are +written down but carried by no standards plugin — this marketplace being +the example, where a review would otherwise ignore `plugin-authoring`, +`plugin-versioning`, `standards-rule-tags` and `repo-hygiene` — a +reviewer that knows only the rubric produces findings that compete with +the project's rules instead of applying them. How such a finding cites +its source is part of open question 1. + +Everything else is unchanged: `mode: agent`, one run one report, the same +layout, the same severity subsections. + +**Recorded tension.** This is the first `working-process` component that +judges code rather than process. The boundary claim is that WP owns the +domain-agnostic fallback — the same role its report contract already +plays for a Standalone install — and that the rubric it grades by is +contract-owned, not invented by the agent. The counter-claim worth +hearing is that a review citing no standard has no authority to grade at +all. Decision 1 does not depend on decision 2: if the counter-claim +wins, unclaimed files are reported as unreviewed and nothing else +changes. + +## Open questions + +Nothing below is decided. The first three are the architect session's; +they are prerequisites, not polish. + +1. **The `Candidate gap` term must change, or the citation must.** The + glossary (`docs/domain/glossary.md:149-160`) defines a Candidate gap + as a finding cited `rule: none`, and makes "surfaced in the run's + reply as a candidate for a new rule, with offers to park it in Project + memory or report it upstream" part of the definition. It also + enumerates exactly two citation forms — against the loaded domain + skill, or against the standards plugin. A generic run's + `(standard: none, rule: none)` is a third form, and suppressing its + offers contradicts the term. Note that only the **upstream** offer + loses its addressee: the Project-memory park offer does not, and a + missing standard for a whole technology is exactly what a store entry + is for. Route through a grilling-session, which records glossary + updates as they land. +2. **The `Contract probe` term gains a third runner.** The term names "a + domain review skill — or a dispatching review command, pre-dispatch"; + `code-review-dispatch` is neither. #10 already had to extend this + term once for the same reason. Same grilling-session. +3. **Who owns the dispatch discipline?** Decision 1 step 5 currently + defers to each domain's review *command* — a user-facing surface, not + an interface, and the surface #10 rewrote from scratch. The + alternative is to move the discipline (contract probe, pre-dispatch + first-create check, the ban on report-shaping instructions, + background dispatch) into the report contract, which already owns + neighbouring sentences, and have both the commands and this skill + reference it. That is more churn now and one less cross-plugin + dependency on an unversioned surface forever. +4. **Scope slug derivation for fan-out targets.** `rerun-of` resolves + among reports of the same scope; the dispatcher narrows scope per + target, so each target's slug must be stable across runs and distinct + between targets, or rerun disposition silently degrades and same-day + filenames collide more often. +5. **Two entry points to one review.** `/python-review` and this skill + both start a Python review. State the division (commands as the + single-domain, explicitly-scoped entry; the skill as resolve-and-fan- + out) or make the commands delegate. +6. **Project-level `*-code-review` skills as first-class targets.** The + companion spec already establishes the project-level path for + `*-plan-review`. The symmetric move is free here, and it shrinks + decision 2's target to genuinely unclaimed files. Worth stating + explicitly rather than leaving to the reader of "every available + skill". + +## Surfaces and changes + +| Surface | Change | +| --- | --- | +| `skills/code-review-dispatch/SKILL.md` | new file — target resolution, the offer, one shared first-create check, background fan-out, relay | +| `agents/code-reviewer.md` | new file — generic review, procedure inline | +| `rules/review-reports.md` | the `## Run scope` declaration a dispatcher reads; citation form and offer stance for a run with no standards plugin (pending question 1); possibly the dispatch discipline (question 3) | +| `rules/workflow.md` | step 7's code-review offer dispatches this skill when available | +| `plugins/working-process/.claude-plugin/plugin.json` | `description` enumerates components — add both | +| `.claude-plugin/marketplace.json` | catalog entry description — same enumeration | +| `README.md` | root table row — same enumeration | +| `plugins/working-process/README.md` | bullets for the new skill and agent | +| `docs/domain/glossary.md` | `Candidate gap` and `Contract probe` (via grilling-session) | + +The three component enumerations are listed because the marketplace-sync +rule requires them to agree, with the manifest canonical — and because +the companion spec's own author missed exactly this consumer list until +an `rg` sweep surfaced it. + +## Invariants + +- Nothing is dispatched that the developer did not choose; declining + everything is a listed choice; a declined offer is never repeated + uninvited; a dropped target is reported as unreviewed rather than + silently omitted. +- A dispatcher never shapes a report: it passes scope, the directory-mode + decision and a prior runid, and no instruction about counting, layout + or severity. +- No run cites a standard it did not read, and no run invents a rule id. +- One run, one report, written by the run's owner — the contract's + existing rule, unmodified. + +## Out of scope + +- The **finding router** (review → local fix / plan / spec seed). +- Aggregating several targets into one report: a mixed single-run report + stays contract-legal for a run that genuinely loads several standards + plugins, and nothing here forecloses it. +- Standards content for the technologies the generic review covers. + Promoting a recurring generic finding into a rule means a standards + plugin for that technology — its own work, its own ticket. + +## Versioning and validation + +No version bump on the topic branch. Both components are new, so the +accumulated change is **minor** for `working-process`, sized in the +release PR. + +This spec is the part of the original design that behavior can falsify, +so it carries the dogfooding gate, designed against the vacuity failure +of #10: a real mixed scope — Python files, Salesforce metadata and at +least one file no plugin claims, changed in one diff — dispatched through +the skill itself, not a hand-rolled dispatch. Pass criteria: the offer +lists three targets with their counts; a run where one target is dropped +dispatches only the rest and names the dropped one as unreviewed; the +accepted run writes three reports whose `standards:` name one plugin, the +other plugin, and `none`; each scope is narrowed to its target; the +first-create question is asked at most once; the local pocket is written +once; the generic report's citations match whatever question 1 settles. A +single-domain scope does not satisfy this gate. + +The round runs against the *installed* payload, which the plugin cache +keys by version, so it needs a `-dev.` string. From 75b646b7a0dc5362a9e24f73ed245ca52a54329c Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 15:34:38 +0200 Subject: [PATCH 003/120] docs: spec the trigger-framework hub for salesforce-standards --- ...08-salesforce-trigger-frameworks-design.md | 410 ++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md new file mode 100644 index 0000000..381e9fd --- /dev/null +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -0,0 +1,410 @@ +--- +date: 2026-09-08 +status: draft +branch: feature/trigger-frameworks +base: develop +--- + +# salesforce-standards — trigger frameworks across projects + +## Overview + +`salesforce-standards` teaches one way to write an Apex trigger. Projects +use several, and the choice is mutually exclusive: a project on a +base-class framework cannot follow guidance written for a metadata-driven +one. This spec adds a `salesforce-triggers` skill that owns what holds +whatever framework a project uses, resolves which framework governs the +file at hand, and loads the matching guidance from a per-framework +document. + +The design keeps the class of frameworks open. A project may use one this +plugin never ships — a homegrown dispatcher, fflib, TDTM, a fork — and +reaches the same guidance by naming its own document or skill in the +declaration. + +## The problem + +`salesforce-apex/SKILL.md` teaches a handler of static methods, dispatched +by conditional logic in the trigger body. Three approaches are in current +use across the projects this plugin serves: + +1. **Base-class** — a `virtual TriggerHandler` class copied into the org. + The trigger body reads `new OrderTriggerHandler().run();` and the + handler overrides context methods. +2. **Metadata-driven** — execution order and bypasses live in Custom + Metadata, so ordering changes without a code deployment. +3. **Frameworkless** — today's content. + +Serving one approach and calling it the standard would leave two projects +in three following guidance that contradicts their code. + +The skill also carries a defect this work fixes. `SKILL.md` requires a +trigger body of "a single delegating call — no `if`/`for`/field logic in +the `.trigger` file", while `reference/trigger-handler.md` shows a trigger +body branching on `Trigger.isBefore` and `Trigger.isInsert`. + +## Settled decisions + +The developer settled these; the spec records them rather than reopening +them. + +- **All three approaches are served.** This is not an exercise in picking + a winner. +- **The class of frameworks is open.** A fourth is expected. Its guidance + may live in this plugin, in a document or skill inside the project, or + in another plugin. +- **Resolution reads a declaration first**, infers from code second, and + asks the developer third, presenting the case for and against each + option. +- **The declaration lives in the project**, keyed so a grep finds it. +- **Per-framework guidance ships as `reference/` documents**, not as + separate skills. +- **Resolution is per trigger file.** The search walks up from the file to + the nearest declaration. +- **The migration case is deferred.** The first release serves one + framework per path plus vendor exclusions. +- **No enforcement hook.** The protocol is advisory. + +## Parts and boundaries + +| Part | Owns | Deliberately excludes | +|---|---|---| +| `salesforce-triggers` (new skill) | framework-agnostic rules; the resolution protocol; the `framework-id` → path table; the questions a framework document answers; fingerprints; the vendor-exclusion scope; `reference/choosing-a-framework.md` | trigger or handler code; bypass API names; the name of the dispatching class | +| `reference/framework-base-class.md`, `-metadata.md`, `-none.md` | dispatch shape; where the handler reads context; bypass and recursion API with its error semantics; test-isolation idiom; gotchas; framework-specific rules; two example units | layers below the handler; bulkification; test-class structure | +| `salesforce-apex` | naming, layers, bulkification, governor limits, sharing, error handling; the four framework-independent example units | the trigger section, reduced to a layer-table row pointing at the hub | +| `salesforce-apex-testing` | test structure, factory, assertions, mocks | how to disable a handler in a test | +| `salesforce-code-review`, `salesforce-code-reviewer` | a resolution step before grading `.trigger` and `*TriggerHandler*.cls` | asking the developer — a background agent cannot | +| `/salesforce-review` | resolution before dispatch, passed in the prompt | — | +| `rules/salesforce-toolchain.md` | a routing line for the hub; the documented declaration key | the declaration itself, which belongs to the project | + +The hub must reach a session on its own `description:`, because +`salesforce-standards` declares no dependencies and installs without +`working-process`, whose `sync-rules` skill is the only carrier for the +rules payload. The toolchain rule strengthens routing; it never gates it. +For the same reason `CLAUDE.md` is the declaration's primary home: it +needs no plugin. + +### Descriptions stay disjoint + +`salesforce-apex` currently advertises "lightweight layering (one trigger +handler per object, service, selector, domain)", which would compete with +the hub for a question about triggers. Its description drops the trigger +clause. The hub's description reads: + +> Use when writing or reviewing Apex triggers — one trigger per object, +> what belongs in the trigger body, resolving which trigger framework the +> project uses, and loading the matching framework guidance. Apex classes, +> layering and bulkification belong to salesforce-apex; Apex tests to +> salesforce-apex-testing. + +This honours the constraint the plugin set itself in +`2026-07-20-salesforce-standards-design.md`: "Descriptions written +disjointly (no two skills compete for the same trigger)." + +## Hub rules + +Rule ids carry the `trigger-` prefix; framework documents use +`trigger-base-class-`, `trigger-metadata-` and `trigger-none-`. A finding +citation names the skill, so ids never collide across framework documents. + +**One trigger per object.** An object has one `.trigger` file. Several +handler calls inside that file are correct, in the order they must run; +the violation is a second trigger file, never a second line. Scope: the +directories listed in `packageDirectories`, excluding triggers on objects +a vendor package owns. + +(id: `trigger-one-per-object`; severity: critical; kind: defect; source: +Salesforce Well-Architected, record-triggered decision guide; Apex +Developer Guide p. 282) + +Sub-rules: + +- the two triggers declare disjoint contexts (id: + `trigger-one-per-object.disjoint-contexts`; severity: important) + +The group default grades the dangerous case. Two triggers whose declared +contexts overlap fire in an order the platform leaves undefined, so the +same code yields different results between runs — irregular data +corruption, which the rubric grades critical. Disjoint contexts carry no +ordering risk today, yet they split an object's automation across files +and sit one context declaration away from the critical case. + +**The trigger body delegates.** The body contains delegating calls and +nothing else — no conditional, loop or field logic. + +(id: `trigger-body-delegates`; severity: important; source: Apex Developer +Guide, "Document Your Apex Code") + +**Context stops at the handler.** Nothing below the handler reads +`Trigger.*`; context descends as parameters, which keeps the logic +testable without DML. + +(id: `trigger-context-below-handler`; severity: important; source: this +standard) + +The earlier formulation, "read `Trigger.*` only in the handler", is false +for a base-class framework, where the base class reads it, and for a +handler dispatched on `Trigger.operationType`, where the trigger reads it. + +**Naming.** A trigger is named `Trigger`. Pre-existing +non-conforming names stay; the convention binds new code. The dispatching +class is named by the framework document, not here. + +(id: `trigger-naming`; severity: minor; source: this standard) + +**The framework is declared.** The project declares its trigger framework. +The finding lands in the report's `## Project` section, and lands there +even when inference succeeded, because inference resolves a session while +a declaration resolves the project. + +(id: `trigger-framework-declared`; severity: minor; source: this standard) + +Bulkification stays with `salesforce-apex`. The hub cites +`apex-bulkification` and `apex-bulkification.loop-on-trigger-path` and +carries no severity word of its own. + +## What a framework document answers + +Four questions, and a fifth for the documents this plugin ships. A missing +answer is stated to the developer, never filled from another framework. + +1. **Dispatch** — the trigger body and the handler shape, as two files + calling the same lower-layer methods the shipped example uses. +2. **Context access** — where the handler reads `Trigger.*`. +3. **Bypass and recursion** — the API names *and* what happens when a + limit is exceeded. +4. **Test isolation** — how to call the handler without DML, and how to + disable it in a test. +5. **Fingerprint** — what identifies this framework in code. Required for + the documents shipped here, since step (b) of the protocol depends on + it. + +Question 3 earns its place: `setMaxLoopCount(1)` is a correct guard under +one base-class framework and silences automation under another. Names +alone cannot tell a reviewer which. + +Framework-specific rules are optional. A document that carries none is +graded by hub rules alone, and the report says so. The tag grammar does +not become a public contract in this release, so a document from outside +this plugin is not graded on its framework rules at all. + +## Resolution protocol + +### The declaration + +One line, one key: + +``` +trigger-framework: base-class +trigger-framework: trigger-actions +trigger-framework: frameworkless +trigger-framework: acme-dispatcher; doc-path: docs/acme-triggers.md +trigger-framework: acme-dispatcher; skill: acme-plugin:acme-triggers +``` + +Homes: the repository's `CLAUDE.md`, a directory's `CLAUDE.md`, a local +copy of `salesforce-toolchain.md`, or a project rule. A grep does not +distinguish them, so the design does not either. + +### Steps + +Resolution runs per trigger file, walking up from the file to the nearest +declaration. + +| Step | Action | +|---|---| +| (a) | grep the candidate files for `trigger-framework:` — an explicit read, never a wait for context | +| (b) | fingerprint the project's own code, skipping vendor directories | +| (c) | ask the developer, with `reference/choosing-a-framework.md` | + +Step (a) reads rather than waits because a `CLAUDE.md` in a subdirectory +loads lazily: Claude Code "discovers `CLAUDE.md` [...] in subdirectories +under your current working directory. Instead of loading them at launch, +they are included when Claude reads files in those subdirectories." A +session about to write its first trigger has read nothing in that +directory, which is exactly when resolution runs. + +### Fingerprints + +| Framework | Pattern | +|---|---| +| base-class | `extends TriggerHandler` with `new X().run()` | +| metadata-driven | `new MetadataTriggerHandler().run()` with `Trigger_Action__mdt` and `sObject_Trigger_Setting__mdt` records | +| fflib | `fflib_SObjectDomain.triggerHandler(` in the trigger body | +| TDTM | `TDTM_Config_API.run(` | +| dispatcher | `TriggerDispatcher.Run(`, or a handler implementing an interface with `IsDisabled()` | +| frameworkless | no pattern is not a fingerprint — go to step (c) | + +The metadata-driven fingerprint keys on Custom Metadata records and the +trigger body, never on the framework's classes: the records sit in the +repository whether the framework arrives as an unlocked package or as +source, while the classes appear only in the second case. + +The base-class document must also separate two frameworks that look +identical in the trigger body and differ in error semantics. The +discriminator is the method name inside `TriggerHandler.cls`: +`incrementCheckLoopCount` silences an exceeded loop count, +`addToLoopCount` throws. + +### Exclusions + +Two layers, not one: directories outside `packageDirectories`, and +triggers on objects a vendor package owns even when its source sits inside +a package directory. A logging package distributed unlocked and without a +namespace puts its classes in the org's own namespace beside the +project's. Reviews already exclude this material — +`salesforce-code-review/SKILL.md` states "never review third-party +libraries" — so the exclusion needs a scope, not a rule. + +### The resolution record + +One line, before the first trigger edit, carrying three fields — the +framework, the source, and the document loaded: + +``` +Trigger framework: base-class — declared in force-app/billing/triggers/CLAUDE.md — loading framework-base-class.md +``` + +The source reads `declared `, `inferred ` or `asked`. Any +source other than `declared` ends with an offer to write the declaration: +a line in a conversation dies at the next compaction, a declaration +survives it. The hub never remembers a resolution; after compaction it +reads the files again. + +### Failure modes + +| Situation | Behaviour | +|---|---| +| No declaration, fingerprint matches | infer, cite the file and pattern, offer to write the declaration | +| No declaration, no fingerprint | go to (c), saying no pattern matched in N triggers and asking whether the project is frameworkless or on a framework this plugin does not know | +| No triggers at all | go to (c) | +| Two declarations disagree | the nearer one wins, the disagreement is always stated, and the session offers to reconcile them | +| The declared document is missing | say so and grade by hub rules; never substitute another framework's document, since guidance for the wrong framework writes code that does not compile | +| A homegrown framework with no document | offer to write one, describing their code rather than a pattern from the internet — the "dispatcher, handler, helper" division has no primary source | + +## Review surface + +`salesforce-code-review` gains a step: for `.trigger` and +`*TriggerHandler*.cls`, load the hub, resolve through (a) and (b), then +load the framework document by path or name. + +Step (c) does not exist for a background agent, so `/salesforce-review` +resolves before dispatch and passes the record in the prompt, as it +already passes the directory mode. The agent verifies the record against +the declaration, which costs nothing. + +When resolution fails, the run grades hub rules, skips framework rules +rather than guessing them, notes `trigger framework: unresolved — +framework-specific rules not graded` among the Summary's out-of-scope +notes, and reports `trigger-framework-declared` in the `## Project` +section. Both slots already exist in the review-report contract. + +## Changes to shipped content + +Three rules change owner, and the spec names each because a moved id +breaks a citation: + +1. "One trigger per object" leaves `apex-layering` for + `trigger-one-per-object`. +2. `TriggerHandler` leaves `apex-naming`. The hub names only + `Trigger`. +3. `apex-bulkification` and its sub-rule stay put, cited by the hub. + +The frameworkless document adopts a dispatch shape that keeps +`trigger-body-delegates` true: + +```apex +trigger OrderTrigger on Order (before insert, before update, after update) { + OrderTriggerHandler.handle(Trigger.operationType, Trigger.new, Trigger.oldMap); +} +``` + +The handler switches on `System.TriggerOperation`. This fixes the defect +named above, makes the hub rule true for all three frameworks, and gives +the frameworkless handler what the frameworks already have: a dispatch +entry point callable from a test without DML. + +The example splits rather than triples. `OrderDomain`, `OrderSelector`, +`OrderService` and `OrderProcessingException` do not depend on the +framework and stay in `salesforce-apex/reference/`. Each framework +document carries `OrderTrigger` and `OrderTriggerHandler` and calls the +same lower-layer methods, so the three examples are demonstrably one +scenario. + +## Evidence and its limits + +Framework behaviour was read from source, not from documentation. Two +findings correct the documentation: + +- The base-class fork documents `TriggerHandler.setGlobalBypass()`, which + its source does not define. The method is `bypassAll()`. +- Its silent stop is deliberate. The `throw` sits commented out beside the + line `// Do not throw an exception if we exceed the loop count - just + stop executing`. + +Two citations carry weaker provenance, recorded so a later reader can +judge them: + +- Apex Developer Guide quotations come from the official PDF, fetched with + TLS verification disabled after the HTML pages returned 403. +- Well-Architected quotations come from two consistent extractions of the + same page rather than a byte-exact fetch. + +One provenance note matters for the rule that carries the most weight. The +phrase "one trigger per object" appears nowhere in the 825-page Apex +Developer Guide. The Guide supplies the reasoning — execution order is +undefined for two triggers on one object for the same event — while the +prescription lives in Well-Architected. The formulation everyone quotes +comes from a retired `developer.salesforce.com` wiki page written in 2014 +by the author of the original base-class framework. The rule is sound; its +popular pedigree is authored content, not platform norm. + +## Deviations from the consultations + +Both personas were consulted on one briefing and disagreed about whether +per-framework guidance should ship as skills. The architect held that a +skill buys routing this design does not want and identity cost it pays +forever; the system designer held that skills with +`disable-model-invocation: true` cost no listing and unify loading. +Verification settled it: such a skill's description does stay out of +context, but the hub reads shipped and project-local documents by path in +either shape, so uniformity was not the deciding gain. Slash-command +invocation was, and nobody wanted it. + +Two further deviations: + +- **Resolution is per file, not per package.** The designer proposed the + sfdx package as the unit. A package holding several application folders + would then take one declaration for two answers. Walking up from the + file generalises the designer's own nearest-wins rule, and the package + keeps the job it is needed for: bounding vendor exclusions. +- **The fflib argument is dropped.** The architect argued that fflib names + its handler `Domain`, colliding with this standard's Domain + layer. fflib split domain from trigger handler in April 2021 and its + sample code now separates them. The decision stands on the + metadata-driven case, where the handler is the framework's own class. + +## Out of scope + +- **Migration between frameworks.** Deferred, with it the rule that would + grade a new file against a declared target. +- **The tag grammar as a public contract.** A document from another plugin + is not graded on framework rules, so the grammar stays internal and + `plugin-versioning` gains no new breaking surface. +- **Enforcement.** No hook ships. `PreToolUse` could deny a write to a + `.trigger` file with no declaration in scope, and `InstructionsLoaded` + could not help at all, having no decision control. The consequence is + recorded rather than hidden: a session that writes a trigger without + loading the hub meets no obstacle, and review catches the miss + afterwards through `trigger-framework-declared`. + +## Open questions + +- **Ticket.** None. The developer chose to open no issue and to name the + branch `feature/trigger-frameworks`, without the issue number the + repository convention prescribes. A ticket may still be opened before + the pull request. +- **How many consumers run a framework other than frameworkless.** + Unknown, and no longer load-bearing: the question tested the + proportionality of shipping four skills, and this design ships one skill + with three documents. From 046b18b104ee9eaa1488498561e124ab2611ce40 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 16:11:58 +0200 Subject: [PATCH 004/120] docs: grill the trigger-framework spec --- ...08-salesforce-trigger-frameworks-design.md | 126 +++++++++++++----- 1 file changed, 91 insertions(+), 35 deletions(-) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index 381e9fd..3a767a5 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -1,6 +1,8 @@ --- +ticket: none date: 2026-09-08 status: draft +grilled: 2026-09-08 branch: feature/trigger-frameworks base: develop --- @@ -71,25 +73,28 @@ them. |---|---|---| | `salesforce-triggers` (new skill) | framework-agnostic rules; the resolution protocol; the `framework-id` → path table; the questions a framework document answers; fingerprints; the vendor-exclusion scope; `reference/choosing-a-framework.md` | trigger or handler code; bypass API names; the name of the dispatching class | | `reference/framework-base-class.md`, `-metadata.md`, `-none.md` | dispatch shape; where the handler reads context; bypass and recursion API with its error semantics; test-isolation idiom; gotchas; framework-specific rules; two example units | layers below the handler; bulkification; test-class structure | -| `salesforce-apex` | naming, layers, bulkification, governor limits, sharing, error handling; the four framework-independent example units | the trigger section, reduced to a layer-table row pointing at the hub | +| `salesforce-apex` | naming, layers, bulkification, governor limits, sharing, error handling; the four framework-independent example units | the trigger section, reduced to a layer-table row pointing at `salesforce-triggers` | | `salesforce-apex-testing` | test structure, factory, assertions, mocks | how to disable a handler in a test | | `salesforce-code-review`, `salesforce-code-reviewer` | a resolution step before grading `.trigger` and `*TriggerHandler*.cls` | asking the developer — a background agent cannot | | `/salesforce-review` | resolution before dispatch, passed in the prompt | — | -| `rules/salesforce-toolchain.md` | a routing line for the hub; the documented declaration key | the declaration itself, which belongs to the project | - -The hub must reach a session on its own `description:`, because -`salesforce-standards` declares no dependencies and installs without -`working-process`, whose `sync-rules` skill is the only carrier for the -rules payload. The toolchain rule strengthens routing; it never gates it. -For the same reason `CLAUDE.md` is the declaration's primary home: it -needs no plugin. +| `rules/salesforce-toolchain.md` | a routing line for `salesforce-triggers`; the documented declaration key | the declaration itself, which belongs to the project | + +`salesforce-triggers` must reach a session on its own `description:`, +because `salesforce-standards` declares no dependencies and supports a +Standalone install, where the Rules engine that carries its payload is +absent. The toolchain rule strengthens routing; it never gates it. For +the same reason `CLAUDE.md` is the declaration's primary home: it needs +no plugin. The review surface survives a Standalone install unchanged, +because the inline fallback in `salesforce-code-review` already defines +both slots this design writes to — a Summary carrying out-of-scope files +and a `## Project` section. ### Descriptions stay disjoint `salesforce-apex` currently advertises "lightweight layering (one trigger handler per object, service, selector, domain)", which would compete with -the hub for a question about triggers. Its description drops the trigger -clause. The hub's description reads: +`salesforce-triggers` for a question about triggers. Its description drops +the trigger clause. The new skill's description reads: > Use when writing or reviewing Apex triggers — one trigger per object, > what belongs in the trigger body, resolving which trigger framework the @@ -101,7 +106,7 @@ This honours the constraint the plugin set itself in `2026-07-20-salesforce-standards-design.md`: "Descriptions written disjointly (no two skills compete for the same trigger)." -## Hub rules +## Framework-independent rules Rule ids carry the `trigger-` prefix; framework documents use `trigger-base-class-`, `trigger-metadata-` and `trigger-none-`. A finding @@ -159,7 +164,20 @@ a declaration resolves the project. (id: `trigger-framework-declared`; severity: minor; source: this standard) -Bulkification stays with `salesforce-apex`. The hub cites +Sub-rules: + +- two declaration homes cover one path with different values (id: + `trigger-framework-declared.ambiguous`; severity: important) + +The group default grades an absent declaration, which announces itself: +resolution falls through to inference or to a question, and the reader +sees it happen. A collision hides instead. The project has declared +twice, so the letter of the rule is met while nothing can be resolved +from it, and framework rules quietly go ungraded — a review that loses +half its scope without saying why. The two are mutually exclusive: a path +has no declaration or it has competing ones. + +Bulkification stays with `salesforce-apex`. `salesforce-triggers` cites `apex-bulkification` and `apex-bulkification.loop-on-trigger-path` and carries no severity word of its own. @@ -184,7 +202,7 @@ one base-class framework and silences automation under another. Names alone cannot tell a reviewer which. Framework-specific rules are optional. A document that carries none is -graded by hub rules alone, and the report says so. The tag grammar does +graded by the framework-independent rules alone, and the report says so. The tag grammar does not become a public contract in this release, so a document from outside this plugin is not graded on its framework rules at all. @@ -203,13 +221,29 @@ trigger-framework: acme-dispatcher; skill: acme-plugin:acme-triggers ``` Homes: the repository's `CLAUDE.md`, a directory's `CLAUDE.md`, a local -copy of `salesforce-toolchain.md`, or a project rule. A grep does not -distinguish them, so the design does not either. +copy of `salesforce-toolchain.md`, or a project rule. A grep finds them +all; what separates them is scope. + +Every declaration carries one, and the two kinds are shaped differently. +A `CLAUDE.md` scopes itself by **position** — the subtree of the +directory holding it. A rule scopes itself by **glob**: its `paths:` +patterns, or the whole repository when it declares none. A rule always +sits at the repository root whatever its glob says, so position tells +nothing about how narrow it is. + +The most specific scope covering a trigger file wins, and specificity is +only ever claimed between two subtrees, where one strictly contains the +other. A glob is never ranked against a subtree or against another glob: +comparing them invents an answer the project never stated. Two +declarations of unequal shape therefore have no winner, which the next +section turns into behaviour rather than a tie-break. ### Steps -Resolution runs per trigger file, walking up from the file to the nearest -declaration. +Resolution runs per trigger file. Step (a) collects every declaration +whose scope covers that file — walking up the directory tree for +`CLAUDE.md` homes and reading `paths:` for rules — and then applies the +specificity rule above. | Step | Action | |---|---| @@ -217,6 +251,16 @@ declaration. | (b) | fingerprint the project's own code, skipping vendor directories | | (c) | ask the developer, with `reference/choosing-a-framework.md` | +**Who repairs what.** Step (c) and the collision between declarations of +unequal shape both need a person, so both belong to an interactive +session. A background agent asks nothing: it enforces the rules it can +resolve and reports whatever it cannot. A session, in turn, does not +settle such an ambiguity in conversation — an answer there dies at the +next compaction while the ambiguity returns every session. It settles it +in the project, by rescoping or removing one of the competing homes until +the declaration surface states one framework per path and says so +unambiguously. The fix is an edit, not an answer. + Step (a) reads rather than waits because a `CLAUDE.md` in a subdirectory loads lazily: Claude Code "discovers `CLAUDE.md` [...] in subdirectories under your current working directory. Instead of loading them at launch, @@ -268,7 +312,7 @@ Trigger framework: base-class — declared in force-app/billing/triggers/CLAUDE. The source reads `declared `, `inferred ` or `asked`. Any source other than `declared` ends with an offer to write the declaration: a line in a conversation dies at the next compaction, a declaration -survives it. The hub never remembers a resolution; after compaction it +survives it. `salesforce-triggers` never remembers a resolution; after compaction it reads the files again. ### Failure modes @@ -278,26 +322,32 @@ reads the files again. | No declaration, fingerprint matches | infer, cite the file and pattern, offer to write the declaration | | No declaration, no fingerprint | go to (c), saying no pattern matched in N triggers and asking whether the project is frameworkless or on a framework this plugin does not know | | No triggers at all | go to (c) | -| Two declarations disagree | the nearer one wins, the disagreement is always stated, and the session offers to reconcile them | -| The declared document is missing | say so and grade by hub rules; never substitute another framework's document, since guidance for the wrong framework writes code that does not compile | +| Two subtree-scoped declarations disagree | the inner one wins, and the disagreement is always stated — never resolved silently | +| Declarations of unequal shape both cover the file — a rule and a `CLAUDE.md` | no winner: name every file that declares, then work out with the developer which home survives and write that change. Picking one silently would answer a question only the project can, and answering it in conversation would leave the collision to recur next session | +| The declared document is missing | say so and grade by the framework-independent rules; never substitute another framework's document, since guidance for the wrong framework writes code that does not compile | | A homegrown framework with no document | offer to write one, describing their code rather than a pattern from the internet — the "dispatcher, handler, helper" division has no primary source | ## Review surface `salesforce-code-review` gains a step: for `.trigger` and -`*TriggerHandler*.cls`, load the hub, resolve through (a) and (b), then +`*TriggerHandler*.cls`, load `salesforce-triggers`, resolve through (a) and (b), then load the framework document by path or name. -Step (c) does not exist for a background agent, so `/salesforce-review` -resolves before dispatch and passes the record in the prompt, as it -already passes the directory mode. The agent verifies the record against -the declaration, which costs nothing. +A background agent enforces standards and asks nothing. +`/salesforce-review` therefore resolves before dispatch and passes the +record in the prompt, as it already passes the directory mode; the agent +verifies the record against the declaration, which costs nothing. +Dispatched any other way, it grades what the framework-independent rules +cover and reports the rest as unresolved — choosing a framework for the +project is as far outside its job as asking for one. -When resolution fails, the run grades hub rules, skips framework rules +When resolution fails, the run grades the framework-independent rules, skips framework rules rather than guessing them, notes `trigger framework: unresolved — framework-specific rules not graded` among the Summary's out-of-scope -notes, and reports `trigger-framework-declared` in the `## Project` -section. Both slots already exist in the review-report contract. +notes, and reports the declaration rule in the `## Project` section — +`trigger-framework-declared` where nothing declares the path, +`trigger-framework-declared.ambiguous` where competing homes do. Both +slots already exist in the review-report contract. ## Changes to shipped content @@ -306,9 +356,10 @@ breaks a citation: 1. "One trigger per object" leaves `apex-layering` for `trigger-one-per-object`. -2. `TriggerHandler` leaves `apex-naming`. The hub names only - `Trigger`. -3. `apex-bulkification` and its sub-rule stay put, cited by the hub. +2. `TriggerHandler` leaves `apex-naming`. `salesforce-triggers` + names only `Trigger`. +3. `apex-bulkification` and its sub-rule stay put, cited by + `salesforce-triggers`. The frameworkless document adopts a dispatch shape that keeps `trigger-body-delegates` true: @@ -320,7 +371,7 @@ trigger OrderTrigger on Order (before insert, before update, after update) { ``` The handler switches on `System.TriggerOperation`. This fixes the defect -named above, makes the hub rule true for all three frameworks, and gives +named above, makes the rule true for all three frameworks, and gives the frameworkless handler what the frameworks already have: a dispatch entry point callable from a test without DML. @@ -367,7 +418,7 @@ skill buys routing this design does not want and identity cost it pays forever; the system designer held that skills with `disable-model-invocation: true` cost no listing and unify loading. Verification settled it: such a skill's description does stay out of -context, but the hub reads shipped and project-local documents by path in +context, but `salesforce-triggers` reads shipped and project-local documents by path in either shape, so uniformity was not the deciding gain. Slash-command invocation was, and nobody wanted it. @@ -391,11 +442,16 @@ Two further deviations: - **The tag grammar as a public contract.** A document from another plugin is not graded on framework rules, so the grammar stays internal and `plugin-versioning` gains no new breaking surface. +- **Generality across the Standards family.** `python-standards` commits + to one toolchain outright, so resolving between frameworks has a single + instance today and stays local to this plugin. Should a second + Standards plugin ever need it, the pattern is a candidate for the + family rule ticket #14 defines, not something this spec anticipates. - **Enforcement.** No hook ships. `PreToolUse` could deny a write to a `.trigger` file with no declaration in scope, and `InstructionsLoaded` could not help at all, having no decision control. The consequence is recorded rather than hidden: a session that writes a trigger without - loading the hub meets no obstacle, and review catches the miss + loading `salesforce-triggers` meets no obstacle, and review catches the miss afterwards through `trigger-framework-declared`. ## Open questions From e744aef1687d18493ae4d6fd3178c45a2180c4b5 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 16:32:58 +0200 Subject: [PATCH 005/120] docs: fix architect round-one findings on the trigger-framework spec --- ...08-salesforce-trigger-frameworks-design.md | 123 ++++++++++++++---- 1 file changed, 98 insertions(+), 25 deletions(-) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index 3a767a5..c8023fc 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -3,6 +3,7 @@ ticket: none date: 2026-09-08 status: draft grilled: 2026-09-08 +architect: blocking branch: feature/trigger-frameworks base: develop --- @@ -72,7 +73,7 @@ them. | Part | Owns | Deliberately excludes | |---|---|---| | `salesforce-triggers` (new skill) | framework-agnostic rules; the resolution protocol; the `framework-id` → path table; the questions a framework document answers; fingerprints; the vendor-exclusion scope; `reference/choosing-a-framework.md` | trigger or handler code; bypass API names; the name of the dispatching class | -| `reference/framework-base-class.md`, `-metadata.md`, `-none.md` | dispatch shape; where the handler reads context; bypass and recursion API with its error semantics; test-isolation idiom; gotchas; framework-specific rules; two example units | layers below the handler; bulkification; test-class structure | +| `reference/framework-base-class.md`, `-metadata-driven.md`, `-frameworkless.md` | dispatch shape; where the handler reads context; bypass and recursion API with its error semantics; test-isolation idiom; gotchas; framework-specific rules; two example units | layers below the handler; bulkification; test-class structure | | `salesforce-apex` | naming, layers, bulkification, governor limits, sharing, error handling; the four framework-independent example units | the trigger section, reduced to a layer-table row pointing at `salesforce-triggers` | | `salesforce-apex-testing` | test structure, factory, assertions, mocks | how to disable a handler in a test | | `salesforce-code-review`, `salesforce-code-reviewer` | a resolution step before grading `.trigger` and `*TriggerHandler*.cls` | asking the developer — a background agent cannot | @@ -109,7 +110,8 @@ disjointly (no two skills compete for the same trigger)." ## Framework-independent rules Rule ids carry the `trigger-` prefix; framework documents use -`trigger-base-class-`, `trigger-metadata-` and `trigger-none-`. A finding +`trigger-base-class-`, `trigger-metadata-driven-` and +`trigger-frameworkless-`. A finding citation names the skill, so ids never collide across framework documents. **One trigger per object.** An object has one `.trigger` file. Several @@ -166,16 +168,24 @@ a declaration resolves the project. Sub-rules: -- two declaration homes cover one path with different values (id: +- two declarations of unequal shape cover one path, so neither wins (id: `trigger-framework-declared.ambiguous`; severity: important) The group default grades an absent declaration, which announces itself: resolution falls through to inference or to a question, and the reader -sees it happen. A collision hides instead. The project has declared -twice, so the letter of the rule is met while nothing can be resolved -from it, and framework rules quietly go ungraded — a review that loses -half its scope without saying why. The two are mutually exclusive: a path -has no declaration or it has competing ones. +sees it happen. The sub-rule grades the one case the protocol cannot +resolve — a glob-scoped rule and a subtree-scoped `CLAUDE.md` both +covering the same path. That case hides: the project has declared twice, +so the letter of the rule is met while nothing can be resolved from it, +and framework rules quietly go ungraded. + +A root default with a nearer override is **not** this finding. Two +subtree-scoped declarations are comparable, the inner one wins, and +declaring a repository-wide default beside a per-package exception is a +configuration the protocol resolves — not a violation it grades. Stating +that disagreement belongs to the resolution record as a duty, never to a +rule as a grade. The two dispositions are therefore mutually exclusive: a +path has no declaration, or it has two the protocol cannot rank. Bulkification stays with `salesforce-apex`. `salesforce-triggers` cites `apex-bulkification` and `apex-bulkification.loop-on-trigger-path` and @@ -187,7 +197,8 @@ Four questions, and a fifth for the documents this plugin ships. A missing answer is stated to the developer, never filled from another framework. 1. **Dispatch** — the trigger body and the handler shape, as two files - calling the same lower-layer methods the shipped example uses. + calling the same lower-layer methods the shipped example uses, plus the + glob matching this framework's handler classes. 2. **Context access** — where the handler reads `Trigger.*`. 3. **Bypass and recursion** — the API names *and* what happens when a limit is exceeded. @@ -201,6 +212,14 @@ Question 3 earns its place: `setMaxLoopCount(1)` is a correct guard under one base-class framework and silences automation under another. Names alone cannot tell a reviewer which. +Question 1 carries a glob for the same reason `salesforce-triggers` names +no dispatching class: handler naming belongs to the framework. A +metadata-driven project's action classes are named `TA__` +and implement `TriggerAction.*`, so they match no `*TriggerHandler*` +pattern — a review scoped by that pattern would grade the framework's own +`MetadataTriggerHandler`, which the vendor exclusion skips, and miss every +class the project wrote. + Framework-specific rules are optional. A document that carries none is graded by the framework-independent rules alone, and the report says so. The tag grammar does not become a public contract in this release, so a document from outside @@ -208,21 +227,53 @@ this plugin is not graded on its framework rules at all. ## Resolution protocol +One framework id names each framework everywhere it appears — declaration +value, fingerprint row, document filename, rule prefix. The ids name an +**approach**, never a product, which is why two base-class frameworks share +one document and one prefix. `salesforce-triggers` owns the table: + +| `framework-id` | Document | Rule prefix | +|---|---|---| +| `base-class` | `reference/framework-base-class.md` | `trigger-base-class-` | +| `metadata-driven` | `reference/framework-metadata-driven.md` | `trigger-metadata-driven-` | +| `frameworkless` | `reference/framework-frameworkless.md` | `trigger-frameworkless-` | + +An id absent from this table is a framework this plugin does not ship. Its +declaration carries a `doc-path:` or a `skill:` locator, and the table +never grows to accommodate one — that is the open class working as +intended. + ### The declaration One line, one key: ``` trigger-framework: base-class -trigger-framework: trigger-actions +trigger-framework: metadata-driven trigger-framework: frameworkless trigger-framework: acme-dispatcher; doc-path: docs/acme-triggers.md trigger-framework: acme-dispatcher; skill: acme-plugin:acme-triggers +vendor-paths: force-app/nebula/**, force-app/vendor/** ``` -Homes: the repository's `CLAUDE.md`, a directory's `CLAUDE.md`, a local -copy of `salesforce-toolchain.md`, or a project rule. A grep finds them -all; what separates them is scope. +`vendor-paths:` is the second key of the surface, and the only other thing +the protocol reads from the project. It lists what belongs to a package +the project did not write. The scope is **declared, never inferred**: a +package installed unlocked and without a namespace, vendored as source, +sits in a package directory carrying no platform signal that marks it +foreign, so no amount of reading `sfdx-project.json` recovers it. + +Homes, and the exact set a grep reads: `CLAUDE.md`, `.claude/CLAUDE.md` +and `CLAUDE.local.md` at the repository root or in any directory above the +trigger file, plus any rule the project itself owns under +`.claude/rules/`. Two things are deliberately not homes. A declaration +belongs to the project, so a personal rule under `~/.claude/rules/` never +carries one — it would follow the developer between projects that disagree. +And the installed `salesforce-toolchain.md` copy is not a home either: the +Rules engine owns that file, so its next sync either overwrites a +hand-added line or freezes the rule at the version that carried it, and +its `paths:` already covers `**/*.trigger` repo-wide — the same scope as +the root `CLAUDE.md`, which would manufacture a collision by construction. Every declaration carries one, and the two kinds are shaped differently. A `CLAUDE.md` scopes itself by **position** — the subtree of the @@ -292,13 +343,17 @@ discriminator is the method name inside `TriggerHandler.cls`: ### Exclusions -Two layers, not one: directories outside `packageDirectories`, and -triggers on objects a vendor package owns even when its source sits inside -a package directory. A logging package distributed unlocked and without a -namespace puts its classes in the org's own namespace beside the -project's. Reviews already exclude this material — +Two layers, not one: directories outside `packageDirectories`, and the +paths `vendor-paths:` declares. The first is read from +`sfdx-project.json`; the second must be declared, because a package +installed unlocked and without a namespace and vendored as source sits in +a package directory with nothing marking it foreign. A logging package +distributed that way puts its classes in the org's own namespace beside +the project's, and its own triggers on its own objects are not the +project's to review. Reviews already exclude this material — `salesforce-code-review/SKILL.md` states "never review third-party -libraries" — so the exclusion needs a scope, not a rule. +libraries" — so the exclusion needs a scope, not a rule, and +`vendor-paths:` is where that scope is stated. ### The resolution record @@ -322,6 +377,7 @@ reads the files again. | No declaration, fingerprint matches | infer, cite the file and pattern, offer to write the declaration | | No declaration, no fingerprint | go to (c), saying no pattern matched in N triggers and asking whether the project is frameworkless or on a framework this plugin does not know | | No triggers at all | go to (c) | +| A fingerprint matches a framework this plugin ships nothing for — fflib, TDTM, a dispatcher | the id is sound and only the document is missing, so take the homegrown row below: offer to write a project document, never substitute another framework's | | Two subtree-scoped declarations disagree | the inner one wins, and the disagreement is always stated — never resolved silently | | Declarations of unequal shape both cover the file — a rule and a `CLAUDE.md` | no winner: name every file that declares, then work out with the developer which home survives and write that change. Picking one silently would answer a question only the project can, and answering it in conversation would leave the collision to recur next session | | The declared document is missing | say so and grade by the framework-independent rules; never substitute another framework's document, since guidance for the wrong framework writes code that does not compile | @@ -329,9 +385,12 @@ reads the files again. ## Review surface -`salesforce-code-review` gains a step: for `.trigger` and -`*TriggerHandler*.cls`, load `salesforce-triggers`, resolve through (a) and (b), then -load the framework document by path or name. +`salesforce-code-review` gains a step: for `.trigger` files — the one +artefact every framework has — load `salesforce-triggers`, resolve through +(a) and (b), then load the framework document by path or name and take the +handler glob from its Dispatch answer to bring the project's handler +classes into scope. The file scope is resolved, never hardcoded, because +only the framework knows what its handlers are called. A background agent enforces standards and asks nothing. `/salesforce-review` therefore resolves before dispatch and passes the @@ -346,8 +405,8 @@ rather than guessing them, notes `trigger framework: unresolved — framework-specific rules not graded` among the Summary's out-of-scope notes, and reports the declaration rule in the `## Project` section — `trigger-framework-declared` where nothing declares the path, -`trigger-framework-declared.ambiguous` where competing homes do. Both -slots already exist in the review-report contract. +`trigger-framework-declared.ambiguous` where two declarations of unequal +shape do. Both slots already exist in the review-report contract. ## Changes to shipped content @@ -431,7 +490,7 @@ Two further deviations: keeps the job it is needed for: bounding vendor exclusions. - **The fflib argument is dropped.** The architect argued that fflib names its handler `Domain`, colliding with this standard's Domain - layer. fflib split domain from trigger handler in April 2021 and its + layer. fflib split domain from trigger handler in March 2021 and its sample code now separates them. The decision stands on the metadata-driven case, where the handler is the framework's own class. @@ -464,3 +523,17 @@ Two further deviations: Unknown, and no longer load-bearing: the question tested the proportionality of shipping four skills, and this design ships one skill with three documents. + +## Review rounds + +### 2026-09-08 — architect, fable 5.1, blocking (round 1, full-document) + +- fixed 2026-09-08 — [Important] The review step's file scope, `*TriggerHandler*.cls`, matches no handler class in a metadata-driven project, so that project's framework rules go ungraded; license: the parts table excludes the dispatching class's name from `salesforce-triggers`, and question 1 already owns the dispatch shape; the review step now resolves on `.trigger` alone and takes the handler glob from the framework document, which question 1 was extended to carry +- fixed 2026-09-08 — [Important] The `.ambiguous` sub-rule's wording fires on the nested-`CLAUDE.md` override the protocol itself resolves, and "competing" is left undefined; license: `.claude/rules/standards-rule-tags.md:40` requires a sub-rule that could overlap to draw its boundary explicitly; the sub-rule now names the unequal-shape no-winner case, and the text states that a root default with a nearer override is a configuration the protocol resolves rather than a violation it grades +- fixed 2026-09-08 — [Important] Naming the installed `salesforce-toolchain.md` copy a declaration home contradicts the parts table and manufactures the unequal-shape collision by construction; ruling: 2026-09-08; the copy is no longer a home, and the homes paragraph now says why — the Rules engine owns that file, and its `paths:` already covers `**/*.trigger` repo-wide +- fixed 2026-09-08 — [Important] The vendor-exclusion scope carries both the critical rule and step (b), yet the spec never says how that scope is known; ruling: 2026-09-08; a second declaration key, `vendor-paths:`, carries the scope, and the exclusions section states it is declared and never inferred, since a namespace-less package vendored as source carries no platform signal +- fixed 2026-09-08 — [Minor] The framework id appears in three vocabularies, and the `framework-id` → path table the skill is said to own is never given; license: the base-class document already covers two products, which chose approach ids over product ids; one id now names each framework everywhere, `trigger-actions` became `metadata-driven`, `-none` became `-frameworkless`, and the table is given +- fixed 2026-09-08 — [Minor] A fingerprint match on fflib, TDTM or dispatcher yields an id with no shipped document, and the failure-mode table states no next step; license: that table already routes a missing document to the homegrown row; a row now sends an inferred unshipped framework there +- fixed 2026-09-08 — [Minor] "A rule always sits at the repository root" omits user-level rules, and the `CLAUDE.md` homes omit `.claude/CLAUDE.md` and `CLAUDE.local.md`; license: the parts table already says the declaration belongs to the project, which excludes a personal rule; the homes paragraph now names the grep candidate set exactly and rules out `~/.claude/rules/` +- fixed 2026-09-08 — The fflib domain/handler split is dated April 2021 where the reviewer's cited commit is 2021-03-15; license: the reviewer's citation, offered as an ungraded aside; the date now reads March 2021 +- signal 2026-09-08 — one diff-scoped round should close the document once the four Important findings land; a full-document re-read is warranted only if the first finding's repair moves the handler-shape question into the framework-document contract, and the Minor leftovers are worth a single fix wave rather than a round of their own From bbd1863e342a78e5cfe98e6f3fa5326ec6887064 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 16:49:19 +0200 Subject: [PATCH 006/120] docs: fix the propagation hit in the trigger-framework spec --- docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index c8023fc..c36d6b0 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -76,7 +76,7 @@ them. | `reference/framework-base-class.md`, `-metadata-driven.md`, `-frameworkless.md` | dispatch shape; where the handler reads context; bypass and recursion API with its error semantics; test-isolation idiom; gotchas; framework-specific rules; two example units | layers below the handler; bulkification; test-class structure | | `salesforce-apex` | naming, layers, bulkification, governor limits, sharing, error handling; the four framework-independent example units | the trigger section, reduced to a layer-table row pointing at `salesforce-triggers` | | `salesforce-apex-testing` | test structure, factory, assertions, mocks | how to disable a handler in a test | -| `salesforce-code-review`, `salesforce-code-reviewer` | a resolution step before grading `.trigger` and `*TriggerHandler*.cls` | asking the developer — a background agent cannot | +| `salesforce-code-review`, `salesforce-code-reviewer` | a resolution step before grading `.trigger`, then the handler glob the resolved framework document supplies | asking the developer — a background agent cannot | | `/salesforce-review` | resolution before dispatch, passed in the prompt | — | | `rules/salesforce-toolchain.md` | a routing line for `salesforce-triggers`; the documented declaration key | the declaration itself, which belongs to the project | @@ -536,4 +536,5 @@ Two further deviations: - fixed 2026-09-08 — [Minor] A fingerprint match on fflib, TDTM or dispatcher yields an id with no shipped document, and the failure-mode table states no next step; license: that table already routes a missing document to the homegrown row; a row now sends an inferred unshipped framework there - fixed 2026-09-08 — [Minor] "A rule always sits at the repository root" omits user-level rules, and the `CLAUDE.md` homes omit `.claude/CLAUDE.md` and `CLAUDE.local.md`; license: the parts table already says the declaration belongs to the project, which excludes a personal rule; the homes paragraph now names the grep candidate set exactly and rules out `~/.claude/rules/` - fixed 2026-09-08 — The fflib domain/handler split is dated April 2021 where the reviewer's cited commit is 2021-03-15; license: the reviewer's citation, offered as an ungraded aside; the date now reads March 2021 +- hit fixed 2026-09-08 — the parts table still scoped the review step to `.trigger` and `*TriggerHandler*.cls` after the first finding's fix removed the second pattern everywhere else; the row now reads `.trigger` plus the handler glob the resolved framework document supplies - signal 2026-09-08 — one diff-scoped round should close the document once the four Important findings land; a full-document re-read is warranted only if the first finding's repair moves the handler-shape question into the framework-document contract, and the Minor leftovers are worth a single fix wave rather than a round of their own From db7390458cbf31bdda4ef1bbc5961228df54bde1 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 17:02:36 +0200 Subject: [PATCH 007/120] docs: fix architect round-two findings on the trigger-framework spec --- ...08-salesforce-trigger-frameworks-design.md | 136 ++++++++++++------ 1 file changed, 96 insertions(+), 40 deletions(-) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index c36d6b0..8ad1b37 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -76,7 +76,7 @@ them. | `reference/framework-base-class.md`, `-metadata-driven.md`, `-frameworkless.md` | dispatch shape; where the handler reads context; bypass and recursion API with its error semantics; test-isolation idiom; gotchas; framework-specific rules; two example units | layers below the handler; bulkification; test-class structure | | `salesforce-apex` | naming, layers, bulkification, governor limits, sharing, error handling; the four framework-independent example units | the trigger section, reduced to a layer-table row pointing at `salesforce-triggers` | | `salesforce-apex-testing` | test structure, factory, assertions, mocks | how to disable a handler in a test | -| `salesforce-code-review`, `salesforce-code-reviewer` | a resolution step before grading `.trigger`, then the handler glob the resolved framework document supplies | asking the developer — a background agent cannot | +| `salesforce-code-review`, `salesforce-code-reviewer` | a resolution step before grading `.trigger`, then handler selection by the signature the resolved framework document supplies | asking the developer — a background agent cannot | | `/salesforce-review` | resolution before dispatch, passed in the prompt | — | | `rules/salesforce-toolchain.md` | a routing line for `salesforce-triggers`; the documented declaration key | the declaration itself, which belongs to the project | @@ -116,9 +116,11 @@ citation names the skill, so ids never collide across framework documents. **One trigger per object.** An object has one `.trigger` file. Several handler calls inside that file are correct, in the order they must run; -the violation is a second trigger file, never a second line. Scope: the -directories listed in `packageDirectories`, excluding triggers on objects -a vendor package owns. +the violation is a second trigger file, never a second line. Scope: triggers under the +directories listed in `packageDirectories` and outside `vendor-paths:` — +the same two layers the Exclusions section reads, stated by path rather +than by which package owns an object, since a vendor's trigger on a +standard object belongs to nobody by ownership and to the vendor by path. (id: `trigger-one-per-object`; severity: critical; kind: defect; source: Salesforce Well-Architected, record-triggered decision guide; Apex @@ -168,14 +170,17 @@ a declaration resolves the project. Sub-rules: -- two declarations of unequal shape cover one path, so neither wins (id: +- two declarations cover one path and neither is a subtree strictly + containing the other, so neither wins (id: `trigger-framework-declared.ambiguous`; severity: important) The group default grades an absent declaration, which announces itself: resolution falls through to inference or to a question, and the reader -sees it happen. The sub-rule grades the one case the protocol cannot -resolve — a glob-scoped rule and a subtree-scoped `CLAUDE.md` both -covering the same path. That case hides: the project has declared twice, +sees it happen. The sub-rule grades every case the protocol cannot +rank — a glob-scoped rule against a subtree-scoped `CLAUDE.md`, and +equally two glob-scoped rules, one of them scoped to the whole repository +by declaring no `paths:` at all. That second pairing is the likelier one +wherever a project keeps its declaration in rules. Both hide: the project has declared twice, so the letter of the rule is met while nothing can be resolved from it, and framework rules quietly go ungraded. @@ -198,7 +203,7 @@ answer is stated to the developer, never filled from another framework. 1. **Dispatch** — the trigger body and the handler shape, as two files calling the same lower-layer methods the shipped example uses, plus the - glob matching this framework's handler classes. + **signature** that makes a class one of this framework's handlers. 2. **Context access** — where the handler reads `Trigger.*`. 3. **Bypass and recursion** — the API names *and* what happens when a limit is exceeded. @@ -212,13 +217,24 @@ Question 3 earns its place: `setMaxLoopCount(1)` is a correct guard under one base-class framework and silences automation under another. Names alone cannot tell a reviewer which. -Question 1 carries a glob for the same reason `salesforce-triggers` names -no dispatching class: handler naming belongs to the framework. A -metadata-driven project's action classes are named `TA__` -and implement `TriggerAction.*`, so they match no `*TriggerHandler*` -pattern — a review scoped by that pattern would grade the framework's own -`MetadataTriggerHandler`, which the vendor exclusion skips, and miss every -class the project wrote. +Question 1 carries a signature rather than a filename pattern, and the +distinction is the whole point. A review run already covers every Apex +class — `salesforce-code-review/SKILL.md` lists "Apex: `*.cls`, +`*.trigger`" — so nothing about handlers is a question of scope. What the +answer decides is which of those classes the framework's rules grade, and +no framework imposes a name on them: a metadata-driven project wires an +action through a free-text metadata field and an interface, and this +standard's own `trigger-naming` rule leaves pre-existing names alone. A +class called `LegacyAccountHandler` that implements +`TriggerAction.BeforeInsert` is a handler, and any name-matching pattern +misses it silently. + +The signature is a content pattern, the same mechanism the Fingerprints +table below already uses to recognise a framework: `implements +TriggerAction.` for metadata-driven, `extends TriggerHandler` for +base-class, the `handle(System.TriggerOperation` entry point for +frameworkless. A framework document may add a filename glob as a hint, +never as the test. Framework-specific rules are optional. A document that carries none is graded by the framework-independent rules alone, and the report says so. The tag grammar does @@ -227,10 +243,11 @@ this plugin is not graded on its framework rules at all. ## Resolution protocol -One framework id names each framework everywhere it appears — declaration -value, fingerprint row, document filename, rule prefix. The ids name an -**approach**, never a product, which is why two base-class frameworks share -one document and one prefix. `salesforce-triggers` owns the table: +One framework id names each shipped framework everywhere it appears — +declaration value, fingerprint row, document filename, rule prefix. A +shipped id names an **approach**, never a product, which is why two +base-class frameworks share one document and one prefix. +`salesforce-triggers` owns the table: | `framework-id` | Document | Rule prefix | |---|---|---| @@ -263,13 +280,32 @@ package installed unlocked and without a namespace, vendored as source, sits in a package directory carrying no platform signal that marks it foreign, so no amount of reading `sfdx-project.json` recovers it. -Homes, and the exact set a grep reads: `CLAUDE.md`, `.claude/CLAUDE.md` -and `CLAUDE.local.md` at the repository root or in any directory above the -trigger file, plus any rule the project itself owns under -`.claude/rules/`. Two things are deliberately not homes. A declaration -belongs to the project, so a personal rule under `~/.claude/rules/` never -carries one — it would follow the developer between projects that disagree. -And the installed `salesforce-toolchain.md` copy is not a home either: the +Unlike the framework key, it resolves in exactly one place: **the +repository root**, in whichever root home the project already uses, with +globs relative to the root. It gets no scope rule of its own and needs +none. What counts as somebody else's code is a fact about the repository +rather than about a path inside it, and a single home also settles by +construction what nearest-wins would get backwards — an inner declaration +narrowing an exclusion would re-include a vendor directory, which is the +opposite of what an exclusion is for. A monorepo lists every vendor path +at the root and loses nothing but locality. + +Homes, and the exact set a grep reads: `CLAUDE.md` and +`.claude/CLAUDE.md` at the repository root or in any directory above the +trigger file, plus any rule file under `.claude/rules/` that sits outside +a directory carrying a Rules-engine manifest — the manifest is what marks +a directory as an installed payload, so ownership becomes something a +grep can see. + +Three things are deliberately not homes. A declaration belongs to the +project, so a personal rule under `~/.claude/rules/` never carries one — +it would follow the developer between projects that disagree. Neither does +`CLAUDE.local.md`, on the same footing and for a sharper reason: it is +documented as personal, git-ignored preference, so declaring there would +resolve the author's own sessions while every teammate and every CI run +reported the declaration missing — the one reader who could fix it is the +one reader who never sees it. And the installed `salesforce-toolchain.md` +copy is not a home either: the Rules engine owns that file, so its next sync either overwrites a hand-added line or freezes the rule at the version that carried it, and its `paths:` already covers `**/*.trigger` repo-wide — the same scope as @@ -285,9 +321,9 @@ nothing about how narrow it is. The most specific scope covering a trigger file wins, and specificity is only ever claimed between two subtrees, where one strictly contains the other. A glob is never ranked against a subtree or against another glob: -comparing them invents an answer the project never stated. Two -declarations of unequal shape therefore have no winner, which the next -section turns into behaviour rather than a tie-break. +comparing them invents an answer the project never stated. Any two +declarations the protocol cannot rank therefore have no winner, which the +next section turns into behaviour rather than a tie-break. ### Steps @@ -298,12 +334,12 @@ specificity rule above. | Step | Action | |---|---| -| (a) | grep the candidate files for `trigger-framework:` — an explicit read, never a wait for context | +| (a) | grep the candidate files for `trigger-framework:`, and the root homes for `vendor-paths:` — an explicit read, never a wait for context | | (b) | fingerprint the project's own code, skipping vendor directories | | (c) | ask the developer, with `reference/choosing-a-framework.md` | -**Who repairs what.** Step (c) and the collision between declarations of -unequal shape both need a person, so both belong to an interactive +**Who repairs what.** Step (c) and the collision between declarations the +protocol cannot rank both need a person, so both belong to an interactive session. A background agent asks nothing: it enforces the rules it can resolve and reports whatever it cannot. A session, in turn, does not settle such an ambiguity in conversation — an answer there dies at the @@ -330,6 +366,12 @@ directory, which is exactly when resolution runs. | dispatcher | `TriggerDispatcher.Run(`, or a handler implementing an interface with `IsDisabled()` | | frameworkless | no pattern is not a fingerprint — go to step (c) | +Only the first three rows carry shipped ids. `fflib`, `TDTM` and +`dispatcher` are recognition labels: they name what the pattern found so +the session can say it out loud, and they are products or families rather +than approaches. A label becomes an id only when something declares it, +and then it is the project's word, not this table's. + The metadata-driven fingerprint keys on Custom Metadata records and the trigger body, never on the framework's classes: the records sit in the repository whether the framework arrives as an unlocked package or as @@ -379,7 +421,8 @@ reads the files again. | No triggers at all | go to (c) | | A fingerprint matches a framework this plugin ships nothing for — fflib, TDTM, a dispatcher | the id is sound and only the document is missing, so take the homegrown row below: offer to write a project document, never substitute another framework's | | Two subtree-scoped declarations disagree | the inner one wins, and the disagreement is always stated — never resolved silently | -| Declarations of unequal shape both cover the file — a rule and a `CLAUDE.md` | no winner: name every file that declares, then work out with the developer which home survives and write that change. Picking one silently would answer a question only the project can, and answering it in conversation would leave the collision to recur next session | +| Two declarations cover the file and neither is a subtree containing the other — a rule against a `CLAUDE.md`, or two rules | no winner: name every file that declares, then work out with the developer which home survives and write that change. Picking one silently would answer a question only the project can, and answering it in conversation would leave the collision to recur next session | +| The resolved document answers question 1 without a signature | grade the framework-independent rules, skip the framework rules, and say so in the Summary — a document that cannot say what its handlers are cannot have them graded, and guessing a signature would repeat the mistake a filename pattern already made | | The declared document is missing | say so and grade by the framework-independent rules; never substitute another framework's document, since guidance for the wrong framework writes code that does not compile | | A homegrown framework with no document | offer to write one, describing their code rather than a pattern from the internet — the "dispatcher, handler, helper" division has no primary source | @@ -387,10 +430,11 @@ reads the files again. `salesforce-code-review` gains a step: for `.trigger` files — the one artefact every framework has — load `salesforce-triggers`, resolve through -(a) and (b), then load the framework document by path or name and take the -handler glob from its Dispatch answer to bring the project's handler -classes into scope. The file scope is resolved, never hardcoded, because -only the framework knows what its handlers are called. +(a) and (b), then load the framework document by path or name and select +the project's handler classes by the signature its Dispatch answer gives, +out of the Apex classes the run already covers. Which classes the +framework rules grade is resolved, never hardcoded, because only the +framework knows what makes a class one of its handlers. A background agent enforces standards and asks nothing. `/salesforce-review` therefore resolves before dispatch and passes the @@ -405,8 +449,8 @@ rather than guessing them, notes `trigger framework: unresolved — framework-specific rules not graded` among the Summary's out-of-scope notes, and reports the declaration rule in the `## Project` section — `trigger-framework-declared` where nothing declares the path, -`trigger-framework-declared.ambiguous` where two declarations of unequal -shape do. Both slots already exist in the review-report contract. +`trigger-framework-declared.ambiguous` where two the protocol cannot rank +do. Both slots already exist in the review-report contract. ## Changes to shipped content @@ -538,3 +582,15 @@ Two further deviations: - fixed 2026-09-08 — The fflib domain/handler split is dated April 2021 where the reviewer's cited commit is 2021-03-15; license: the reviewer's citation, offered as an ungraded aside; the date now reads March 2021 - hit fixed 2026-09-08 — the parts table still scoped the review step to `.trigger` and `*TriggerHandler*.cls` after the first finding's fix removed the second pattern everywhere else; the row now reads `.trigger` plus the handler glob the resolved framework document supplies - signal 2026-09-08 — one diff-scoped round should close the document once the four Important findings land; a full-document re-read is warranted only if the first finding's repair moves the handler-shape question into the framework-document contract, and the Minor leftovers are worth a single fix wave rather than a round of their own + +### 2026-09-08 — architect, fable 5.1, blocking (round 2, diff-scoped) + +- fixed 2026-09-08 — [Important] The handler-glob repair swapped one naming-convention scope for another: every `.cls` is already in the run scope, so the pattern's real job is selecting which classes framework rules grade, and no framework imposes handler names — a legacy-named class implementing `TriggerAction.BeforeInsert` matches no glob and goes ungraded; license: the Fingerprints table already recognises frameworks by content pattern, the mechanism this repair was missing; question 1 now supplies a signature, the review step selects handlers by it out of the classes already covered, a glob is demoted to an optional hint, and a failure row covers a document that answers question 1 without one +- fixed 2026-09-08 — [Important] The `.ambiguous` sub-rule grades "two declarations of unequal shape" while the ranking rule leaves two globs unrankable and equal in shape, so two project rules declaring the same path fall between the sub-rule and the group default; license: the rule's own closing sentence already states the boundary as "two the protocol cannot rank"; the sub-rule, its prose, the review section and the failure row now all read that way, and the second pairing is named outright +- fixed 2026-09-08 — [Important] `CLAUDE.local.md` is listed as a declaration home although the same paragraph excludes personal rules because a declaration belongs to the project; declaring there resolves the author's session while every teammate and CI reports the declaration finding; license: the recorded license of the round-one fix excludes a personal file on exactly this footing; the file is no longer a home, and the paragraph says why in terms of who can see the finding +- fixed 2026-09-08 — [Important] `vendor-paths:` borrowed the declaration surface's homes but no resolution rule: nothing states what the globs are relative to or how values combine across homes, nearest-wins is backwards for an exclusion, and no step reads the key that the critical rule's scope depends on; ruling: 2026-09-08; the key resolves at the repository root alone with root-relative globs, which removes the combination question rather than answering it, and step (a) now reads it +- fixed 2026-09-08 — [Minor] `trigger-one-per-object` scopes by object ownership while Exclusions scopes by path, so a vendor trigger on a standard object is skipped by one and counted by the other; license: the Exclusions section, rewritten in the previous wave, already states the scope by path; the rule now reads "under `packageDirectories` and outside `vendor-paths:`" +- fixed 2026-09-08 — [Minor] "The ids name an approach, never a product" is contradicted by the `fflib` and `TDTM` fingerprint rows, which the same wave calls sound ids; license: the id table the same paragraph introduces holds three rows, so the principle was always about shipped ids; the principle now says so, and the remaining fingerprint rows are named recognition labels +- fixed 2026-09-08 — [Minor] The homes set is called exact while one member is defined by ownership, which a grep cannot see; license: the glossary defines a Rules payload as installed beside a manifest, which makes ownership mechanical; the member is now "a rule file outside a directory carrying a Rules-engine manifest" +- fixed 2026-09-08 — this wave left two sentences carrying the old "unequal shape" boundary, in the specificity paragraph and in "Who repairs what"; license: the same sentence that licensed the sub-rule's repair; both now read "the protocol cannot rank". No reviewer graded this and no gate detected it — the session's own sweep found it, so the line carries no severity and is not a gate line +- signal 2026-09-08 — a diff-scoped round 3 over these repairs earns its cost, since each Important fix reshapes a mechanism and that is the class breeding the next round's defects; a full-document re-read now would read text about to change, so the chain debt is better discharged by the integrity audit at the consumption gate, and the three Minor leftovers belong in round 3's diff rather than a round of their own From 22e3bd266756d2940fccea2f4ea149fe2db77491 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 17:23:11 +0200 Subject: [PATCH 008/120] docs: specify the trigger spec's three mechanisms as separate contracts --- ...08-salesforce-trigger-frameworks-design.md | 311 ++++++++++++++---- 1 file changed, 254 insertions(+), 57 deletions(-) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index 8ad1b37..8e93735 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -73,7 +73,7 @@ them. | Part | Owns | Deliberately excludes | |---|---|---| | `salesforce-triggers` (new skill) | framework-agnostic rules; the resolution protocol; the `framework-id` → path table; the questions a framework document answers; fingerprints; the vendor-exclusion scope; `reference/choosing-a-framework.md` | trigger or handler code; bypass API names; the name of the dispatching class | -| `reference/framework-base-class.md`, `-metadata-driven.md`, `-frameworkless.md` | dispatch shape; where the handler reads context; bypass and recursion API with its error semantics; test-isolation idiom; gotchas; framework-specific rules; two example units | layers below the handler; bulkification; test-class structure | +| `reference/framework-base-class.md`, `-metadata-driven.md`, `-frameworkless.md` | dispatch shape; the handler signature and the framework's own types; where the handler reads context; bypass and recursion API with its error semantics; test-isolation idiom; gotchas; framework-specific rules; two example units | layers below the handler; bulkification; test-class structure | | `salesforce-apex` | naming, layers, bulkification, governor limits, sharing, error handling; the four framework-independent example units | the trigger section, reduced to a layer-table row pointing at `salesforce-triggers` | | `salesforce-apex-testing` | test structure, factory, assertions, mocks | how to disable a handler in a test | | `salesforce-code-review`, `salesforce-code-reviewer` | a resolution step before grading `.trigger`, then handler selection by the signature the resolved framework document supplies | asking the developer — a background agent cannot | @@ -146,7 +146,10 @@ Guide, "Document Your Apex Code") **Context stops at the handler.** Nothing below the handler reads `Trigger.*`; context descends as parameters, which keeps the logic -testable without DML. +testable without DML. The types a framework document lists as its own are +exempt: a base class reading `Trigger.*` is the framework doing its job. +This rule alone among the framework-independent ones needs a handler set, +so it goes ungraded wherever no signature supplies one. (id: `trigger-context-below-handler`; severity: important; source: this standard) @@ -170,17 +173,18 @@ a declaration resolves the project. Sub-rules: -- two declarations cover one path and neither is a subtree strictly - containing the other, so neither wins (id: +- two declarations cover one path with different values and neither is a + subtree strictly containing the other, so neither wins (id: `trigger-framework-declared.ambiguous`; severity: important) The group default grades an absent declaration, which announces itself: resolution falls through to inference or to a question, and the reader sees it happen. The sub-rule grades every case the protocol cannot -rank — a glob-scoped rule against a subtree-scoped `CLAUDE.md`, and -equally two glob-scoped rules, one of them scoped to the whole repository -by declaring no `paths:` at all. That second pairing is the likelier one -wherever a project keeps its declaration in rules. Both hide: the project has declared twice, +rank — a glob-scoped rule against a subtree-scoped `CLAUDE.md`, two rules +whose `paths:` differ, and `CLAUDE.md` beside `.claude/CLAUDE.md` in one +directory, whose subtrees are equal so neither contains the other. +Declarations that agree never reach the rule: identical values collapse +before ranking. All of these hide: the project has declared twice, so the letter of the rule is met while nothing can be resolved from it, and framework rules quietly go ungraded. @@ -203,7 +207,9 @@ answer is stated to the developer, never filled from another framework. 1. **Dispatch** — the trigger body and the handler shape, as two files calling the same lower-layer methods the shipped example uses, plus the - **signature** that makes a class one of this framework's handlers. + **signature** that makes a class one of this framework's handlers and + the framework's **own types**, which are neither handlers nor layers + below one. 2. **Context access** — where the handler reads `Trigger.*`. 3. **Bypass and recursion** — the API names *and* what happens when a limit is exceeded. @@ -229,18 +235,72 @@ class called `LegacyAccountHandler` that implements `TriggerAction.BeforeInsert` is a handler, and any name-matching pattern misses it silently. -The signature is a content pattern, the same mechanism the Fingerprints -table below already uses to recognise a framework: `implements -TriggerAction.` for metadata-driven, `extends TriggerHandler` for -base-class, the `handle(System.TriggerOperation` entry point for -frameworkless. A framework document may add a filename glob as a hint, -never as the test. +A signature and a fingerprint share a technique — a pattern matched +against source text — and nothing else. A fingerprint reads the whole +project, one match anywhere is enough, and it yields a label. A signature +reads one class, matches in a named place, and yields membership. Calling +them one mechanism is what let two rounds hand each of them the other's +domain. + +The answer is a block under a fixed heading, in one grammar, so a session +loading a document written elsewhere can tell whether the contract is met +— the base-class answer, in full: + +``` +Signature: + header: extends TriggerHandler +Framework types: TriggerHandler +``` + +`header:` matches the type's declaration header — from the `class` keyword +to the opening brace — which keeps a comment, a string or a javadoc +mention from counting. `implements TriggerAction.` there — the +metadata-driven answer — matches every one of that framework's +interfaces, a class implementing several, and a class implementing +something else besides. `member:` matches a method declaration line inside +the type, which is where the frameworkless answer, +`handle(System.TriggerOperation`, belongs and where a header match would +never look. Matching ignores case, because Apex does, and an optional +namespace prefix is allowed before every type name, because a framework +delivered as a managed package appears in project code as `extends +acme.TriggerHandler`. A document may add a filename glob as a hint, never +as the test. + +The handler set is the smallest fixed point of the classes matching the +signature together with the classes whose header `extends` a type already +in the set. Without that closure, `OrderHandler extends +BaseTriggerHandler` — an org's own layer over the framework's — matches +no pattern and goes ungraded. The closure reads parents from the +repository even where a parent lies outside a diff-scoped run, so a full +run and a diff-scoped run select the same handlers; a parent under a +vendor path is read to recognise it and never graded; a parent absent +from the repository makes the class a non-handler, named in the Summary +with the parent that could not be read. Only top-level types are +handlers — a metadata-driven action is instantiated by name and +`Outer.Inner` is addressable, so that document names the deviation rather +than leaving it to a reader. + +`Framework types:` names what the framework itself owns — a copied base +class, a dispatcher — and is empty for frameworkless. Those types are +read to recognise a framework, graded by no framework rule, and exempt +from `trigger-context-below-handler`, which says so itself. Without the +field, a base-class project's own copy of `TriggerHandler.cls` reads +`Trigger.*`, matches no signature and sits under no vendor path, and the +rule fires on the framework. Framework-specific rules are optional. A document that carries none is graded by the framework-independent rules alone, and the report says so. The tag grammar does not become a public contract in this release, so a document from outside this plugin is not graded on its framework rules at all. +The signature grammar is public regardless — a deviation from the second +consultation, which proposed publishing it together with the tag grammar +and never separately. A document from another plugin must write its +signature in that grammar or its handlers cannot be selected, and +selection buys such a document `trigger-context-below-handler` alone. A +small return, and the grammar has to be published for even that, so the +two grammars move on their own schedules. + ## Resolution protocol One framework id names each shipped framework everywhere it appears — @@ -260,6 +320,27 @@ declaration carries a `doc-path:` or a `skill:` locator, and the table never grows to accommodate one — that is the open class working as intended. +### Three mechanisms, three shapes + +The protocol reads three things from outside itself, and they are not one +mechanism. A **signature** selects over a class's content and comes from +the framework document. A **declaration** resolves over a path, comes from +the project in several places, and carries a scope. **`vendor-paths:`** +filters over a path, comes from one place, and carries no scope at all. +Two review rounds caught this design handing one of them another's rules +by default — a filter given a scope, a resolver read as content, a +selector scoped to the run — so each is specified separately below and +none inherits from a neighbour. + +Each specification answers the same five questions, and answering four of +them is what bred the defects: where the thing is declared and in what +grammar; what it applies over and where in the order; what it returns, +including the value that means *unresolved*; who consumes that value and +in what words; and what makes a document carrying it conformant, tested +when the document loads. One constraint crosses all three: every test +reduces to a grep or a prefix comparison, never to parsing. A monorepo +holds thousands of Apex classes and the reader is a model with file tools. + ### The declaration One line, one key: @@ -270,9 +351,16 @@ trigger-framework: metadata-driven trigger-framework: frameworkless trigger-framework: acme-dispatcher; doc-path: docs/acme-triggers.md trigger-framework: acme-dispatcher; skill: acme-plugin:acme-triggers -vendor-paths: force-app/nebula/**, force-app/vendor/** +vendor-paths: force-app/nebula, force-app/vendor ``` +One key at the start of a line, one declaration per home, one value per +key. A shipped id carrying a locator is a contradiction: the shipped +document is used and the locator reported. An id absent from the table +carrying no locator is the homegrown-without-a-document case below. Two +`trigger-framework:` lines in one home make that home unreadable, which +is reported with the file rather than resolved by picking a line. + `vendor-paths:` is the second key of the surface, and the only other thing the protocol reads from the project. It lists what belongs to a package the project did not write. The scope is **declared, never inferred**: a @@ -280,22 +368,40 @@ package installed unlocked and without a namespace, vendored as source, sits in a package directory carrying no platform signal that marks it foreign, so no amount of reading `sfdx-project.json` recovers it. -Unlike the framework key, it resolves in exactly one place: **the -repository root**, in whichever root home the project already uses, with -globs relative to the root. It gets no scope rule of its own and needs -none. What counts as somebody else's code is a fact about the repository -rather than about a path inside it, and a single home also settles by -construction what nearest-wins would get backwards — an inner declaration -narrowing an exclusion would re-include a vendor directory, which is the -opposite of what an exclusion is for. A monorepo lists every vendor path -at the root and loses nothing but locality. +Unlike the framework key, it resolves at **the repository root** alone. +What counts as somebody else's code is a fact about the repository rather +than about a path inside it, and a scope rule would get an exclusion +backwards — an inner declaration narrowing it would re-include the vendor +directory it was written to remove. + +Its value is a list of **directory prefixes** relative to the root, not +globs: a file is vendor when its path equals an entry or begins with that +entry and a slash. That is the shape `packageDirectories` already uses, +so the two exclusion layers compose in one arithmetic of prefixes; it +leaves no glob dialect to name and no question of whether an entry matches +a file or a directory; and it reduces the test to a prefix comparison +rather than a match an agent can get wrong. Nothing re-includes, so an +entry is a declaration to make narrowly. + +Every root home carries the key — both root `CLAUDE.md` files and every +rule file outside a payload directory, whatever its `paths:`, since this +key has no scope for a glob to narrow — and several homes **union**, +which needs no order because exclusion is monotonic. An absent key and an +empty one say the same thing: nothing is vendor. An entry matching +nothing on disk earns a note, the only signal an agent can give for a +typo. An entry outside every package directory is redundant and passes in +silence, the first layer having excluded it already. A monorepo lists +every vendor path at the root and loses nothing but locality. Homes, and the exact set a grep reads: `CLAUDE.md` and `.claude/CLAUDE.md` at the repository root or in any directory above the trigger file, plus any rule file under `.claude/rules/` that sits outside a directory carrying a Rules-engine manifest — the manifest is what marks a directory as an installed payload, so ownership becomes something a -grep can see. +grep can see. The protocol greps these files rather than waiting for the +platform to load them, which also settles a question the platform +documents nowhere: whether a nested `.claude/CLAUDE.md` is discovered at +all. Three things are deliberately not homes. A declaration belongs to the project, so a personal rule under `~/.claude/rules/` never carries one — @@ -311,32 +417,70 @@ hand-added line or freezes the rule at the version that carried it, and its `paths:` already covers `**/*.trigger` repo-wide — the same scope as the root `CLAUDE.md`, which would manufacture a collision by construction. -Every declaration carries one, and the two kinds are shaped differently. -A `CLAUDE.md` scopes itself by **position** — the subtree of the -directory holding it. A rule scopes itself by **glob**: its `paths:` -patterns, or the whole repository when it declares none. A rule always -sits at the repository root whatever its glob says, so position tells -nothing about how narrow it is. - -The most specific scope covering a trigger file wins, and specificity is -only ever claimed between two subtrees, where one strictly contains the -other. A glob is never ranked against a subtree or against another glob: -comparing them invents an answer the project never stated. Any two -declarations the protocol cannot rank therefore have no winner, which the -next section turns into behaviour rather than a tie-break. +Every declaration carries one, and the kinds are shaped differently. A +`CLAUDE.md` scopes itself by **position** — the subtree of the directory +holding it, and for `.claude/CLAUDE.md` the subtree of the directory +holding `.claude/`. A rule declaring `paths:` scopes itself by **glob**; +a rule declaring none scopes the whole repository, which is the root +subtree and ranks as one. Reading that rule as an unrankable glob would +cost the design its commonest comparison — a repository-wide default +against a per-package override — for nothing. A rule always sits at the +repository root whatever its glob says, so position tells nothing about +how narrow it is. + +Ranking runs in three moves. First, **identical values collapse**: a root +`CLAUDE.md` and a repository-wide rule naming the same framework are one +declaration however many files carry it, and the record names them all. A +project writes that redundancy without thinking, and grading it would be +grading agreement. Second, one value left means resolved. Third, more than +one value means the winner is the declaration whose scope is strictly +contained in the scope of **every** declaration carrying a different +value — subtree within subtree by path prefix, and never a pair involving +a glob, since comparing a glob to anything invents an answer the project +never stated. That one sentence settles a chain of three candidates +without a case of its own. + +No such declaration means no winner, which the next section turns into +behaviour rather than a tie-break. Two members of that set are worth +naming, because both look resolvable and are not. `CLAUDE.md` and +`.claude/CLAUDE.md` in one directory carry the same subtree, so neither +contains the other — and the platform documents nothing about what +happens when both exist, so a protocol picking one would claim knowledge +nobody has. Two rules with differing `paths:` are the same case for the +same reason. + +Where a glob's coverage cannot be settled, the pattern counts as +covering. The platform names no dialect for `paths:`, so the protocol +applies the matching the platform applies and leans on `**`, `*`, +`{a,b}` and root-relative paths alone. A declaration that covers and +cannot be ranked is loud; one dropped from the set is silent. ### Steps -Resolution runs per trigger file. Step (a) collects every declaration -whose scope covers that file — walking up the directory tree for -`CLAUDE.md` homes and reading `paths:` for rules — and then applies the -specificity rule above. +Resolution runs per file. For a trigger file it decides which framework's +rules grade that trigger; for a handler class it decides the same, from +the declarations covering the class's own path. Step (a) collects every +declaration whose scope covers the file — walking up the directory tree +for `CLAUDE.md` homes and reading `paths:` for rules — and then ranks +them as above. + +The order is itself a contract: each step consumes what the one before it +produced. | Step | Action | |---|---| -| (a) | grep the candidate files for `trigger-framework:`, and the root homes for `vendor-paths:` — an explicit read, never a wait for context | -| (b) | fingerprint the project's own code, skipping vendor directories | +| (0) | read `vendor-paths:` from the root homes and subtract it, with the directories outside `packageDirectories`, from the files the run covers — what remains is the grading universe every later step works over | +| (a) | grep the covering homes for `trigger-framework:` and rank them — an explicit read, never a wait for context | +| (b) | fingerprint the grading universe, reading a framework's own types outside it where a document's Fingerprint answer names one | | (c) | ask the developer, with `reference/choosing-a-framework.md` | +| (d) | select the resolved framework's handlers from the grading universe by its signature, closed over `extends` | + +Vendor code is never graded and never fingerprinted, and it is still +**read** where recognition needs it. Those two verbs get two answers on +purpose: the discriminator separating the two base-class frameworks is a +method name inside `TriggerHandler.cls`, which a project vendoring the +framework as source puts under `vendor-paths:` — so a step forbidden to +read there could not tell the two frameworks apart. **Who repairs what.** Step (c) and the collision between declarations the protocol cannot rank both need a person, so both belong to an interactive @@ -395,19 +539,29 @@ the project's, and its own triggers on its own objects are not the project's to review. Reviews already exclude this material — `salesforce-code-review/SKILL.md` states "never review third-party libraries" — so the exclusion needs a scope, not a rule, and -`vendor-paths:` is where that scope is stated. +`vendor-paths:` is where that scope is stated. It excludes material from +grading and from fingerprinting, never from reading: the Steps table above +states where recognition reaches into a vendor path and why it must. ### The resolution record -One line, before the first trigger edit, carrying three fields — the -framework, the source, and the document loaded: +Three lines, before the first trigger edit. The first carries the +framework, the source and the document loaded; the other two carry what +the filter and the selector actually did: ``` Trigger framework: base-class — declared in force-app/billing/triggers/CLAUDE.md — loading framework-base-class.md +Vendor paths: 2 entries from CLAUDE.md +Handlers: 7 selected by the signature in framework-base-class.md ``` -The source reads `declared `, `inferred ` or `asked`. Any -source other than `declared` ends with an offer to write the declaration: +The last two lines exist because an agent that cannot ask must at least +say which filter and which selector it applied. Without them, "no +framework findings" and "framework rules not graded" read identically. +Each degrades in place — `none declared`, or the reason no handler set was +selected. The source reads `declared `, `inferred ` or +`asked`. Any source other than `declared` ends with an offer to write the +declaration: a line in a conversation dies at the next compaction, a declaration survives it. `salesforce-triggers` never remembers a resolution; after compaction it reads the files again. @@ -421,18 +575,24 @@ reads the files again. | No triggers at all | go to (c) | | A fingerprint matches a framework this plugin ships nothing for — fflib, TDTM, a dispatcher | the id is sound and only the document is missing, so take the homegrown row below: offer to write a project document, never substitute another framework's | | Two subtree-scoped declarations disagree | the inner one wins, and the disagreement is always stated — never resolved silently | -| Two declarations cover the file and neither is a subtree containing the other — a rule against a `CLAUDE.md`, or two rules | no winner: name every file that declares, then work out with the developer which home survives and write that change. Picking one silently would answer a question only the project can, and answering it in conversation would leave the collision to recur next session | -| The resolved document answers question 1 without a signature | grade the framework-independent rules, skip the framework rules, and say so in the Summary — a document that cannot say what its handlers are cannot have them graded, and guessing a signature would repeat the mistake a filename pattern already made | +| Two declarations cover the file with different values and neither is a subtree containing the other — a glob-scoped rule against a `CLAUDE.md`, two rules whose `paths:` differ, or `CLAUDE.md` beside `.claude/CLAUDE.md` in one directory | no winner: name every file that declares, then work out with the developer which home survives and write that change. Picking one silently would answer a question only the project can, and answering it in conversation would leave the collision to recur next session | +| The resolved document answers question 1 without a signature, or with one that does not fit the grammar | skip the framework rules **and** `trigger-context-below-handler`, the one framework-independent rule needing a handler set; grade the rest and name the document in the Summary — a document that cannot say what its handlers are cannot have them graded, and guessing a signature would repeat the mistake a filename pattern already made | +| A class matches two frameworks' signatures | grade it under the framework resolved for its own path, which is step (a) over the class rather than over a trigger; where that path resolves to one framework and the class still matches another's signature, grade it under the resolved one and name the other in the Summary | +| A class's `extends` parent is not in the repository | treat the class as a non-handler and name it in the Summary with the parent that could not be read — a managed-package parent is unreadable by construction, and assuming membership would grade a class no rule was written for | +| A home carries two `trigger-framework:` lines, or a shipped id with a locator | the home resolves nothing and is named; a shipped id keeps its shipped document and the stray locator is reported | +| `vendor-paths:` is absent and a directory is plainly third-party | grade it as the project's, since nothing declares otherwise, and note the directory with its evidence in the Summary — a note rather than a finding, because no rule requires the key | +| A `vendor-paths:` entry matches nothing on disk | note it; nothing distinguishes a typo from a directory yet to be added, and a typo silently excludes nothing | | The declared document is missing | say so and grade by the framework-independent rules; never substitute another framework's document, since guidance for the wrong framework writes code that does not compile | | A homegrown framework with no document | offer to write one, describing their code rather than a pattern from the internet — the "dispatcher, handler, helper" division has no primary source | ## Review surface `salesforce-code-review` gains a step: for `.trigger` files — the one -artefact every framework has — load `salesforce-triggers`, resolve through -(a) and (b), then load the framework document by path or name and select -the project's handler classes by the signature its Dispatch answer gives, -out of the Apex classes the run already covers. Which classes the +artefact every framework has — load `salesforce-triggers` and run the +protocol's steps in order, (0) through (b), then load the framework +document by path or name and take step (d), selecting the project's +handler classes by the signature its Dispatch answer gives out of the +Apex classes the run already covers. Which classes the framework rules grade is resolved, never hardcoded, because only the framework knows what makes a class one of its handlers. @@ -450,7 +610,10 @@ framework-specific rules not graded` among the Summary's out-of-scope notes, and reports the declaration rule in the `## Project` section — `trigger-framework-declared` where nothing declares the path, `trigger-framework-declared.ambiguous` where two the protocol cannot rank -do. Both slots already exist in the review-report contract. +do. Both slots already exist in the review-report contract. Where +resolution succeeds and the document supplies no signature, the same note +names `trigger-context-below-handler` beside the framework rules, that +rule needing a handler set too. ## Changes to shipped content @@ -503,6 +666,10 @@ judge them: TLS verification disabled after the HTML pages returned 403. - Well-Architected quotations come from two consistent extractions of the same page rather than a byte-exact fetch. +- Apex's case-insensitivity, which the signature grammar relies on, is + uncited. The language reference pages return 403 and the PDF was not + extracted for it, so the claim rests on expert knowledge — as sound as + it is unverified here. One provenance note matters for the rule that carries the most weight. The phrase "one trigger per object" appears nowhere in the 825-page Apex @@ -538,6 +705,32 @@ Two further deviations: sample code now separates them. The decision stands on the metadata-driven case, where the handler is the framework's own class. +A second consultation, on the three mechanisms above, followed two +blocking rounds that diagnosed one repeated failure. Its proposals are +adopted, three of them as choices rather than repairs: + +- **`vendor-paths:` carries directory prefixes, not globs.** Prefixes + compose with `packageDirectories` in one arithmetic and leave no dialect + to name. The cost is that `**/nebula` cannot be written; a monorepo pays + it by listing paths at the root, which this design already accepted. +- **A handler class is graded under the framework resolved for its own + path.** The designer named this the developer's call, against grading by + whichever signature matches anywhere in the run. Resolving per path is + the generalisation round one already made for trigger files, and the + alternative ungrades every class in a monorepo running two frameworks. +- **An undeclared vendor directory earns a note, not a rule.** A minor + rule was the alternative, an analogue of `trigger-framework-declared`. + The note is the narrower change and keeps clear of the ruling that this + scope is declared and never inferred; the rule is an open question + below. + +One proposal is declined. The designer would couple the signature grammar +to the tag grammar, publishing neither before the other. A foreign +document cannot write a conforming signature without the grammar, so +withholding it would leave the open class unreachable — the reasoning +sits beside the clause it concerns, under "What a framework document +answers". + ## Out of scope - **Migration between frameworks.** Deferred, with it the rule that would @@ -563,6 +756,10 @@ Two further deviations: branch `feature/trigger-frameworks`, without the issue number the repository convention prescribes. A ticket may still be opened before the pull request. +- **Whether an undeclared vendor directory should be graded.** Today it + earns a note in the Summary. A minor rule — the analogue of + `trigger-framework-declared` — would grade it, and the only argument + against is that inference would then produce a finding. Undecided. - **How many consumers run a framework other than frameworkless.** Unknown, and no longer load-bearing: the question tested the proportionality of shipping four skills, and this design ships one skill From 5411140dac3e3bdf8bfe463a7ce50d36d1b64b89 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 17:47:26 +0200 Subject: [PATCH 009/120] docs: fix round-three concerns in the trigger-framework spec --- ...08-salesforce-trigger-frameworks-design.md | 98 +++++++++++++------ 1 file changed, 67 insertions(+), 31 deletions(-) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index 8e93735..0e6062f 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -3,7 +3,7 @@ ticket: none date: 2026-09-08 status: draft grilled: 2026-09-08 -architect: blocking +architect: concerns branch: feature/trigger-frameworks base: develop --- @@ -183,8 +183,8 @@ sees it happen. The sub-rule grades every case the protocol cannot rank — a glob-scoped rule against a subtree-scoped `CLAUDE.md`, two rules whose `paths:` differ, and `CLAUDE.md` beside `.claude/CLAUDE.md` in one directory, whose subtrees are equal so neither contains the other. -Declarations that agree never reach the rule: identical values collapse -before ranking. All of these hide: the project has declared twice, +Declarations that agree never reach the rule: ranking counts distinct +values, and agreement is not a collision. All of these hide: the project has declared twice, so the letter of the rule is met while nothing can be resolved from it, and framework rules quietly go ungraded. @@ -281,12 +281,20 @@ handlers — a metadata-driven action is instantiated by name and than leaving it to a reader. `Framework types:` names what the framework itself owns — a copied base -class, a dispatcher — and is empty for frameworkless. Those types are -read to recognise a framework, graded by no framework rule, and exempt -from `trigger-context-below-handler`, which says so itself. Without the -field, a base-class project's own copy of `TriggerHandler.cls` reads -`Trigger.*`, matches no signature and sits under no vendor path, and the -rule fires on the framework. +class, a dispatcher. An absent field and an empty one say the same thing, +as they do for `vendor-paths:`, and that is the frameworkless answer; a +document omitting the field still has a signature, and only a malformed +`Signature:` block fails the grammar. The field is also the only place +those names are written: a Fingerprint answer that has to read inside a +framework's own class points at a type from here rather than repeating +it. + +What the field buys is one exemption, which is why a document may leave +it out. Its types are read to recognise a framework, graded by no +framework rule, and exempt from `trigger-context-below-handler`, which +says so itself. Without them, a base-class project's own copy of +`TriggerHandler.cls` reads `Trigger.*`, matches no signature and sits +under no vendor path, and the rule fires on the framework. Framework-specific rules are optional. A document that carries none is graded by the framework-independent rules alone, and the report says so. The tag grammar does @@ -355,7 +363,9 @@ vendor-paths: force-app/nebula, force-app/vendor ``` One key at the start of a line, one declaration per home, one value per -key. A shipped id carrying a locator is a contradiction: the shipped +key. The value is everything after the key, locator included: two homes +naming one framework through different documents disagree, and ranking +treats them as it treats any two values. A shipped id carrying a locator is a contradiction: the shipped document is used and the locator reported. An id absent from the table carrying no locator is the homegrown-without-a-document case below. Two `trigger-framework:` lines in one home make that home unreadable, which @@ -428,17 +438,19 @@ against a per-package override — for nothing. A rule always sits at the repository root whatever its glob says, so position tells nothing about how narrow it is. -Ranking runs in three moves. First, **identical values collapse**: a root -`CLAUDE.md` and a repository-wide rule naming the same framework are one -declaration however many files carry it, and the record names them all. A -project writes that redundancy without thinking, and grading it would be -grading agreement. Second, one value left means resolved. Third, more than -one value means the winner is the declaration whose scope is strictly -contained in the scope of **every** declaration carrying a different -value — subtree within subtree by path prefix, and never a pair involving -a glob, since comparing a glob to anything invents an answer the project -never stated. That one sentence settles a chain of three candidates -without a case of its own. +Ranking runs in three moves, and every declaration keeps its own scope +through all of them. First, count the **distinct values**: a root +`CLAUDE.md` and a repository-wide rule naming the same framework agree, +and the record names both. A project writes that redundancy without +thinking, and grading agreement is not grading a defect. Second, one +distinct value means resolved. Third, more than one means the winner is +the declaration whose scope is strictly contained in the scope of +**every** declaration carrying a different value — subtree within subtree +by path prefix, and never a pair involving a glob, since comparing a glob +to anything invents an answer the project never stated. Because nothing +is merged, a repository-wide default against a nearer override still has +two scopes to compare, and a chain of three candidates settles without a +case of its own. No such declaration means no winner, which the next section turns into behaviour rather than a tie-break. Two members of that set are worth @@ -469,12 +481,16 @@ produced. | Step | Action | |---|---| -| (0) | read `vendor-paths:` from the root homes and subtract it, with the directories outside `packageDirectories`, from the files the run covers — what remains is the grading universe every later step works over | +| (0) | read `vendor-paths:` from the root homes and subtract it, with the directories outside `packageDirectories`, from the repository's Apex files — what remains is the grading universe every later step works over, whether the caller is a review run or a session about to write a trigger | | (a) | grep the covering homes for `trigger-framework:` and rank them — an explicit read, never a wait for context | -| (b) | fingerprint the grading universe, reading a framework's own types outside it where a document's Fingerprint answer names one | +| (b) | fingerprint the grading universe, reading a framework's own types outside it where the Fingerprint answer points at one of the types `Framework types:` names | | (c) | ask the developer, with `reference/choosing-a-framework.md` | | (d) | select the resolved framework's handlers from the grading universe by its signature, closed over `extends` | +Steps (0) through (c) serve both surfaces. Step (d) belongs to the review +surface alone: a session about to write a trigger needs the resolved +document, not an enumeration of the project's handlers. + Vendor code is never graded and never fingerprinted, and it is still **read** where recognition needs it. Those two verbs get two answers on purpose: the discriminator separating the two base-class frameworks is a @@ -559,7 +575,9 @@ The last two lines exist because an agent that cannot ask must at least say which filter and which selector it applied. Without them, "no framework findings" and "framework rules not graded" read identically. Each degrades in place — `none declared`, or the reason no handler set was -selected. The source reads `declared `, `inferred ` or +selected — and the `Handlers:` line appears only where step (d) ran, +which is the review surface; an authoring session's record stops at the +document. The source reads `declared `, `inferred ` or `asked`. Any source other than `declared` ends with an offer to write the declaration: a line in a conversation dies at the next compaction, a declaration @@ -589,10 +607,12 @@ reads the files again. `salesforce-code-review` gains a step: for `.trigger` files — the one artefact every framework has — load `salesforce-triggers` and run the -protocol's steps in order, (0) through (b), then load the framework -document by path or name and take step (d), selecting the project's -handler classes by the signature its Dispatch answer gives out of the -Apex classes the run already covers. Which classes the +protocol's steps in order, (0) through (b), then load each resolved +framework's document by path or name and take step (d), selecting that +framework's handler classes out of the grading universe by the signature +its Dispatch answer gives. Resolution runs per file throughout, so a +monorepo running two frameworks loads two documents and grades every +class under the framework resolved for its own path. Which classes the framework rules grade is resolved, never hardcoded, because only the framework knows what makes a class one of its handlers. @@ -727,7 +747,9 @@ adopted, three of them as choices rather than repairs: One proposal is declined. The designer would couple the signature grammar to the tag grammar, publishing neither before the other. A foreign document cannot write a conforming signature without the grammar, so -withholding it would leave the open class unreachable — the reasoning +withholding it would leave every foreign document's handlers +unselectable, and with them the one framework-independent rule needing a +handler set — the reasoning sits beside the clause it concerns, under "What a framework document answers". @@ -736,8 +758,10 @@ answers". - **Migration between frameworks.** Deferred, with it the rule that would grade a new file against a declared target. - **The tag grammar as a public contract.** A document from another plugin - is not graded on framework rules, so the grammar stays internal and - `plugin-versioning` gains no new breaking surface. + is not graded on framework rules, so that grammar stays internal. The + signature grammar does go public, so it is a convention others rely on + in the sense `plugin-versioning` defines: a change to `header:`, + `member:` or `Framework types:` takes the bump that rule prescribes. - **Generality across the Standards family.** `python-standards` commits to one toolchain outright, so resolving between frameworks has a single instance today and stays local to this plugin. Should a second @@ -791,3 +815,15 @@ answers". - fixed 2026-09-08 — [Minor] The homes set is called exact while one member is defined by ownership, which a grep cannot see; license: the glossary defines a Rules payload as installed beside a manifest, which makes ownership mechanical; the member is now "a rule file outside a directory carrying a Rules-engine manifest" - fixed 2026-09-08 — this wave left two sentences carrying the old "unequal shape" boundary, in the specificity paragraph and in "Who repairs what"; license: the same sentence that licensed the sub-rule's repair; both now read "the protocol cannot rank". No reviewer graded this and no gate detected it — the session's own sweep found it, so the line carries no severity and is not a gate line - signal 2026-09-08 — a diff-scoped round 3 over these repairs earns its cost, since each Important fix reshapes a mechanism and that is the class breeding the next round's defects; a full-document re-read now would read text about to change, so the chain debt is better discharged by the integrity audit at the consumption gate, and the three Minor leftovers belong in round 3's diff rather than a round of their own + +### 2026-09-08 — architect, fable 5.1, concerns (round 3, diff-scoped) + +- held — [Important] The managed-package base-class case falls between two clauses of the same wave: `extends acme.TriggerHandler` satisfies both the namespace-prefix allowance in the signature grammar and the parent-absent row that makes an unreadable parent a non-handler, and the discriminator separating the two base-class frameworks is unreadable in a managed package, so even a selected handler cannot be graded on question 3; question: keep the namespace-prefix allowance and scope the parent-absent rule to closure candidates only, or drop the allowance and route a managed-package framework to the unshipped-framework row?; options: (a) drop the allowance — one clause deleted plus a routing sentence, and the reviewer's stop signal says round 4 would not repay its cost; (b) keep it, scope the parent-absent rule to closure, and have the base-class document state how the fork is identified when its source is unreadable — this touches that document's contract and earns one more diff-scoped round; the session recommends (b), since a direct signature match and a closure candidate are different things and reading the parent-absent rule as scoped to closure costs nothing, while dropping the allowance closes the door on a framework packaged for reuse across orgs +- fixed 2026-09-08 — [Minor] "Identical values collapse" is written as merging declarations, so move three has no scope to rank and the three-candidate chain the same paragraph claims to settle has none for the merged value; the value compared is also never said to include the locator; license: the same paragraph claims to settle a chain of three candidates, which the finding shows it cannot; move one now counts distinct values while every declaration keeps its scope, nothing is merged, and the declaration grammar states that the value is everything after the key, locator included +- fixed 2026-09-08 — [Minor] `Framework types:` is mandatory in form while its empty value already means none, and a document omitting it loses its whole signature — against the sibling key, where an absent and an empty value say the same thing; license: the `vendor-paths:` paragraph already states that an absent key and an empty one say the same thing; the field is now optional on that footing, only a malformed `Signature:` block fails the grammar, and the paragraph says what the field buys — one exemption — so a foreign author knows when to write it +- fixed 2026-09-08 — [Minor] The signature grammar is published while Out of scope still says the grammar stays internal and `plugin-versioning` gains no new breaking surface, which that rule counts as a convention others rely on; "unreachable" also overstates what withholding it would cost; license: `.claude/rules/plugin-versioning.md:46-47` grades a breaking change to a convention others rely on; Out of scope now scopes its claim to the tag grammar and names the signature grammar as a published convention the bump rule covers, and "unreachable" became the concrete cost — every foreign document's handlers unselectable, and with them the one rule needing a handler set +- fixed 2026-09-08 — [Minor] Step (0), step (d) and the resolution record are defined in review-run vocabulary although the protocol runs at authoring time too, where there is no run and no handler set is needed; license: the resolution record is written "before the first trigger edit", which is the authoring surface the document already serves; step (0) now subtracts from the repository's Apex files for either caller, a sentence assigns steps (0) to (c) to both surfaces and step (d) to the review surface alone, and the `Handlers:` line appears only where step (d) ran +- fixed 2026-09-08 — [Minor] One type name is carried by `Framework types:` and by the Fingerprint answer for two consumers, against the one-mechanism-one-shape discipline the same wave introduced; license: the `Three mechanisms, three shapes` subsection, written in the same wave, forbids exactly this; `Framework types:` is now the only place those names are written and step (b) points at a type from that field rather than naming one of its own +- fixed 2026-09-08 — the F2 repair left the `.ambiguous` prose saying "identical values collapse before ranking", which the same wave had replaced with counting distinct values and merging nothing; license: the sentence the repair itself wrote; the prose now says ranking counts distinct values and agreement is not a collision. The session's own sweep found it — no reviewer graded it and no gate ran over it +- fixed 2026-09-08 — the review-surface paragraph still read "out of the Apex classes the run already covers" and named one framework document, lagging the per-file resolution this wave had already adopted; license: the Steps section states that resolution runs per file and the deviations section records the decision; the paragraph now selects out of the grading universe, loads each resolved framework's document, and says that a monorepo running two frameworks grades every class under the framework resolved for its own path. The reviewer noted the lag in one line without grading it and left it to the integrity audit; the propagation gate returned CLEAN over it +- signal 2026-09-08 — the stop signal is conditional on the held finding: under option (a) round 4 does not repay its cost, the Minor leftovers being self-fixable and the noted lag already owed to the integrity audit at the consumption gate; under option (b) one diff-scoped round over the base-class document's contract earns its cost, that being a reshaped mechanism again. The Minor leftovers are worth a single fix wave, not a round of their own From 6d9de084aa812d4f75193552e0b53b56527c3738 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 17:51:19 +0200 Subject: [PATCH 010/120] docs: scope the extends-parent rules to closure candidates --- ...08-salesforce-trigger-frameworks-design.md | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index 0e6062f..7aa8483 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -184,8 +184,9 @@ rank — a glob-scoped rule against a subtree-scoped `CLAUDE.md`, two rules whose `paths:` differ, and `CLAUDE.md` beside `.claude/CLAUDE.md` in one directory, whose subtrees are equal so neither contains the other. Declarations that agree never reach the rule: ranking counts distinct -values, and agreement is not a collision. All of these hide: the project has declared twice, -so the letter of the rule is met while nothing can be resolved from it, +values, and agreement is not a collision. All of these hide: the project +has declared twice, so the letter of the rule is met while nothing can be +resolved from it, and framework rules quietly go ungraded. A root default with a nearer override is **not** this finding. Two @@ -270,12 +271,20 @@ The handler set is the smallest fixed point of the classes matching the signature together with the classes whose header `extends` a type already in the set. Without that closure, `OrderHandler extends BaseTriggerHandler` — an org's own layer over the framework's — matches -no pattern and goes ungraded. The closure reads parents from the -repository even where a parent lies outside a diff-scoped run, so a full -run and a diff-scoped run select the same handlers; a parent under a -vendor path is read to recognise it and never graded; a parent absent -from the repository makes the class a non-handler, named in the Summary -with the parent that could not be read. Only top-level types are +no pattern and goes ungraded. The boundary between the two is worth +one sentence: a class matching the signature is a handler on that match +alone, and the parent rules below govern closure candidates — the classes +matching nothing that reach the set only through a parent. Otherwise +`OrderHandler extends acme.TriggerHandler`, a framework delivered as a +managed package, would be admitted by the namespace prefix and rejected +by the same paragraph. + +The closure reads parents from the repository even where a parent lies +outside a diff-scoped run, so a full run and a diff-scoped run select the +same handlers; a parent under a vendor path is read to recognise it and +never graded; a parent absent from the repository leaves a closure +candidate a non-handler, named in the Summary with the parent that could +not be read. Only top-level types are handlers — a metadata-driven action is instantiated by name and `Outer.Inner` is addressable, so that document names the deviation rather than leaving it to a reader. @@ -365,8 +374,9 @@ vendor-paths: force-app/nebula, force-app/vendor One key at the start of a line, one declaration per home, one value per key. The value is everything after the key, locator included: two homes naming one framework through different documents disagree, and ranking -treats them as it treats any two values. A shipped id carrying a locator is a contradiction: the shipped -document is used and the locator reported. An id absent from the table +treats them as it treats any two values. A shipped id carrying a locator +is a contradiction: the shipped document is used and the locator +reported. An id absent from the table carrying no locator is the homegrown-without-a-document case below. Two `trigger-framework:` lines in one home make that home unreadable, which is reported with the file rather than resolved by picking a line. @@ -543,6 +553,15 @@ discriminator is the method name inside `TriggerHandler.cls`: `incrementCheckLoopCount` silences an exceeded loop count, `addToLoopCount` throws. +That file is unreadable where the framework arrives as a managed package, +so the base-class document owes a second answer: what else tells the two +apart — a namespace, a version — and what a reviewer does when nothing +does. The answer is the discipline every other mechanism here takes. The +rules keyed to error semantics go ungraded, the Summary says which and +why, and no reviewer guesses; the framework's other rules grade normally, +the project's own handlers being readable whatever carries their +parent. + ### Exclusions Two layers, not one: directories outside `packageDirectories`, and the @@ -596,7 +615,8 @@ reads the files again. | Two declarations cover the file with different values and neither is a subtree containing the other — a glob-scoped rule against a `CLAUDE.md`, two rules whose `paths:` differ, or `CLAUDE.md` beside `.claude/CLAUDE.md` in one directory | no winner: name every file that declares, then work out with the developer which home survives and write that change. Picking one silently would answer a question only the project can, and answering it in conversation would leave the collision to recur next session | | The resolved document answers question 1 without a signature, or with one that does not fit the grammar | skip the framework rules **and** `trigger-context-below-handler`, the one framework-independent rule needing a handler set; grade the rest and name the document in the Summary — a document that cannot say what its handlers are cannot have them graded, and guessing a signature would repeat the mistake a filename pattern already made | | A class matches two frameworks' signatures | grade it under the framework resolved for its own path, which is step (a) over the class rather than over a trigger; where that path resolves to one framework and the class still matches another's signature, grade it under the resolved one and name the other in the Summary | -| A class's `extends` parent is not in the repository | treat the class as a non-handler and name it in the Summary with the parent that could not be read — a managed-package parent is unreadable by construction, and assuming membership would grade a class no rule was written for | +| A closure candidate's `extends` parent is not in the repository | treat the class as a non-handler and name it in the Summary with the parent that could not be read — a managed-package parent is unreadable by construction, and assuming membership would grade a class no rule was written for. A class matching the signature itself is unaffected, parent or no parent | +| The base-class framework arrives as a managed package, so `TriggerHandler.cls` cannot be read | select handlers as usual, the signature matching the project's own classes, and grade every rule except those keyed to error semantics, naming in the Summary that the discriminator was unreadable — a guess between silencing and throwing is the one guess that turns a correct bypass into silenced automation | | A home carries two `trigger-framework:` lines, or a shipped id with a locator | the home resolves nothing and is named; a shipped id keeps its shipped document and the stray locator is reported | | `vendor-paths:` is absent and a directory is plainly third-party | grade it as the project's, since nothing declares otherwise, and note the directory with its evidence in the Summary — a note rather than a finding, because no rule requires the key | | A `vendor-paths:` entry matches nothing on disk | note it; nothing distinguishes a typo from a directory yet to be added, and a typo silently excludes nothing | @@ -818,7 +838,7 @@ answers". ### 2026-09-08 — architect, fable 5.1, concerns (round 3, diff-scoped) -- held — [Important] The managed-package base-class case falls between two clauses of the same wave: `extends acme.TriggerHandler` satisfies both the namespace-prefix allowance in the signature grammar and the parent-absent row that makes an unreadable parent a non-handler, and the discriminator separating the two base-class frameworks is unreadable in a managed package, so even a selected handler cannot be graded on question 3; question: keep the namespace-prefix allowance and scope the parent-absent rule to closure candidates only, or drop the allowance and route a managed-package framework to the unshipped-framework row?; options: (a) drop the allowance — one clause deleted plus a routing sentence, and the reviewer's stop signal says round 4 would not repay its cost; (b) keep it, scope the parent-absent rule to closure, and have the base-class document state how the fork is identified when its source is unreadable — this touches that document's contract and earns one more diff-scoped round; the session recommends (b), since a direct signature match and a closure candidate are different things and reading the parent-absent rule as scoped to closure costs nothing, while dropping the allowance closes the door on a framework packaged for reuse across orgs +- fixed 2026-09-08 — [Important] The managed-package base-class case falls between two clauses of the same wave: `extends acme.TriggerHandler` satisfies both the namespace-prefix allowance in the signature grammar and the parent-absent row that makes an unreadable parent a non-handler, and the discriminator separating the two base-class frameworks is unreadable in a managed package, so even a selected handler cannot be graded on question 3; ruling: 2026-09-08; the allowance stands and the parent rules were scoped to closure candidates, a class matching the signature being a handler on that match alone; the base-class document now owes a second answer for an unreadable source, and the rules keyed to error semantics go ungraded with the Summary saying why rather than a reviewer guessing between silencing and throwing - fixed 2026-09-08 — [Minor] "Identical values collapse" is written as merging declarations, so move three has no scope to rank and the three-candidate chain the same paragraph claims to settle has none for the merged value; the value compared is also never said to include the locator; license: the same paragraph claims to settle a chain of three candidates, which the finding shows it cannot; move one now counts distinct values while every declaration keeps its scope, nothing is merged, and the declaration grammar states that the value is everything after the key, locator included - fixed 2026-09-08 — [Minor] `Framework types:` is mandatory in form while its empty value already means none, and a document omitting it loses its whole signature — against the sibling key, where an absent and an empty value say the same thing; license: the `vendor-paths:` paragraph already states that an absent key and an empty one say the same thing; the field is now optional on that footing, only a malformed `Signature:` block fails the grammar, and the paragraph says what the field buys — one exemption — so a foreign author knows when to write it - fixed 2026-09-08 — [Minor] The signature grammar is published while Out of scope still says the grammar stays internal and `plugin-versioning` gains no new breaking surface, which that rule counts as a convention others rely on; "unreachable" also overstates what withholding it would cost; license: `.claude/rules/plugin-versioning.md:46-47` grades a breaking change to a convention others rely on; Out of scope now scopes its claim to the tag grammar and names the signature grammar as a published convention the bump rule covers, and "unreachable" became the concrete cost — every foreign document's handlers unselectable, and with them the one rule needing a handler set From 7bb8931a53fc808b2b0c23bfa34c9106492aa3cc Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 17:57:34 +0200 Subject: [PATCH 011/120] docs: dismiss the propagation gate's hit on a historical ledger line --- docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index 7aa8483..8cd9f37 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -846,4 +846,5 @@ answers". - fixed 2026-09-08 — [Minor] One type name is carried by `Framework types:` and by the Fingerprint answer for two consumers, against the one-mechanism-one-shape discipline the same wave introduced; license: the `Three mechanisms, three shapes` subsection, written in the same wave, forbids exactly this; `Framework types:` is now the only place those names are written and step (b) points at a type from that field rather than naming one of its own - fixed 2026-09-08 — the F2 repair left the `.ambiguous` prose saying "identical values collapse before ranking", which the same wave had replaced with counting distinct values and merging nothing; license: the sentence the repair itself wrote; the prose now says ranking counts distinct values and agreement is not a collision. The session's own sweep found it — no reviewer graded it and no gate ran over it - fixed 2026-09-08 — the review-surface paragraph still read "out of the Apex classes the run already covers" and named one framework document, lagging the per-file resolution this wave had already adopted; license: the Steps section states that resolution runs per file and the deviations section records the decision; the paragraph now selects out of the grading universe, loads each resolved framework's document, and says that a monorepo running two frameworks grades every class under the framework resolved for its own path. The reviewer noted the lag in one line without grading it and left it to the integrity audit; the propagation gate returned CLEAN over it +- hit dismissed 2026-09-08 — round one's `hit fixed` line says the parts table row now reads "`.trigger` plus the handler glob the resolved framework document supplies", while the row reads "handler selection by the signature the resolved framework document supplies"; counter: the ledger is chronological, and round two's own `fixed` line records replacing that glob with a signature and demoting the glob to an optional hint — a round-one line describing the round-one state is correct history, and the gate's brief was at fault for asking whether every historical line matches the current text - signal 2026-09-08 — the stop signal is conditional on the held finding: under option (a) round 4 does not repay its cost, the Minor leftovers being self-fixable and the noted lag already owed to the integrity audit at the consumption gate; under option (b) one diff-scoped round over the base-class document's contract earns its cost, that being a reshaped mechanism again. The Minor leftovers are worth a single fix wave, not a round of their own From 55233868c4950813f742375cf05b0e1001232952 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 18:06:14 +0200 Subject: [PATCH 012/120] docs: fix round-four concerns in the trigger-framework spec --- ...08-salesforce-trigger-frameworks-design.md | 60 ++++++++++++++----- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index 8cd9f37..72281f9 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -224,6 +224,18 @@ Question 3 earns its place: `setMaxLoopCount(1)` is a correct guard under one base-class framework and silences automation under another. Names alone cannot tell a reviewer which. +For the base-class document, question 3 carries one duty more, because +the two frameworks answer it differently and the discriminator can be +unreadable — it is a method name inside `TriggerHandler.cls`, and a +managed package hides it. That document says what else tells them apart, +a namespace or a version, and what happens when nothing does: the rules +keyed to error semantics go ungraded, the Summary says which and why, and +no reviewer guesses, while the framework's other rules grade normally, +the project's own handlers being readable whatever carries their parent. +An authoring session has no Summary to write in, so it states both +semantics and says the installed package decides which holds — the +difference being whether `setMaxLoopCount(1)` guards or silences. + Question 1 carries a signature rather than a filename pattern, and the distinction is the whole point. A review run already covers every Apex class — `salesforce-code-review/SKILL.md` lists "Apex: `*.cls`, @@ -273,19 +285,29 @@ in the set. Without that closure, `OrderHandler extends BaseTriggerHandler` — an org's own layer over the framework's — matches no pattern and goes ungraded. The boundary between the two is worth one sentence: a class matching the signature is a handler on that match -alone, and the parent rules below govern closure candidates — the classes -matching nothing that reach the set only through a parent. Otherwise +alone, and the parent rules below govern **closure candidates** — a class +matching no pattern whose header carries `extends`. Otherwise `OrderHandler extends acme.TriggerHandler`, a framework delivered as a managed package, would be admitted by the namespace prefix and rejected by the same paragraph. +The candidate is defined by what its header carries, never by what it +might turn out to be: the second reading is circular, and a rule that +fires on every unreadable parent would report most of a repository. Most +classes extending something the repository does not hold extend a +platform type, `extends Exception` first — the shipped example's own +`OrderProcessingException` does +(`salesforce-apex/reference/trigger-handler.md:177`). Unreadable parents +are therefore reported in aggregate: one Summary line naming the distinct +parents with a count each. Nothing about the line is graded, and a +namespace-qualified parent is the one worth a second look. + The closure reads parents from the repository even where a parent lies outside a diff-scoped run, so a full run and a diff-scoped run select the same handlers; a parent under a vendor path is read to recognise it and never graded; a parent absent from the repository leaves a closure -candidate a non-handler, named in the Summary with the parent that could -not be read. Only top-level types are -handlers — a metadata-driven action is instantiated by name and +candidate a non-handler, counted on that one Summary line. Only top-level +types are handlers — a metadata-driven action is instantiated by name and `Outer.Inner` is addressable, so that document names the deviation rather than leaving it to a reader. @@ -536,6 +558,13 @@ directory, which is exactly when resolution runs. | dispatcher | `TriggerDispatcher.Run(`, or a handler implementing an interface with `IsDisabled()` | | frameworkless | no pattern is not a fingerprint — go to step (c) | +Every pattern tolerates an optional namespace prefix before a type name, +for the fingerprint's own reason rather than the signature's: a +fingerprint exists to recognise, and a framework delivered as a package +is exactly the case a literal `extends TriggerHandler` would miss, +sending a project that plainly has a framework to the question that asks +whether it has one. + Only the first three rows carry shipped ids. `fflib`, `TDTM` and `dispatcher` are recognition labels: they name what the pattern found so the session can say it out loud, and they are products or families rather @@ -553,14 +582,9 @@ discriminator is the method name inside `TriggerHandler.cls`: `incrementCheckLoopCount` silences an exceeded loop count, `addToLoopCount` throws. -That file is unreadable where the framework arrives as a managed package, -so the base-class document owes a second answer: what else tells the two -apart — a namespace, a version — and what a reviewer does when nothing -does. The answer is the discipline every other mechanism here takes. The -rules keyed to error semantics go ungraded, the Summary says which and -why, and no reviewer guesses; the framework's other rules grade normally, -the project's own handlers being readable whatever carries their -parent. +That file is unreadable where the framework arrives as a managed package. +What the base-class document owes then is stated with question 3 above, +beside the rest of that document's error-semantics duty. ### Exclusions @@ -615,7 +639,7 @@ reads the files again. | Two declarations cover the file with different values and neither is a subtree containing the other — a glob-scoped rule against a `CLAUDE.md`, two rules whose `paths:` differ, or `CLAUDE.md` beside `.claude/CLAUDE.md` in one directory | no winner: name every file that declares, then work out with the developer which home survives and write that change. Picking one silently would answer a question only the project can, and answering it in conversation would leave the collision to recur next session | | The resolved document answers question 1 without a signature, or with one that does not fit the grammar | skip the framework rules **and** `trigger-context-below-handler`, the one framework-independent rule needing a handler set; grade the rest and name the document in the Summary — a document that cannot say what its handlers are cannot have them graded, and guessing a signature would repeat the mistake a filename pattern already made | | A class matches two frameworks' signatures | grade it under the framework resolved for its own path, which is step (a) over the class rather than over a trigger; where that path resolves to one framework and the class still matches another's signature, grade it under the resolved one and name the other in the Summary | -| A closure candidate's `extends` parent is not in the repository | treat the class as a non-handler and name it in the Summary with the parent that could not be read — a managed-package parent is unreadable by construction, and assuming membership would grade a class no rule was written for. A class matching the signature itself is unaffected, parent or no parent | +| A closure candidate's `extends` parent is not in the repository | treat the class as a non-handler and count it on the Summary's one line of distinct unreadable parents — a managed-package parent is unreadable by construction, assuming membership would grade a class no rule was written for, and naming each class individually would bury the signal under every custom exception. A class matching the signature itself is unaffected, parent or no parent | | The base-class framework arrives as a managed package, so `TriggerHandler.cls` cannot be read | select handlers as usual, the signature matching the project's own classes, and grade every rule except those keyed to error semantics, naming in the Summary that the discriminator was unreadable — a guess between silencing and throwing is the one guess that turns a correct bypass into silenced automation | | A home carries two `trigger-framework:` lines, or a shipped id with a locator | the home resolves nothing and is named; a shipped id keeps its shipped document and the stray locator is reported | | `vendor-paths:` is absent and a directory is plainly third-party | grade it as the project's, since nothing declares otherwise, and note the directory with its evidence in the Summary — a note rather than a finding, because no rule requires the key | @@ -848,3 +872,11 @@ answers". - fixed 2026-09-08 — the review-surface paragraph still read "out of the Apex classes the run already covers" and named one framework document, lagging the per-file resolution this wave had already adopted; license: the Steps section states that resolution runs per file and the deviations section records the decision; the paragraph now selects out of the grading universe, loads each resolved framework's document, and says that a monorepo running two frameworks grades every class under the framework resolved for its own path. The reviewer noted the lag in one line without grading it and left it to the integrity audit; the propagation gate returned CLEAN over it - hit dismissed 2026-09-08 — round one's `hit fixed` line says the parts table row now reads "`.trigger` plus the handler glob the resolved framework document supplies", while the row reads "handler selection by the signature the resolved framework document supplies"; counter: the ledger is chronological, and round two's own `fixed` line records replacing that glob with a signature and demoting the glob to an optional hint — a round-one line describing the round-one state is correct history, and the gate's brief was at fault for asking whether every historical line matches the current text - signal 2026-09-08 — the stop signal is conditional on the held finding: under option (a) round 4 does not repay its cost, the Minor leftovers being self-fixable and the noted lag already owed to the integrity audit at the consumption gate; under option (b) one diff-scoped round over the base-class document's contract earns its cost, that being a reshaped mechanism again. The Minor leftovers are worth a single fix wave, not a round of their own + +### 2026-09-08 — architect, fable 5.1, concerns (round 4, diff-scoped) + +- fixed 2026-09-08 — [Important] The closure-candidate boundary makes the parent-absent row either unreachable or universal: a class whose parent cannot be read cannot be shown to reach the set, so the row never fires; read loosely, every `extends` of an absent type qualifies, and the shipped example's own `OrderProcessingException extends Exception` puts every conforming project in the Summary on every run; license: the mechanism discipline requires every test to reduce to a grep, which a definition by what a class might become does not, and the shipped example at `salesforce-apex/reference/trigger-handler.md:177` proves the loose reading's cost; a candidate is now defined by what its header carries — matches no pattern, header carries `extends` — and unreadable parents are reported in aggregate on one ungraded Summary line of distinct parents with counts. The reviewer's sharper alternative, naming parents by namespace prefix, was declined: it would rest on an unverified claim about Apex name resolution, and the aggregate line needs none +- fixed 2026-09-08 — [Minor] The namespace-prefix allowance lives only in the signature paragraph, so the literal base-class fingerprint misses `extends acme.TriggerHandler` and an undeclared managed-package project falls to step (c), which asks whether it is frameworkless or on an unknown framework when neither is true; license: the same section calls a fingerprint tolerant and gives recognition as its job; every fingerprint pattern now tolerates an optional namespace prefix, stated on the fingerprint's own grounds rather than borrowed from the signature +- fixed 2026-09-08 — [Minor] The discriminator's second answer is written in review vocabulary only, leaving the authoring surface — which loads the same document before the first trigger edit — with no instruction; this is the class round 3 fixed elsewhere; license: round 3's own fix for this class, recorded under that round's heading; the paragraph now says an authoring session has no Summary to write in, so it states both semantics and says the installed package decides which holds +- fixed 2026-09-08 — [Minor] The base-class document's owed answer sits under the Fingerprints subsection instead of the section that enumerates what a framework document answers, so an author reading that document's interface misses it; license: that section's stated job is what a framework document answers, and its fifth item is already scoped to the documents this plugin ships; the duty moved beside question 3 and Fingerprints keeps a pointer +- signal 2026-09-08 — a round 5 over these fixes does not repay its cost: none of the four reshapes a mechanism, each being a definition tightened or a clause added and checkable by the session against the cited lines, and the whole-document debt is already routed to the integrity audit at the consumption gate. One conditional: taking the reviewer's shape (ii) for the Important finding — naming parents by namespace prefix — would give the document an unverified language claim, which deserves a provenance note rather than a round. The leftovers are worth one fix wave, then the gate From 491168434d4b0cbf02eefcb2ce9bb45e02a851ff Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 18:10:15 +0200 Subject: [PATCH 013/120] docs: record why the namespace-prefix test was declined --- .../2026-09-08-salesforce-trigger-frameworks-design.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index 72281f9..f7942bf 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -300,7 +300,11 @@ platform type, `extends Exception` first — the shipped example's own (`salesforce-apex/reference/trigger-handler.md:177`). Unreadable parents are therefore reported in aggregate: one Summary line naming the distinct parents with a count each. Nothing about the line is graded, and a -namespace-qualified parent is the one worth a second look. +namespace-qualified parent is the one worth a second look — as a reading +hint, never as the test. Making it the test was the sharper alternative +and was declined: it would rest on an unverified claim about how Apex +resolves an unqualified type name absent from the repository, and the +aggregate line rests on no claim at all. The closure reads parents from the repository even where a parent lies outside a diff-scoped run, so a full run and a diff-scoped run select the From f4beab4ccdedff88246c36f459bc07a43a22b419 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 18:17:24 +0200 Subject: [PATCH 014/120] docs: fix round-five leftovers in the trigger-framework spec --- ...08-salesforce-trigger-frameworks-design.md | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index f7942bf..e420db2 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -3,7 +3,7 @@ ticket: none date: 2026-09-08 status: draft grilled: 2026-09-08 -architect: concerns +architect: LGTM branch: feature/trigger-frameworks base: develop --- @@ -286,7 +286,7 @@ BaseTriggerHandler` — an org's own layer over the framework's — matches no pattern and goes ungraded. The boundary between the two is worth one sentence: a class matching the signature is a handler on that match alone, and the parent rules below govern **closure candidates** — a class -matching no pattern whose header carries `extends`. Otherwise +matching no signature whose header carries `extends`. Otherwise `OrderHandler extends acme.TriggerHandler`, a framework delivered as a managed package, would be admitted by the namespace prefix and rejected by the same paragraph. @@ -302,9 +302,12 @@ are therefore reported in aggregate: one Summary line naming the distinct parents with a count each. Nothing about the line is graded, and a namespace-qualified parent is the one worth a second look — as a reading hint, never as the test. Making it the test was the sharper alternative -and was declined: it would rest on an unverified claim about how Apex -resolves an unqualified type name absent from the repository, and the -aggregate line rests on no claim at all. +and was declined: it assumes an unqualified parent absent from the +repository is a platform type, which fails wherever the repository is a +partial view of the org — unretrieved unpackaged metadata, or a +neighbouring package in the org's own namespace, where the prefix is +optional. There the absent parent is the project's own base class, which +that test would skip in silence and the aggregate line names outright. The closure reads parents from the repository even where a parent lies outside a diff-scoped run, so a full run and a diff-scoped run select the @@ -562,12 +565,15 @@ directory, which is exactly when resolution runs. | dispatcher | `TriggerDispatcher.Run(`, or a handler implementing an interface with `IsDisabled()` | | frameworkless | no pattern is not a fingerprint — go to step (c) | -Every pattern tolerates an optional namespace prefix before a type name, -for the fingerprint's own reason rather than the signature's: a -fingerprint exists to recognise, and a framework delivered as a package -is exactly the case a literal `extends TriggerHandler` would miss, -sending a project that plainly has a framework to the question that asks -whether it has one. +Every pattern tolerates an optional namespace prefix, in the form the +token's kind takes: `ns.` before a class name, `ns__` before an object or +Custom Metadata API name. One form would recognise half of the +metadata-driven row, whose tokens are Custom Metadata API names rather +than classes. The allowance is made for the fingerprint's own reason +rather than the signature's: a fingerprint exists to recognise, and a +framework delivered as a package is exactly the case a literal +`extends TriggerHandler` would miss, sending a project that plainly has a +framework to the question asking whether it has one. Only the first three rows carry shipped ids. `fflib`, `TDTM` and `dispatcher` are recognition labels: they name what the pattern found so @@ -828,6 +834,12 @@ answers". branch `feature/trigger-frameworks`, without the issue number the repository convention prescribes. A ticket may still be opened before the pull request. +- **Whether the TDTM fingerprint keys on the subscriber's own code.** + The row is `TDTM_Config_API.run(`. Round 5 noted, without grading it, + that in a subscriber repository this call sits inside NPSP's packaged + triggers rather than the project's code, and that the subscriber-side + signal is more likely `extends npsp.TDTM_Runnable`. The row predates the + namespace allowance and nobody has verified it against an NPSP org. - **Whether an undeclared vendor directory should be graded.** Today it earns a note in the Summary. A minor rule — the analogue of `trigger-framework-declared` — would grade it, and the only argument @@ -884,3 +896,10 @@ answers". - fixed 2026-09-08 — [Minor] The discriminator's second answer is written in review vocabulary only, leaving the authoring surface — which loads the same document before the first trigger edit — with no instruction; this is the class round 3 fixed elsewhere; license: round 3's own fix for this class, recorded under that round's heading; the paragraph now says an authoring session has no Summary to write in, so it states both semantics and says the installed package decides which holds - fixed 2026-09-08 — [Minor] The base-class document's owed answer sits under the Fingerprints subsection instead of the section that enumerates what a framework document answers, so an author reading that document's interface misses it; license: that section's stated job is what a framework document answers, and its fifth item is already scoped to the documents this plugin ships; the duty moved beside question 3 and Fingerprints keeps a pointer - signal 2026-09-08 — a round 5 over these fixes does not repay its cost: none of the four reshapes a mechanism, each being a definition tightened or a clause added and checkable by the session against the cited lines, and the whole-document debt is already routed to the integrity audit at the consumption gate. One conditional: taking the reviewer's shape (ii) for the Important finding — naming parents by namespace prefix — would give the document an unverified language claim, which deserves a provenance note rather than a round. The leftovers are worth one fix wave, then the gate + +### 2026-09-08 — architect, fable 5.1, LGTM (round 5, diff-scoped) + +- fixed 2026-09-08 — [Minor] The closure-candidate definition reads "matching no pattern" where the test is the signature; "pattern" is the Fingerprints table's column name, and the spec itself states that conflating signature with fingerprint cost two rounds; license: that statement, in the paragraph separating a signature from a fingerprint; the definition now reads "matching no signature" +- fixed 2026-09-08 — [Minor] The namespace clause names one prefix form, `ns.` before a class name, while the metadata-driven row's tokens are Custom Metadata API names, which a package prefixes `ns__`; a literal reading of the clause recognises half of that fingerprint and sends the project to step (c); license: the clause's own stated reason is that a fingerprint exists to recognise, which half a match defeats; the clause now gives the form per token kind, `ns.` before a class and `ns__` before an object or Custom Metadata API name. The `ns__` form is the reviewer's expert knowledge, uncited like the case-insensitivity claim the Evidence section already flags +- fixed 2026-09-08 — [Minor] The recorded rationale for declining the namespace-prefix test calls its premise unverified, where the premise is false for a repository that is a partial view of the org — an unqualified absent parent can be the project's own base class, which that test would skip silently and the aggregate line names outright; license: the session's own decision in that paragraph, which the reviewer strengthened rather than contested; the rationale now names the partial-view case instead of calling the premise unverified, which closes a door "unverified" left open +- signal 2026-09-08 — a further round buys nothing: all three leftovers are lexical or clarifying, each licensed by the document itself, and fit one fix wave without a round. This LGTM is diff-scoped, so the whole-document debt belongs to the integrity audit at the consumption gate, or to a confirming full-document round should the developer take that arm of the pair, never to another diff-scoped round From c5657800102dedd4d81bea5587eeda843e76711d Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 18:28:57 +0200 Subject: [PATCH 015/120] docs: state what happens to each touched shipped rule --- .../2026-09-08-salesforce-trigger-frameworks-design.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index e420db2..341f513 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -691,8 +691,9 @@ rule needing a handler set too. ## Changes to shipped content -Three rules change owner, and the spec names each because a moved id -breaks a citation: +Three shipped rules are touched, and the spec names each because a moved +id breaks a citation — one changes owner, one keeps its id and loses a +name from its scope, one stays put and is cited from the new skill: 1. "One trigger per object" leaves `apex-layering` for `trigger-one-per-object`. @@ -902,4 +903,5 @@ answers". - fixed 2026-09-08 — [Minor] The closure-candidate definition reads "matching no pattern" where the test is the signature; "pattern" is the Fingerprints table's column name, and the spec itself states that conflating signature with fingerprint cost two rounds; license: that statement, in the paragraph separating a signature from a fingerprint; the definition now reads "matching no signature" - fixed 2026-09-08 — [Minor] The namespace clause names one prefix form, `ns.` before a class name, while the metadata-driven row's tokens are Custom Metadata API names, which a package prefixes `ns__`; a literal reading of the clause recognises half of that fingerprint and sends the project to step (c); license: the clause's own stated reason is that a fingerprint exists to recognise, which half a match defeats; the clause now gives the form per token kind, `ns.` before a class and `ns__` before an object or Custom Metadata API name. The `ns__` form is the reviewer's expert knowledge, uncited like the case-insensitivity claim the Evidence section already flags - fixed 2026-09-08 — [Minor] The recorded rationale for declining the namespace-prefix test calls its premise unverified, where the premise is false for a repository that is a partial view of the org — an unqualified absent parent can be the project's own base class, which that test would skip silently and the aggregate line names outright; license: the session's own decision in that paragraph, which the reviewer strengthened rather than contested; the rationale now names the partial-view case instead of calling the premise unverified, which closes a door "unverified" left open +- hit fixed 2026-09-08 — "Three rules change owner" over-claims for two of its three items: `apex-naming` keeps its id in `salesforce-apex` and only loses `TriggerHandler` from its scope, and `apex-bulkification` explicitly stays put; the sentence now says three shipped rules are touched and names what happens to each. The defect predates every wave of this loop and survived five architect rounds and four gate episodes - signal 2026-09-08 — a further round buys nothing: all three leftovers are lexical or clarifying, each licensed by the document itself, and fit one fix wave without a round. This LGTM is diff-scoped, so the whole-document debt belongs to the integrity audit at the consumption gate, or to a confirming full-document round should the developer take that arm of the pair, never to another diff-scoped round From f626b87efd3b1b417934ae98ed758523ecddb81e Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 18:47:52 +0200 Subject: [PATCH 016/120] docs: apply the integrity audit's dispositions and stamp the audit --- ...08-salesforce-trigger-frameworks-design.md | 143 +++++++++++++----- 1 file changed, 109 insertions(+), 34 deletions(-) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index 341f513..6056d62 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -4,6 +4,7 @@ date: 2026-09-08 status: draft grilled: 2026-09-08 architect: LGTM +integrity: 2026-09-08 (sha: 366b99f) branch: feature/trigger-frameworks base: develop --- @@ -62,8 +63,10 @@ them. - **The declaration lives in the project**, keyed so a grep finds it. - **Per-framework guidance ships as `reference/` documents**, not as separate skills. -- **Resolution is per trigger file.** The search walks up from the file to - the nearest declaration. +- **Resolution is per file.** Every declaration whose scope covers the + file is collected and ranked. The nearest does not simply win: a rule + scopes by glob rather than by position, and unrankable declarations have + no winner at all. - **The migration case is deferred.** The first release serves one framework per path plus vendor exclusions. - **No enforcement hook.** The protocol is advisory. @@ -72,13 +75,14 @@ them. | Part | Owns | Deliberately excludes | |---|---|---| -| `salesforce-triggers` (new skill) | framework-agnostic rules; the resolution protocol; the `framework-id` → path table; the questions a framework document answers; fingerprints; the vendor-exclusion scope; `reference/choosing-a-framework.md` | trigger or handler code; bypass API names; the name of the dispatching class | -| `reference/framework-base-class.md`, `-metadata-driven.md`, `-frameworkless.md` | dispatch shape; the handler signature and the framework's own types; where the handler reads context; bypass and recursion API with its error semantics; test-isolation idiom; gotchas; framework-specific rules; two example units | layers below the handler; bulkification; test-class structure | +| `salesforce-triggers` (new skill) | framework-agnostic rules; the resolution protocol; the `framework-id` → path table; the questions a framework document answers; the fingerprint table, whose shipped rows come from the documents' fifth answer; the vendor-exclusion scope; `reference/choosing-a-framework.md` | trigger or handler code; bypass API names; the name of the dispatching class | +| `reference/framework-base-class.md`, `-metadata-driven.md`, `-frameworkless.md` | dispatch shape; the handler signature and the framework's own types; its fingerprint row; where the handler reads context; bypass and recursion API with its error semantics; test-isolation idiom; gotchas; framework-specific rules; two example units | layers below the handler; bulkification; test-class structure | | `salesforce-apex` | naming, layers, bulkification, governor limits, sharing, error handling; the four framework-independent example units | the trigger section, reduced to a layer-table row pointing at `salesforce-triggers` | | `salesforce-apex-testing` | test structure, factory, assertions, mocks | how to disable a handler in a test | -| `salesforce-code-review`, `salesforce-code-reviewer` | a resolution step before grading `.trigger`, then handler selection by the signature the resolved framework document supplies | asking the developer — a background agent cannot | +| `salesforce-code-review` | a resolution step before grading Apex, then handler selection by the signature the resolved framework document supplies | asking the developer — a background agent cannot | +| `salesforce-code-reviewer` | nothing — the skill owns the step, so the agent's card is unchanged | — | | `/salesforce-review` | resolution before dispatch, passed in the prompt | — | -| `rules/salesforce-toolchain.md` | a routing line for `salesforce-triggers`; the documented declaration key | the declaration itself, which belongs to the project | +| `rules/salesforce-toolchain.md` | a routing line for `salesforce-triggers`; both documented declaration keys | the declaration itself, which belongs to the project | `salesforce-triggers` must reach a session on its own `description:`, because `salesforce-standards` declares no dependencies and supports a @@ -160,7 +164,10 @@ handler dispatched on `Trigger.operationType`, where the trigger reads it. **Naming.** A trigger is named `Trigger`. Pre-existing non-conforming names stay; the convention binds new code. The dispatching -class is named by the framework document, not here. +class is named by the framework document, not here — and each shipped +document carries that name as a rule of its own, at minor severity and +with the same stance on pre-existing names, so the convention leaves this +skill without leaving the standard. (id: `trigger-naming`; severity: minor; source: this standard) @@ -216,9 +223,10 @@ answer is stated to the developer, never filled from another framework. limit is exceeded. 4. **Test isolation** — how to call the handler without DML, and how to disable it in a test. -5. **Fingerprint** — what identifies this framework in code. Required for - the documents shipped here, since step (b) of the protocol depends on - it. +5. **Fingerprint** — what identifies this framework in code, and so the + row this document contributes to the skill's table. Every shipped + document answers; the frameworkless answer is that nothing identifies + it, which is why step (b) falls through to (c) rather than matching. Question 3 earns its place: `setMaxLoopCount(1)` is a correct guard under one base-class framework and silences automation under another. Names @@ -273,7 +281,11 @@ interfaces, a class implementing several, and a class implementing something else besides. `member:` matches a method declaration line inside the type, which is where the frameworkless answer, `handle(System.TriggerOperation`, belongs and where a header match would -never look. Matching ignores case, because Apex does, and an optional +never look. A header runs from the `class` or `interface` keyword to the +opening brace however the line breaks fall, so a header split over lines +is still one header, and the modifiers before that keyword — `abstract`, +`virtual`, `global` — sit outside the match and never have to be +enumerated. Matching ignores case, because Apex does, and an optional namespace prefix is allowed before every type name, because a framework delivered as a managed package appears in project code as `extends acme.TriggerHandler`. A document may add a filename glob as a hint, never @@ -316,7 +328,9 @@ never graded; a parent absent from the repository leaves a closure candidate a non-handler, counted on that one Summary line. Only top-level types are handlers — a metadata-driven action is instantiated by name and `Outer.Inner` is addressable, so that document names the deviation rather -than leaving it to a reader. +than leaving it to a reader. Naming it is all it does: the grammar has no +key for an exception, so such an action goes ungraded and the document +says so, which is the honest end of a rule that reduces to a grep. `Framework types:` names what the framework itself owns — a copied base class, a dispatcher. An absent field and an empty one say the same thing, @@ -366,6 +380,15 @@ declaration carries a `doc-path:` or a `skill:` locator, and the table never grows to accommodate one — that is the open class working as intended. +A `doc-path:` is read as a file, relative to the repository root. A +`skill:` is invoked by its `plugin:skill` name, which is how the platform +addresses a skill and needs no path the project would have to know; a +skill whose plugin is not installed takes the missing-document row below, +since a locator naming nothing and a path naming nothing fail the same +way. This is not the slash-command invocation the design rejected: that +question was whether *our* per-framework guidance should be skills, and +loading a skill the project points at is a different act. + ### Three mechanisms, three shapes The protocol reads three things from outside itself, and they are not one @@ -384,7 +407,10 @@ grammar; what it applies over and where in the order; what it returns, including the value that means *unresolved*; who consumes that value and in what words; and what makes a document carrying it conformant, tested when the document loads. One constraint crosses all three: every test -reduces to a grep or a prefix comparison, never to parsing. A monorepo +reduces to a grep, a prefix comparison or a glob match, never to parsing. +The glob is the weakest of the three, the platform naming no dialect for +it, which is why a pattern whose coverage cannot be settled counts as +covering rather than being evaluated harder. A monorepo holds thousands of Apex classes and the reader is a model with file tools. ### The declaration @@ -400,8 +426,11 @@ trigger-framework: acme-dispatcher; skill: acme-plugin:acme-triggers vendor-paths: force-app/nebula, force-app/vendor ``` -One key at the start of a line, one declaration per home, one value per -key. The value is everything after the key, locator included: two homes +The key sits in the file's body, at the start of a line — not in a rule's +frontmatter, whose keys belong to the platform's schema — and a line +inside a fenced code block does not count, which is what lets this spec +quote the key without declaring one. One declaration per home, one value +per key. The value is everything after the key, locator included: two homes naming one framework through different documents disagree, and ranking treats them as it treats any two values. A shipped id carrying a locator is a contradiction: the shipped document is used and the locator @@ -523,7 +552,7 @@ produced. | (0) | read `vendor-paths:` from the root homes and subtract it, with the directories outside `packageDirectories`, from the repository's Apex files — what remains is the grading universe every later step works over, whether the caller is a review run or a session about to write a trigger | | (a) | grep the covering homes for `trigger-framework:` and rank them — an explicit read, never a wait for context | | (b) | fingerprint the grading universe, reading a framework's own types outside it where the Fingerprint answer points at one of the types `Framework types:` names | -| (c) | ask the developer, with `reference/choosing-a-framework.md` | +| (c) | ask the developer, with `reference/choosing-a-framework.md` — the case for and against each of the three shipped approaches, plus a fourth entry sending a recognition label or an unknown framework to the homegrown-document path rather than to a comparison this plugin cannot write | | (d) | select the resolved framework's handlers from the grading universe by its signature, closed over `extends` | Steps (0) through (c) serve both surfaces. Step (d) belongs to the review @@ -600,7 +629,10 @@ beside the rest of that document's error-semantics duty. Two layers, not one: directories outside `packageDirectories`, and the paths `vendor-paths:` declares. The first is read from -`sfdx-project.json`; the second must be declared, because a package +`sfdx-project.json`, and contributes nothing where that file is absent — +a repository in metadata-API format has no package directories, so its +universe is every Apex file the run covers minus `vendor-paths:`; the +second must be declared, because a package installed unlocked and without a namespace and vendored as source sits in a package directory with nothing marking it foreign. A logging package distributed that way puts its classes in the org's own namespace beside @@ -614,9 +646,10 @@ states where recognition reaches into a vendor path and why it must. ### The resolution record -Three lines, before the first trigger edit. The first carries the +Up to three lines, before the first trigger edit. The first carries the framework, the source and the document loaded; the other two carry what -the filter and the selector actually did: +the filter and the selector actually did, and the third belongs to the +review surface alone: ``` Trigger framework: base-class — declared in force-app/billing/triggers/CLAUDE.md — loading framework-base-class.md @@ -624,6 +657,19 @@ Vendor paths: 2 entries from CLAUDE.md Handlers: 7 selected by the signature in framework-base-class.md ``` +The first line's source names every file that declares, not one: where +homes agree it lists them all, and where a nearer declaration beat a +repository-wide default it names both with the winner first, since the +disagreement is always stated. Resolution being per file, a monorepo +yields one record per resolved framework with the paths it covers, and +that is what `/salesforce-review` passes in the prompt; an agent finding +the record and the declaration disagree reports the mismatch and grades +by the declaration, the files being the authority. Where the resolved +framework's document turns on a discriminator its own question 3 names — +the base-class case — the variant is read when the document loads and +named on this first line, because it decides which of two error semantics +that document's rules carry. + The last two lines exist because an agent that cannot ask must at least say which filter and which selector it applied. Without them, "no framework findings" and "framework rules not graded" read identically. @@ -644,7 +690,7 @@ reads the files again. | No declaration, fingerprint matches | infer, cite the file and pattern, offer to write the declaration | | No declaration, no fingerprint | go to (c), saying no pattern matched in N triggers and asking whether the project is frameworkless or on a framework this plugin does not know | | No triggers at all | go to (c) | -| A fingerprint matches a framework this plugin ships nothing for — fflib, TDTM, a dispatcher | the id is sound and only the document is missing, so take the homegrown row below: offer to write a project document, never substitute another framework's | +| A fingerprint matches a framework this plugin ships nothing for — fflib, TDTM, a dispatcher | the label is sound and only the document is missing, so take the homegrown row below: offer to write a project document, never substitute another framework's | | Two subtree-scoped declarations disagree | the inner one wins, and the disagreement is always stated — never resolved silently | | Two declarations cover the file with different values and neither is a subtree containing the other — a glob-scoped rule against a `CLAUDE.md`, two rules whose `paths:` differ, or `CLAUDE.md` beside `.claude/CLAUDE.md` in one directory | no winner: name every file that declares, then work out with the developer which home survives and write that change. Picking one silently would answer a question only the project can, and answering it in conversation would leave the collision to recur next session | | The resolved document answers question 1 without a signature, or with one that does not fit the grammar | skip the framework rules **and** `trigger-context-below-handler`, the one framework-independent rule needing a handler set; grade the rest and name the document in the Summary — a document that cannot say what its handlers are cannot have them graded, and guessing a signature would repeat the mistake a filename pattern already made | @@ -659,8 +705,11 @@ reads the files again. ## Review surface -`salesforce-code-review` gains a step: for `.trigger` files — the one -artefact every framework has — load `salesforce-triggers` and run the +`salesforce-code-review` gains a step, triggered by any Apex file in the +run — not a `.trigger` alone, since framework rules grade handler classes +and a change touching only those needs the same resolution. A `.trigger` +is merely the one artefact every framework has. The step loads +`salesforce-triggers` and runs the protocol's steps in order, (0) through (b), then load each resolved framework's document by path or name and take step (d), selecting that framework's handler classes out of the grading universe by the signature @@ -670,13 +719,17 @@ class under the framework resolved for its own path. Which classes the framework rules grade is resolved, never hardcoded, because only the framework knows what makes a class one of its handlers. -A background agent enforces standards and asks nothing. -`/salesforce-review` therefore resolves before dispatch and passes the -record in the prompt, as it already passes the directory mode; the agent -verifies the record against the declaration, which costs nothing. -Dispatched any other way, it grades what the framework-independent rules -cover and reports the rest as unresolved — choosing a framework for the -project is as far outside its job as asking for one. +A background agent enforces standards and asks nothing, which bounds the +protocol rather than the agent. Steps (0), (a) and (b) are mechanical +reads it performs like any other; step (c) is the one it cannot take, and +so is the offer to write a declaration that follows an inference. +`/salesforce-review` resolves before dispatch and passes the record in +the prompt, as it already passes the directory mode, which spares the +agent that work and lets it verify the record against the declaration for +nothing. Dispatched any other way it resolves for itself, and where +resolution would need step (c) it reports the framework as unresolved — +choosing a framework for the project is as far outside its job as asking +for one. When resolution fails, the run grades the framework-independent rules, skips framework rules rather than guessing them, notes `trigger framework: unresolved — @@ -684,7 +737,11 @@ framework-specific rules not graded` among the Summary's out-of-scope notes, and reports the declaration rule in the `## Project` section — `trigger-framework-declared` where nothing declares the path, `trigger-framework-declared.ambiguous` where two the protocol cannot rank -do. Both slots already exist in the review-report contract. Where +do. Both slots already exist in the review-report contract, whose Summary is +"outcome, out-of-scope notes, and (for a rerun) the prior findings' +disposition" (`review-reports.md:117-118`) — so every note this design +adds, the aggregated unreadable parents and the two `vendor-paths:` notes +included, is an out-of-scope note rather than a new kind of content. Where resolution succeeds and the document supplies no signature, the same note names `trigger-context-below-handler` beside the framework rules, that rule needing a handler set too. @@ -723,6 +780,13 @@ document carries `OrderTrigger` and `OrderTriggerHandler` and calls the same lower-layer methods, so the three examples are demonstrably one scenario. +Those four units stay in one file, and the file is renamed — +`order-layers.md` — because `trigger-handler.md` would name a trigger and +a handler it no longer carries, which is the kind of thing this standard +exists to stop. The skill's pointer moves with it and nothing outside the +skill cites the filename; this spec's own citation of the old name +predates the rename and is left as the evidence it was. + ## Evidence and its limits Framework behaviour was read from source, not from documentation. Two @@ -734,8 +798,9 @@ findings correct the documentation: line `// Do not throw an exception if we exceed the loop count - just stop executing`. -Two citations carry weaker provenance, recorded so a later reader can -judge them: +Three provenance notes are recorded so a later reader can judge them — +two citations weaker than the rest, and one claim carrying no citation at +all: - Apex Developer Guide quotations come from the official PDF, fetched with TLS verification disabled after the HTML pages returned 403. @@ -782,7 +847,7 @@ Two further deviations: A second consultation, on the three mechanisms above, followed two blocking rounds that diagnosed one repeated failure. Its proposals are -adopted, three of them as choices rather than repairs: +adopted but one, three of them as choices rather than repairs: - **`vendor-paths:` carries directory prefixes, not globs.** Prefixes compose with `packageDirectories` in one arithmetic and leave no dialect @@ -898,10 +963,20 @@ answers". - fixed 2026-09-08 — [Minor] The base-class document's owed answer sits under the Fingerprints subsection instead of the section that enumerates what a framework document answers, so an author reading that document's interface misses it; license: that section's stated job is what a framework document answers, and its fifth item is already scoped to the documents this plugin ships; the duty moved beside question 3 and Fingerprints keeps a pointer - signal 2026-09-08 — a round 5 over these fixes does not repay its cost: none of the four reshapes a mechanism, each being a definition tightened or a clause added and checkable by the session against the cited lines, and the whole-document debt is already routed to the integrity audit at the consumption gate. One conditional: taking the reviewer's shape (ii) for the Important finding — naming parents by namespace prefix — would give the document an unverified language claim, which deserves a provenance note rather than a round. The leftovers are worth one fix wave, then the gate -### 2026-09-08 — architect, fable 5.1, LGTM (round 5, diff-scoped) +### 2026-09-08 — architect, fable 5.1, LGTM (round 5, diff-scoped), debt discharged 2026-09-08 - fixed 2026-09-08 — [Minor] The closure-candidate definition reads "matching no pattern" where the test is the signature; "pattern" is the Fingerprints table's column name, and the spec itself states that conflating signature with fingerprint cost two rounds; license: that statement, in the paragraph separating a signature from a fingerprint; the definition now reads "matching no signature" - fixed 2026-09-08 — [Minor] The namespace clause names one prefix form, `ns.` before a class name, while the metadata-driven row's tokens are Custom Metadata API names, which a package prefixes `ns__`; a literal reading of the clause recognises half of that fingerprint and sends the project to step (c); license: the clause's own stated reason is that a fingerprint exists to recognise, which half a match defeats; the clause now gives the form per token kind, `ns.` before a class and `ns__` before an object or Custom Metadata API name. The `ns__` form is the reviewer's expert knowledge, uncited like the case-insensitivity claim the Evidence section already flags - fixed 2026-09-08 — [Minor] The recorded rationale for declining the namespace-prefix test calls its premise unverified, where the premise is false for a repository that is a partial view of the org — an unqualified absent parent can be the project's own base class, which that test would skip silently and the aggregate line names outright; license: the session's own decision in that paragraph, which the reviewer strengthened rather than contested; the rationale now names the partial-view case instead of calling the premise unverified, which closes a door "unverified" left open - hit fixed 2026-09-08 — "Three rules change owner" over-claims for two of its three items: `apex-naming` keeps its id in `salesforce-apex` and only loses `TriggerHandler` from its scope, and `apex-bulkification` explicitly stays put; the sentence now says three shipped rules are touched and names what happens to each. The defect predates every wave of this loop and survived five architect rounds and four gate episodes +- fixed 2026-09-08 — integrity audit: question 5 is required of every shipped document while the frameworkless row states that no pattern is a fingerprint, so one of the three shipped documents has nothing to answer with; license: that row; question 5 now says the frameworkless answer is that nothing identifies it, which is why step (b) falls through +- fixed 2026-09-08 — integrity audit: the parts table gives fingerprints to the skill and omits them from what a framework document owns, while question 5 and step (b) make them the document's answer; license: both statements stand and only their division was missing; the skill owns the table and the document contributes its row, and both table rows now say so +- fixed 2026-09-08 — integrity audit: a background agent is told to run steps (0) through (b), which includes inference, and also that choosing a framework is outside its job, while a bare dispatch was said to report everything unresolved; license: "a background agent enforces standards and asks nothing", which bounds the protocol rather than the agent; the paragraph now says (0), (a) and (b) are mechanical reads it performs, and step (c) plus the offer to write a declaration are what it cannot take +- fixed 2026-09-08 — integrity audit: a failure row calls `fflib`, `TDTM` and `dispatcher` ids where the Fingerprints paragraph states a label becomes an id only when something declares it; license: that paragraph; the row now reads "the label is sound" +- fixed 2026-09-08 — integrity audit: the resolution record opens as three lines written before the first trigger edit while the same section says an authoring session's record stops at the document; license: the later sentence, written in round 3's wave; the record now reads "up to three lines" with the third belonging to the review surface alone +- fixed 2026-09-08 — integrity audit: a Settled decision still reads "the search walks up from the file to the nearest declaration", superseded by collect-and-rank, where the nearest does not simply win and a glob-scoped rule is not ranked at all; license: the ranking section and the rulings recorded for it; the decision now states collect-and-rank and says the nearest does not simply win. The line survived five architect rounds because no diff ever touched it +- fixed 2026-09-08 — integrity audit: "every test reduces to a grep or a prefix comparison" is broken by the glob coverage test, which the document itself calls unsettleable on an unnamed dialect; license: the document's own glob fallback; the constraint now names three mechanical tests and says the glob is the weakest, which is why an unsettleable pattern counts as covering +- fixed 2026-09-08 — integrity audit: the deviations section says the second consultation's proposals are adopted and two paragraphs later declines one; license: the declining paragraph; the sentence now reads "adopted but one" +- fixed 2026-09-08 — integrity audit: "Two citations carry weaker provenance" introduces three bullets, the third being an uncited claim rather than a weak citation; license: the list itself; the sentence now reads three provenance notes, two weak citations and one claim carrying no citation. Four propagation gates re-derived counters over this sentence and none caught it +- fixed 2026-09-08 — integrity audit, sixteen ranked implementer questions: fourteen are answered in the spec — where the key sits and what a grep must not match, how a `doc-path:` and a `skill:` locator are followed, what the record carries beyond the simplest case, what triggers the review step, the header grammar's edges and the inner-class deviation's honest end, the dispatching class keeping a rule in each shipped document, the universe where `sfdx-project.json` is absent, what `choosing-a-framework.md` covers, both keys in the toolchain rule and the reviewer agent's card being unchanged, and the review-report Summary slot these notes take (`review-reports.md:117-118`). One is a design call the session took and named: the four remaining example units stay in one file, renamed `order-layers.md`, because the old name would promise a trigger and a handler the file no longer carries. One remains the developer's — whether the unverified TDTM fingerprint row ships as it stands — and it was already the open question the audit ranked as its only blocker - signal 2026-09-08 — a further round buys nothing: all three leftovers are lexical or clarifying, each licensed by the document itself, and fit one fix wave without a round. This LGTM is diff-scoped, so the whole-document debt belongs to the integrity audit at the consumption gate, or to a confirming full-document round should the developer take that arm of the pair, never to another diff-scoped round From 4d4b1b4044800a3603bf8779ba2f79d7b3b7e86e Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 18:57:21 +0200 Subject: [PATCH 017/120] docs: ship the TDTM fingerprint row with its caveat stated --- ...08-salesforce-trigger-frameworks-design.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index 6056d62..d789e3a 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -4,7 +4,7 @@ date: 2026-09-08 status: draft grilled: 2026-09-08 architect: LGTM -integrity: 2026-09-08 (sha: 366b99f) +integrity: 2026-09-08 (sha: 715798b) branch: feature/trigger-frameworks base: develop --- @@ -610,6 +610,16 @@ the session can say it out loud, and they are products or families rather than approaches. A label becomes an id only when something declares it, and then it is the project's word, not this table's. +The TDTM row ships unverified, and says so here rather than hiding it. In +a subscriber repository `TDTM_Config_API.run(` may sit inside NPSP's +packaged triggers rather than the project's own code, in which case the +subscriber-side signal is `extends npsp.TDTM_Runnable` instead; nobody has +checked either against a real NPSP org. Being wrong costs little and costs +it in one direction: TDTM is a recognition label rather than a shipped id, +so a miss drops to step (c), where the session asks — never a confident +wrong answer. The packaged triggers also sit under a vendor path, outside +the universe step (b) reads. + The metadata-driven fingerprint keys on Custom Metadata records and the trigger body, never on the framework's classes: the records sit in the repository whether the framework arrives as an unlocked package or as @@ -900,12 +910,6 @@ answers". branch `feature/trigger-frameworks`, without the issue number the repository convention prescribes. A ticket may still be opened before the pull request. -- **Whether the TDTM fingerprint keys on the subscriber's own code.** - The row is `TDTM_Config_API.run(`. Round 5 noted, without grading it, - that in a subscriber repository this call sits inside NPSP's packaged - triggers rather than the project's code, and that the subscriber-side - signal is more likely `extends npsp.TDTM_Runnable`. The row predates the - namespace allowance and nobody has verified it against an NPSP org. - **Whether an undeclared vendor directory should be graded.** Today it earns a note in the Summary. A minor rule — the analogue of `trigger-framework-declared` — would grade it, and the only argument @@ -978,5 +982,6 @@ answers". - fixed 2026-09-08 — integrity audit: "every test reduces to a grep or a prefix comparison" is broken by the glob coverage test, which the document itself calls unsettleable on an unnamed dialect; license: the document's own glob fallback; the constraint now names three mechanical tests and says the glob is the weakest, which is why an unsettleable pattern counts as covering - fixed 2026-09-08 — integrity audit: the deviations section says the second consultation's proposals are adopted and two paragraphs later declines one; license: the declining paragraph; the sentence now reads "adopted but one" - fixed 2026-09-08 — integrity audit: "Two citations carry weaker provenance" introduces three bullets, the third being an uncited claim rather than a weak citation; license: the list itself; the sentence now reads three provenance notes, two weak citations and one claim carrying no citation. Four propagation gates re-derived counters over this sentence and none caught it +- fixed 2026-09-08 — integrity audit's sixteenth question, its only blocker: whether the unverified TDTM fingerprint row ships as it stands; ruling: 2026-09-08; the row ships and the Fingerprints section now carries the caveat where the row lives — the exposure is one-directional, TDTM being a recognition label rather than a shipped id, so a miss drops to step (c) and asks instead of answering wrongly, and NPSP's packaged triggers sit under a vendor path outside the universe step (b) reads - fixed 2026-09-08 — integrity audit, sixteen ranked implementer questions: fourteen are answered in the spec — where the key sits and what a grep must not match, how a `doc-path:` and a `skill:` locator are followed, what the record carries beyond the simplest case, what triggers the review step, the header grammar's edges and the inner-class deviation's honest end, the dispatching class keeping a rule in each shipped document, the universe where `sfdx-project.json` is absent, what `choosing-a-framework.md` covers, both keys in the toolchain rule and the reviewer agent's card being unchanged, and the review-report Summary slot these notes take (`review-reports.md:117-118`). One is a design call the session took and named: the four remaining example units stay in one file, renamed `order-layers.md`, because the old name would promise a trigger and a handler the file no longer carries. One remains the developer's — whether the unverified TDTM fingerprint row ships as it stands — and it was already the open question the audit ranked as its only blocker - signal 2026-09-08 — a further round buys nothing: all three leftovers are lexical or clarifying, each licensed by the document itself, and fit one fix wave without a round. This LGTM is diff-scoped, so the whole-document debt belongs to the integrity audit at the consumption gate, or to a confirming full-document round should the developer take that arm of the pair, never to another diff-scoped round From 3eabad4bb6dd343495663807095def618e736842 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 19:42:59 +0200 Subject: [PATCH 018/120] docs: add the trigger-frameworks implementation plan with adversary round one applied --- ...026-09-08-salesforce-trigger-frameworks.md | 2005 +++++++++++++++++ 1 file changed, 2005 insertions(+) create mode 100644 docs/plans/2026-09-08-salesforce-trigger-frameworks.md diff --git a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md new file mode 100644 index 0000000..ef25588 --- /dev/null +++ b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md @@ -0,0 +1,2005 @@ +--- +ticket: none +date: 2026-09-08 +status: draft +adversary: blocking +spec: ../specs/2026-09-08-salesforce-trigger-frameworks-design.md +branch: feature/trigger-frameworks +base: develop +--- + +# Salesforce Trigger Frameworks Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship the `salesforce-triggers` skill — framework-independent trigger rules, the framework-resolution protocol, and three per-framework documents — and move trigger content out of `salesforce-apex` so the plugin serves base-class, metadata-driven and frameworkless projects, plus any framework a project documents itself. + +**Architecture:** One new skill directory holds a hub `SKILL.md` and four `reference/` documents. The hub owns the rules and the protocol; each framework document answers the five questions the hub asks and carries its own signature, fingerprint row and rules. Four shipped surfaces change: `salesforce-apex` loses its trigger section and its worked example shrinks to the four framework-independent units, `salesforce-code-review` gains a resolution step, `/salesforce-review` resolves before dispatch, and the toolchain rule routes to the new skill and documents the two declaration keys. The reviewer agent's card is untouched. + +**Tech Stack:** Markdown skill files with rule tags in the repo's tag grammar; Apex examples in fenced blocks that no compiler here checks; `grep`, `rg` and `awk` as the verification mechanism; `claude plugin validate` as the structural check. + +## Global Constraints + +- **The spec is the source.** Where this plan and `docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md` disagree, the spec wins and the plan is wrong — except where the *Deviations from the spec* section below records a departure and its reason. +- **Rule tags follow `.claude/rules/standards-rule-tags.md`:** `(id: \`\`; severity: critical|important|minor[; kind: defect|hardening]; source: )`, `kind` present exactly at critical, sub-rules under a `Sub-rules:` list with absolute severities never equal to the group default, no severity roll-up section anywhere. Cross-skill mentions cite the owning id and carry no severity word. +- **Rule ids:** `trigger-` in the hub, `trigger-base-class-`, `trigger-metadata-driven-` and `trigger-frameworkless-` in the framework documents. Findings cite `(standard: salesforce-triggers, rule: )` for every one of them — a `reference/` document has no standard of its own. +- **The declaration keys never appear at the start of a line outside a fenced code block in any shipped file.** A line starting `trigger-framework:` or `vendor-paths:` in a shipped rule or skill would be read as a declaration by the very protocol it documents. Inline mentions sit in backticks mid-sentence; examples sit in fenced blocks. Task 11 sweeps for it. +- **Frontmatter safety:** no `description:` contains `: ` (colon-space); the two new descriptions are checked for it in Task 1 and Task 11. +- **No version bump.** `plugins/salesforce-standards/.claude-plugin/plugin.json` stays at `0.3.1`; the release PR mints the number, and this topic does not dogfood. +- **No eval files.** Neither `skill-creator` nor any agent writes `evals/trigger-evals.json` or any other eval file under the plugin — a standing decision of this repo. `skill-creator` may be used for `description:` tuning only. +- **Apex examples are valid, copy-pasteable Apex** for the file named above them, in the style of the existing `reference/` files: four-space indent, one class per block, explicit sharing keyword. No compiler runs here, so every block is written once into this plan and copied verbatim into the file — a fix to a shipped block is back-ported into this plan in the same wave. +- **Every framework document calls the same four lower-layer methods** the renamed `order-layers.md` defines: `OrderDomain.applyDefaults(List)`, `OrderDomain.validateStatusTransitions(List, Map)`, `OrderDomain.filterNewlyActivated(List, Map)` returning `List`, and `OrderService.activateFulfillment(List)`. Nothing else is called from a handler. +- **`claude plugin validate .` and `claude plugin validate plugins/salesforce-standards` must both pass** after every task. +- **Public-repo hygiene:** English only, no machine-specific paths, no company or client names, in files and commit subjects alike. +- **Commits are one line** — a conventional-commit subject, no body, no trailers of any kind. +- **Prose wraps at about 72 characters**, as the existing skills do; table rows, fenced code and tag lines run long. +- **Checks:** a check that searches for a prose phrase first normalizes whitespace — `tr -s '[:space:]' ' ' < "$file" | grep -o '' | wc -l` — so a phrase straddling a line break is counted the same as one on a single line, with no judgement about where the wrap falls. A check asserting absence uses `grep -c` when it expects a printed `0` and `rg -l` when it expects no output — `rg -c` never prints `0`. Every check that verifies an edit states a before-value and an after-value that differ; a check that asserts an invariant says so on the step. Before-values below were measured on the tree at `develop` (`98de97e`) on 2026-09-08, which is the state the implementation branch starts from. +- **Branches.** Implementation runs on `feature/trigger-frameworks`, cut from `develop`. This plan and its spec live on `feature/trigger-frameworks.docs` and join the topic branch at the implementation-ready gate; the project has no `CLAUDE.md` note choosing fast-forward or squash, so the session asks once at that gate. + +## Deviations from the spec + +Recorded here and beside the text they concern, so a reviewer trips over the reason where the disagreement lives. + +1. **The metadata-driven signature reads `header: TriggerAction.`, not `header: implements TriggerAction.`** The spec's example says `implements TriggerAction.` "matches … a class implementing something else besides", which holds only when the framework interface comes first: `implements Queueable, TriggerAction.BeforeInsert` does not contain that substring. The spec's own constraint — every test reduces to a grep — decides it. The shorter pattern matches every header naming a `TriggerAction.*` interface wherever it sits. Task 5 carries the sentence. +2. **One citation of the renamed file lives outside `salesforce-apex`.** The spec says "nothing outside the skill cites the filename"; `salesforce-apex-testing/reference/test-patterns.cls:4` does. Task 7 updates it, and the spec's sentence is reported as a spec defect rather than silently outrun — the plan corrects the file, not the claim. +3. **Context methods are overridden `public override`**, the form the fork's README shows, where the base class declares them `protected virtual`. Both compile; the spec prescribes neither. +4. **A `Framework types:` entry leaves the handler set even where its header matches the signature.** The spec says such types are "graded by no framework rule" and, separately, that "a class matching the signature is a handler on that match alone", without ordering the two. The plan orders them — the exemption wins — because `MetadataTriggerHandler` implements the framework's own seven interfaces and would otherwise be graded as an action wherever the framework is vendored as source outside `vendor-paths:`. The first spec sentence licenses the order; the spec's silence on it is reported. Task 1 carries the sentence and Task 5 names the instance. +5. **The hub does not say "as a minor rule".** The spec's Naming rule reads "each shipped document carries that name as a rule of its own, at minor severity"; a shipped surface restating a grade outside the tag is what `.claude/rules/standards-rule-tags.md` forbids, so the hub says "as a rule of its own" and the three documents' tags carry the grade. Not a spec defect — the tag rule binds plugin content, not design documents — but the phrase does not travel. + +## File structure + +Created: + +- `plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md` — the hub: description, the five framework-independent rules, the framework-document contract and signature grammar, the resolution protocol. +- `plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-frameworkless.md` +- `plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-base-class.md` +- `plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-metadata-driven.md` +- `plugins/salesforce-standards/skills/salesforce-triggers/reference/choosing-a-framework.md` — the step (c) comparison. + +Modified: + +- `plugins/salesforce-standards/skills/salesforce-apex/SKILL.md` — description, intro, naming table, layers table, example pointer. +- `plugins/salesforce-standards/skills/salesforce-apex/reference/trigger-handler.md` → renamed `order-layers.md`, two sections removed. +- `plugins/salesforce-standards/skills/salesforce-apex/reference/bulkification.md:5` and `plugins/salesforce-standards/skills/salesforce-apex-testing/reference/test-patterns.cls:4` — the filename citation. +- `plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md` — Procedure step 2 gains the resolution step. +- `plugins/salesforce-standards/commands/salesforce-review.md` — a resolution step before dispatch. +- `plugins/salesforce-standards/rules/salesforce-toolchain.md` — routing line and the two keys. +- `plugins/salesforce-standards/README.md`, `plugins/salesforce-standards/.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `README.md` — the identity surfaces, in one commit. + +Unchanged on purpose: `agents/salesforce-code-reviewer.md` (the skill owns the step; the card's "step 5" reference is why Task 8 adds to step 2 rather than renumbering). `skills/salesforce-apex-testing/SKILL.md` changes one sentence only, in Task 7: its layering pointer still routes the handler to `salesforce-apex`. + +--- + +### Task 1: The hub skill — rules and the framework-document contract + +**Files:** +- Create: `plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md` + +**Interfaces:** +- Produces: the seven rule ids `trigger-one-per-object`, `trigger-one-per-object.disjoint-contexts`, `trigger-body-delegates`, `trigger-context-below-handler`, `trigger-naming`, `trigger-framework-declared`, `trigger-framework-declared.ambiguous`; the `framework-id` table; the `Signature:` grammar with `header:`, `member:` and `Framework types:`; the heading `## Framework documents`, which Task 2 anchors on. +- Consumes: the four lower-layer method names from Global Constraints, by reference to `order-layers.md`, which Task 7 creates. + +- [ ] **Step 1: Confirm the directory does not exist** + +```bash +ls plugins/salesforce-standards/skills/salesforce-triggers 2>&1 +``` + +Expected: `ls: cannot access ...: No such file or directory`. + +- [ ] **Step 2: Write the file** + +````markdown +--- +name: salesforce-triggers +description: Use when writing or reviewing Apex triggers — one trigger per object, what belongs in the trigger body, resolving which trigger framework the project uses, and loading the matching framework guidance. Apex classes, layering and bulkification belong to salesforce-apex; Apex tests to salesforce-apex-testing. +--- + +# Salesforce triggers + +Standards for Apex triggers that hold whatever trigger framework a +project uses, and the protocol that resolves which framework governs +the file at hand. Cite rules in review findings as +`(standard: salesforce-triggers, rule: )`, the rules of a framework +document included; each rule names its source. Framework-specific +guidance — dispatch shape, bypass API, test isolation — lives in a +per-framework document under `reference/`, loaded once the framework is +resolved. Before the first trigger edit, and before grading the first +Apex file, run the protocol under "Resolving the framework" and write +the resolution record. + +The layers below the handler — domain, service, selector — and +bulkification belong to `salesforce-apex`; Apex tests to +`salesforce-apex-testing`. + +## Rules + +These rules hold under every framework. Their ids carry the `trigger-` +prefix; a framework document's rules carry `trigger--`. + +### One trigger per object + +An object has one `.trigger` file. Several handler calls inside that +file are correct, in the order they must run; the violation is a second +trigger file, never a second line. Scope: triggers under the +directories `packageDirectories` lists and outside `vendor-paths:` — +stated by path, because a vendor's trigger on a standard object belongs +to the vendor by path and to nobody by ownership. + +(id: `trigger-one-per-object`; severity: critical; kind: defect; source: +Salesforce Well-Architected, record-triggered decision guide; Apex +Developer Guide p. 282) + +Sub-rules: +- the two triggers declare disjoint contexts (id: + `trigger-one-per-object.disjoint-contexts`; severity: important) + +Two triggers whose declared contexts overlap fire in an order the +platform leaves undefined, so the same code yields different results +between runs. Disjoint contexts carry no ordering risk today, yet they +split an object's automation across files and sit one context +declaration away from the critical case. + +### The trigger body delegates + +The body contains delegating calls and nothing else — no conditional, +loop or field logic. + +(id: `trigger-body-delegates`; severity: important; source: Apex +Developer Guide, "Document Your Apex Code") + +### Context stops at the handler + +Nothing below the handler reads `Trigger.*`; context descends as +parameters, which keeps the logic testable without DML. The types a +framework document lists under `Framework types:` are exempt — a base +class reading `Trigger.*` is the framework doing its job. This rule +needs a handler set, so it goes ungraded wherever no signature supplies +one. + +(id: `trigger-context-below-handler`; severity: important; source: this +standard) + +### Naming + +A trigger is named `Trigger`. Pre-existing non-conforming names +stay; the convention binds new code. The dispatching class is named by +the framework document, which carries that name as a rule of its own. + +(id: `trigger-naming`; severity: minor; source: this standard) + +### The framework is declared + +The project declares its trigger framework — the declaration under +"Resolving the framework". The finding lands in the report's +`## Project` section, and lands there even when inference succeeded: +inference resolves a session, a declaration resolves the project. + +(id: `trigger-framework-declared`; severity: minor; source: this +standard) + +Sub-rules: +- two declarations cover one path with different values and neither is + a subtree strictly containing the other, so neither wins (id: + `trigger-framework-declared.ambiguous`; severity: important) + +The group default grades an absent declaration, which announces itself. +The sub-rule grades every case the protocol cannot rank — a glob-scoped +rule against a subtree-scoped `CLAUDE.md`, two rules whose `paths:` +differ, `CLAUDE.md` beside `.claude/CLAUDE.md` in one directory. A root +default with a nearer subtree override is not this finding: the inner +one wins, and the disagreement is stated in the resolution record. +Declarations that agree never reach the rule. The two dispositions are +mutually exclusive: a path has no declaration, or it has two the +protocol cannot rank. + +### Bulkification + +Owned by `salesforce-apex`: `apex-bulkification` and +`apex-bulkification.loop-on-trigger-path` grade the trigger path, and +this skill carries no grade of its own for it. + +## Framework documents + +One framework id names each shipped framework everywhere it appears — +declaration value, fingerprint row, document filename, rule prefix. A +shipped id names an approach, never a product: two base-class +frameworks share one document and one prefix. + +| `framework-id` | Document | Rule prefix | +|---|---|---| +| `base-class` | `reference/framework-base-class.md` | `trigger-base-class-` | +| `metadata-driven` | `reference/framework-metadata-driven.md` | `trigger-metadata-driven-` | +| `frameworkless` | `reference/framework-frameworkless.md` | `trigger-frameworkless-` | + +An id absent from this table is a framework this plugin does not ship. +Its declaration carries a `doc-path:` or a `skill:` locator, and this +table never grows to hold one. A `doc-path:` is read as a file relative +to the repository root; a `skill:` is invoked by its `plugin:skill` +name. A locator naming nothing takes the missing-document row of the +failure table. A document from outside this plugin is graded on the +rules above alone, never on its own framework rules; a signature +written in the grammar below buys it `trigger-context-below-handler`. + +### What a document answers + +Four questions, and a fifth for the documents this plugin ships. A +missing answer is stated to the developer, never filled from another +framework. + +1. **Dispatch** — the trigger body and the handler shape, as two files + calling the same lower-layer methods `salesforce-apex`'s + `reference/order-layers.md` defines, plus the **signature** that + makes a class one of this framework's handlers and the framework's + **own types**, which are neither handlers nor layers below one. +2. **Context access** — where the handler reads `Trigger.*`. +3. **Bypass and recursion** — the API names *and* what happens when a + limit is exceeded. `setMaxLoopCount(1)` guards under one base-class + framework and silences automation under another; names alone cannot + tell a reviewer which. +4. **Test isolation** — how to call the handler without DML, and how to + disable it in a test. +5. **Fingerprint** — what identifies this framework in code: the row + the document contributes to the fingerprint table below. The + frameworkless answer is that nothing identifies it. + +A document may add framework-specific rules, tagged like any rule here +under its prefix, and each shipped document carries the name of its +dispatching class as a rule of its own. A document carrying no rules is +graded by the rules above alone, and the report says so. + +### The signature + +The answer to question 1 is a block under a fixed heading, so a +session loading a document written elsewhere can tell whether the +contract is met — the base-class answer in full: + +``` +Signature: + header: extends TriggerHandler +Framework types: TriggerHandler +``` + +- `header:` matches the type's declaration header — from the `class` + or `interface` keyword to the opening brace, however the lines break. + Modifiers before the keyword (`abstract`, `virtual`, `global`) sit + outside the match. A comment, a string or a javadoc mention never + counts. +- `member:` matches a method declaration line inside the type — where + `handle(System.TriggerOperation`, the frameworkless answer, belongs. +- Matching ignores case, because Apex does. An optional namespace + prefix is allowed before every type name: `extends acme.TriggerHandler` + matches `extends TriggerHandler`. +- `Framework types:` names what the framework itself owns — a copied + base class, a dispatcher. Absent and empty say the same thing: + nothing. Its types are read to recognise the framework, graded by no + framework rule, and exempt from `trigger-context-below-handler`. A + type named here leaves the handler set even where its own header + matches the signature: a framework class implementing the framework's + own interfaces is the framework, not a handler. +- A filename glob may be added as a hint, never as the test. +- Only a malformed `Signature:` block fails the grammar. + +A signature and a fingerprint share a technique and nothing else. A +fingerprint reads the whole project, matches anywhere, and yields a +label. A signature reads one class, matches in a named place, and +yields membership. + +**The handler set** is the smallest fixed point of the classes matching +the signature together with the classes whose header `extends` a type +already in the set — so `OrderHandler extends BaseTriggerHandler`, an +org's own layer over the framework's, is graded. A class matching the +signature is a handler on that match alone. The parent rules govern +**closure candidates** only — a class matching no signature whose header +carries `extends`: + +- the parent is read from the repository even where it lies outside a + diff-scoped run, so a full run and a diff-scoped run select the same + handlers; +- a parent under a vendor path is read to recognise it and never + graded; +- a parent absent from the repository leaves the candidate a + non-handler. Such parents are reported in aggregate: one Summary line + naming the distinct parents with a count each, ungraded. Most are + platform types, `extends Exception` first. A namespace-qualified + parent is the one worth a second look — as a reading hint, never as + the test, since an unqualified absent parent can be the project's own + base class where the repository is a partial view of the org. + +Only top-level types are handlers. A document whose framework can +dispatch an inner type says so; such a type goes ungraded, and the +document says that too. + +Every test in this skill reduces to a grep, a prefix comparison or a +glob match — never to parsing. +```` + +- [ ] **Step 3: Check the file's shape** + +```bash +f=plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md +grep -c '(id:' "$f" # rule tags — no trailing space: a sub-rule tag wraps after `(id:` +grep -c 'severity: critical' "$f" # the one critical rule +grep -c 'kind: defect' "$f" # kind present exactly there +grep -c '^description: .*: ' "$f" # frontmatter safety +grep -c '^## Framework documents$' "$f" # Task 2's anchor +grep -c '^## Resolving the framework$' "$f" +``` + +Expected: `7`, `1`, `1`, `0`, `1`, `0`. The last is `0` here and `1` after Task 2; the fourth is an invariant and stays `0`. + +- [ ] **Step 4: Validate** + +```bash +claude plugin validate . && claude plugin validate plugins/salesforce-standards +``` + +Expected: both pass — the new skill has a `name:` matching its directory and a quoted-safe description. + +- [ ] **Step 5: Commit** + +```bash +git add plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md +git commit -m "feat(salesforce-standards): add the salesforce-triggers skill with its framework-independent rules" +``` + +--- + +### Task 2: The hub skill — the resolution protocol + +**Files:** +- Modify: `plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md` — insert a `## Resolving the framework` section directly before `## Framework documents` + +**Interfaces:** +- Consumes: the heading `## Framework documents` from Task 1. +- Produces: the step letters (0), (a), (b), (c), (d); the fingerprint table; the resolution record's three-line shape; the failure table. Tasks 3–5 contribute rows to the fingerprint table by reference; Tasks 8 and 9 cite the step letters and the record. + +- [ ] **Step 1: Measure before** + +```bash +f=plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md +grep -c '^## ' "$f" +grep -c '^### ' "$f" +awk '/^```/{f=!f;next} !f' "$f" | grep -c '^trigger-framework:\|^vendor-paths:' +``` + +Expected: `2`, `8`, `0`. The first counts `## Rules` and `## Framework documents`; the second the six rule headings plus `### What a document answers` and `### The signature`; the third strips fenced blocks and counts declaration keys at line start outside them — an invariant that stays `0`. + +- [ ] **Step 2: Insert the section** + +Find the line `## Framework documents` and insert the following before it, followed by one blank line: + +````markdown +## Resolving the framework + +Three things are read from outside this skill, and they are not one +mechanism. A **signature** selects over a class's content and comes +from the framework document. A **declaration** resolves over a path, +comes from the project in several places, and carries a scope. +**`vendor-paths:`** filters over a path, comes from the repository root, +and carries no scope. Each is specified on its own; none inherits a +neighbour's rules. + +### The declaration + +One line, one key, in the body of a home file at the start of a line: + +``` +trigger-framework: base-class +trigger-framework: metadata-driven +trigger-framework: frameworkless +trigger-framework: acme-dispatcher; doc-path: docs/acme-triggers.md +trigger-framework: acme-dispatcher; skill: acme-plugin:acme-triggers +vendor-paths: force-app/nebula, force-app/vendor +``` + +- The key sits in the file's body, never in a rule's frontmatter, and a + line inside a fenced code block does not count — which is how this + file quotes the key without declaring one. +- One declaration per home, one value per key. The value is everything + after the key, locator included: two homes naming one framework + through different documents disagree. +- A shipped id carrying a locator is a contradiction: the shipped + document is used and the locator reported. An id absent from the + table below carrying no locator is a homegrown framework without a + document. +- Two `trigger-framework:` lines in one home make that home + unreadable; it is reported with the file, never resolved by picking a + line. + +**Homes** — the exact set a grep reads: `CLAUDE.md` and +`.claude/CLAUDE.md` at the repository root or in any directory above +the file, plus any rule file under `.claude/rules/` that sits outside a +directory carrying a Rules-engine manifest (`.manifest.json`). Grep +these files rather than waiting for the platform to load them: a +`CLAUDE.md` in a subdirectory loads only when a file there is read, +which a session about to write its first trigger has not done. + +Not homes: a personal rule under `~/.claude/rules/`, which would follow +the developer between projects that disagree; `CLAUDE.local.md`, +personal and git-ignored, so a declaration there resolves the author's +sessions while every teammate and CI run reports it missing; and the +installed `salesforce-toolchain.md` copy, which the Rules engine owns +and overwrites at its next sync. + +**Scope.** A `CLAUDE.md` scopes by position — the subtree of its +directory, and for `.claude/CLAUDE.md` the subtree of the directory +holding `.claude/`. A rule declaring `paths:` scopes by glob; a rule +declaring none scopes the whole repository, which is the root subtree +and ranks as one. Where a glob's coverage cannot be settled, the +pattern counts as covering: apply the matching the platform applies and +lean on `**`, `*`, `{a,b}` and root-relative paths alone. + +**Ranking**, in three moves, every declaration keeping its own scope: + +1. Count the distinct values. A root `CLAUDE.md` and a repository-wide + rule naming the same framework agree; the record names both. +2. One distinct value — resolved. +3. More than one — the winner is the declaration whose scope is + strictly contained in the scope of every declaration carrying a + different value: subtree within subtree by path prefix, never a pair + involving a glob. No such declaration means no winner, which the + failure table turns into behaviour. `CLAUDE.md` beside + `.claude/CLAUDE.md` in one directory carry equal subtrees, so + neither contains the other; two rules with differing `paths:` are + the same case. + +### `vendor-paths:` + +The second key, and the only other thing read from the project. It +lists what belongs to a package the project did not write, and the +scope is **declared, never inferred**: a package installed unlocked and +without a namespace, vendored as source, carries no platform signal +that marks it foreign. + +- It resolves at the **repository root** alone. Every root home + carries it — both root `CLAUDE.md` files and every rule file outside + a payload directory, whatever its `paths:` — and several homes union. + A scope rule would get an exclusion backwards. +- Its value is a list of **directory prefixes** relative to the root, + not globs: a file is vendor when its path equals an entry or begins + with the entry and a slash. That is the shape `packageDirectories` + uses, so the two exclusion layers compose in one arithmetic. +- An absent key and an empty one say the same thing: nothing is vendor. + An entry matching nothing on disk earns a note, the only signal an + agent can give for a typo. An entry outside every package directory + is redundant and passes in silence. +- Vendor code is never graded and never fingerprinted, and it is still + **read** where recognition needs it: the discriminator separating the + two base-class frameworks is a method name inside + `TriggerHandler.cls`, which a project vendoring the framework as + source puts under a vendor path. + +### Steps + +Resolution runs per file. For a trigger it decides which framework's +rules grade that trigger; for a class, the same from the declarations +covering the class's own path. Each step consumes what the one before +it produced. + +| Step | Action | +|---|---| +| (0) | read `vendor-paths:` from the root homes and subtract it, with the directories outside `packageDirectories` (read from `sfdx-project.json`; where that file is absent this layer excludes nothing), from the repository's Apex files — what remains is the grading universe | +| (a) | grep the covering homes for `trigger-framework:` and rank them | +| (b) | fingerprint the grading universe, reading a framework's own types outside it where a fingerprint row points at a type `Framework types:` names | +| (c) | ask the developer, with `reference/choosing-a-framework.md` | +| (d) | select the resolved framework's handlers from the grading universe by its signature, closed over `extends` — the review surface alone | + +Steps (0) through (c) serve a session writing a trigger and a review +run alike. A session about to write a trigger stops at the resolved +document; a review run takes step (d). + +**Who repairs what.** Step (c) and a collision the protocol cannot rank +both need a person, so both belong to an interactive session. A +background agent asks nothing: it performs (0), (a) and (b) as +mechanical reads, enforces the rules it can resolve, and reports what +it cannot — step (c) and the offer to write a declaration are what it +never takes. A session does not settle an ambiguity in conversation +either, where the answer dies at the next compaction: it rescopes or +removes one of the competing homes until the declaration surface states +one framework per path. The fix is an edit, not an answer. + +### Fingerprints + +| Framework | Pattern | +|---|---| +| base-class | `extends TriggerHandler` with `new X().run()` | +| metadata-driven | `new MetadataTriggerHandler().run()` with `Trigger_Action__mdt` and `sObject_Trigger_Setting__mdt` records | +| fflib | `fflib_SObjectDomain.triggerHandler(` in the trigger body | +| TDTM | `TDTM_Config_API.run(` | +| dispatcher | `TriggerDispatcher.Run(`, or a handler implementing an interface with `IsDisabled()` | +| frameworkless | no pattern is not a fingerprint — go to step (c) | + +Every pattern tolerates an optional namespace prefix in the form the +token's kind takes: `ns.` before a class name, `ns__` before an object +or Custom Metadata API name. Only the first three rows carry shipped +ids; `fflib`, `TDTM` and `dispatcher` are recognition labels naming what +was found, and a label becomes an id only when a declaration carries +it. The TDTM row is unverified against a real NPSP org: the pattern may +sit inside the package's own triggers under a vendor path, and the +subscriber-side signal may be `extends npsp.TDTM_Runnable`. A miss there +drops to step (c) and asks. + +The base-class document separates two frameworks identical in the +trigger body and different in error semantics; its discriminator is a +method name inside `TriggerHandler.cls`, read when that document loads. + +### The resolution record + +Up to three lines, before the first trigger edit or the first graded +file: + +``` +Trigger framework: base-class — declared in force-app/billing/triggers/CLAUDE.md — loading framework-base-class.md +Vendor paths: 2 entries from CLAUDE.md +Handlers: 7 selected by the signature in framework-base-class.md +``` + +- The source reads `declared `, `inferred ` or `asked`. + It names every file that declares: where homes agree, all of them; + where a nearer declaration beat a repository-wide default, both with + the winner first. Where the document turns on a discriminator — the + base-class case — the variant is named on this line. +- Any source other than `declared` ends with an offer to write the + declaration, since a line in conversation dies at the next + compaction. This skill never remembers a resolution; after compaction + it reads the files again. +- The second and third lines say what the filter and the selector did, + degrading in place — `none declared`, or the reason no handler set + was selected. The `Handlers:` line appears only where step (d) ran. + Without them "no framework findings" and "framework rules not graded" + read identically. +- A monorepo yields one record per resolved framework with the paths it + covers. + +### Failure modes + +| Situation | Behaviour | +|---|---| +| No declaration, fingerprint matches | infer, cite the file and pattern, offer to write the declaration | +| No declaration, no fingerprint | go to (c), saying no pattern matched in N triggers and asking whether the project is frameworkless or on a framework this plugin does not know | +| No triggers at all | go to (c) | +| A fingerprint matches a framework this plugin ships nothing for — fflib, TDTM, a dispatcher | the label is sound and only the document is missing: offer to write a project document, never substitute another framework's | +| Two subtree-scoped declarations disagree | the inner one wins, and the disagreement is always stated — never resolved silently | +| Two declarations cover the file with different values and neither is a subtree containing the other | no winner: name every file that declares, then work out with the developer which home survives and write that change — picking one silently would answer a question only the project can | +| The resolved document answers question 1 without a signature, or with one outside the grammar | skip the framework rules and `trigger-context-below-handler`, grade the rest, and name the document in the Summary — guessing a signature would repeat the mistake a filename pattern makes | +| A class matches two frameworks' signatures | grade it under the framework resolved for its own path — step (a) over the class — and name the other in the Summary | +| A closure candidate's `extends` parent is not in the repository | treat the class as a non-handler and count it on the Summary's one line of distinct unreadable parents; a class matching the signature itself is unaffected | +| The base-class framework arrives as a managed package, so `TriggerHandler.cls` cannot be read | select handlers as usual, grade every rule except those keyed to error semantics, and name in the Summary that the discriminator was unreadable — a guess between silencing and throwing is the one guess that turns a correct bypass into silenced automation | +| A home carries two `trigger-framework:` lines, or a shipped id with a locator | the home resolves nothing and is named; a shipped id keeps its shipped document and the stray locator is reported | +| `vendor-paths:` is absent and a directory is plainly third-party | grade it as the project's, since nothing declares otherwise, and note the directory with its evidence in the Summary — a note, not a finding, because no rule requires the key | +| A `vendor-paths:` entry matches nothing on disk | note it; nothing distinguishes a typo from a directory yet to be added | +| The declared document is missing | say so and grade by the framework-independent rules; never substitute another framework's document, since guidance for the wrong framework writes code that does not compile | +| A homegrown framework with no document | offer to write one, describing their code rather than a pattern from the internet | + +Every note above lands among the Summary's out-of-scope notes, the slot +the review-report contract already defines; the declaration findings +land in `## Project`. +```` + +- [ ] **Step 3: Measure after** + +```bash +f=plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md +grep -c '^## ' "$f" +grep -c '^### ' "$f" +grep -c '^## Resolving the framework$' "$f" +awk '/^```/{f=!f;next} !f' "$f" | grep -c '^trigger-framework:\|^vendor-paths:' +grep -c '^| (' "$f" # step rows +grep -c '^| No declaration' "$f" +``` + +Expected: `3`, `14`, `1`, `0`, `5`, `2`. The section adds six `###` headings — `The declaration`, `` `vendor-paths:` ``, `Steps`, `Fingerprints`, `The resolution record`, `Failure modes` — to Step 1's eight. The declaration-key count is the invariant from Step 1: the six example lines sit inside a fence, so stripping fences removes them, and the sentence "Two `trigger-framework:` lines" starts with a word. + +- [ ] **Step 4: Check the section order** + +```bash +grep -n '^## ' plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md +``` + +Expected: three lines, in this order — `## Rules`, `## Resolving the framework`, `## Framework documents`. + +- [ ] **Step 5: Validate** + +```bash +claude plugin validate . && claude plugin validate plugins/salesforce-standards +``` + +Expected: both pass. + +- [ ] **Step 6: Commit** + +```bash +git add plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md +git commit -m "feat(salesforce-standards): add the trigger-framework resolution protocol" +``` + +--- + +### Task 3: The frameworkless framework document + +**Files:** +- Create: `plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-frameworkless.md` + +**Interfaces:** +- Consumes: the four lower-layer methods from Global Constraints; the `Signature:` grammar from Task 1. +- Produces: the rule ids `trigger-frameworkless-dispatching-class`, `trigger-frameworkless-entry-point`, `trigger-frameworkless-recursion-guard`; the signature `member: handle(System.TriggerOperation`. + +- [ ] **Step 1: Write the file** + +````markdown +# Frameworkless triggers + +Framework id `frameworkless`. No base class and no metadata: the +trigger hands its context to one static entry point, and the handler +switches on the operation. Every rule here is cited +`(standard: salesforce-triggers, rule: trigger-frameworkless-)`. + +## Dispatch + +The trigger reads the context once and passes it on. The handler owns +the switch, so `trigger-body-delegates` holds, and it takes the +operation as a parameter, so it is callable from a test without DML. + +### `OrderTrigger.trigger` + +```apex +trigger OrderTrigger on Order (before insert, before update, after update) { + OrderTriggerHandler.handle(Trigger.operationType, Trigger.new, Trigger.oldMap); +} +``` + +### `OrderTriggerHandler.cls` + +Routes each operation to the domain and service layers of +`salesforce-apex`'s `reference/order-layers.md`; no SOQL, no DML, no +business logic of its own. `with sharing`, the default for the handler +layer. + +```apex +public with sharing class OrderTriggerHandler { + + @TestVisible + private static Boolean disabled = false; + + public static void handle(System.TriggerOperation operation, List newOrders, Map oldMap) { + if (disabled) { + return; + } + switch on operation { + when BEFORE_INSERT { + OrderDomain.applyDefaults(newOrders); + } + when BEFORE_UPDATE { + OrderDomain.validateStatusTransitions(newOrders, oldMap); + } + when AFTER_UPDATE { + List activated = OrderDomain.filterNewlyActivated(newOrders, oldMap); + if (!activated.isEmpty()) { + OrderService.activateFulfillment(activated); + } + } + } + } +} +``` + +### Signature + +``` +Signature: + member: handle(System.TriggerOperation +``` + +No `Framework types:` — nothing here belongs to a framework. A handler +is any top-level class declaring a method whose line carries +`handle(System.TriggerOperation`. + +## Context access + +The trigger reads `Trigger.operationType`, `Trigger.new` and +`Trigger.oldMap` once and passes them as parameters. The handler reads +nothing from `Trigger.*`, and neither does anything below it. On +insert `Trigger.oldMap` is null, so the `BEFORE_INSERT` branch never +touches `oldMap`. + +## Bypass and recursion + +Nothing in this shape bypasses or counts. The `disabled` flag above is +the bypass: `@TestVisible`, so a test can set it, and private, so +production code cannot. Recursion is the handler's own duty. A handler +whose after-context work performs DML that re-enters the same trigger +keeps a static set of ids already processed in this transaction and +skips them: + +```apex +private static Set activatedThisTransaction = new Set(); +``` + +What happens when a limit is exceeded: there is no limit to exceed. An +unguarded re-entry loops until a governor limit throws +`System.LimitException`, which no code catches, and the whole +transaction fails. + +## Test isolation + +Call the entry point with records built in memory — no `insert`, no +trigger context: + +```apex +List orders = new List{ new Order(Status = null) }; +OrderTriggerHandler.handle(System.TriggerOperation.BEFORE_INSERT, orders, null); +System.assertEquals('Draft', orders[0].Status); +``` + +To disable the handler in a test of other code that inserts orders, +set `OrderTriggerHandler.disabled = true` in the test and reset it in a +`finally` block. + +## Fingerprint + +Nothing identifies frameworkless code. The absence of every other +fingerprint is not a match: step (b) of the resolution protocol falls +through to step (c), which asks. + +## Rules + +**Dispatching class.** The handler is named `TriggerHandler`. +Pre-existing non-conforming names stay; the convention binds new code. + +(id: `trigger-frameworkless-dispatching-class`; severity: minor; +source: this standard) + +**One entry point.** The trigger's single call is the handler's static +`handle` method taking `System.TriggerOperation` and the records. A +trigger calling a per-context method instead — `handleBeforeInsert`, +with or without a branch around it — leaves its handler outside the +signature, where no framework rule reaches it. The rule is graded on +the trigger for that reason: the class it would grade cannot be +selected. + +(id: `trigger-frameworkless-entry-point`; severity: important; source: +this standard) + +**Recursion guard.** A handler whose after-context work performs DML on +its own object, or on an object whose automation writes back, keeps a +static processed-id set and skips ids already seen. Nothing else in +this shape stops re-entry. + +(id: `trigger-frameworkless-recursion-guard`; severity: important; +source: this standard) + +## Gotchas + +- `Trigger.new` is a `List` and is assignable to a + `List` parameter without a cast; `Trigger.oldMap` to + `Map` likewise. +- A `switch on` an enum needs no `when else`; an unlisted operation + falls through and does nothing, which is the intended behaviour for + a context the trigger does not declare. +- The `disabled` flag is per handler class. A test that inserts several + objects disables each handler it must silence. +- The shape this plugin shipped before `salesforce-triggers` existed — + static `handleBeforeInsert` methods and a trigger branching on + `Trigger.isBefore` — now earns `trigger-body-delegates` on the + trigger and matches no signature. The fix is mechanical: keep the + methods, add one `handle` entry point that switches on the operation + and calls them, and make the trigger call it. +```` + +- [ ] **Step 2: Check the file's shape** + +```bash +f=plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-frameworkless.md +grep -c '(id: `trigger-frameworkless-' "$f" +grep -c 'severity: critical' "$f" +grep -c '^Signature:$' "$f" +grep -c '^ member: \|^ header: ' "$f" +grep -c '^### `Order' "$f" +grep -c 'OrderDomain\.\|OrderService\.' "$f" +``` + +Expected: `3`, `0`, `1`, `1`, `2`, `4`. The last counts the lines calling lower-layer methods — the three domain calls and one service call inside the handler block. + +- [ ] **Step 3: Validate and commit** + +```bash +claude plugin validate . && claude plugin validate plugins/salesforce-standards +git add plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-frameworkless.md +git commit -m "feat(salesforce-standards): add the frameworkless trigger document" +``` + +Expected: validation passes; one commit. + +--- + +### Task 4: The base-class framework document + +**Files:** +- Create: `plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-base-class.md` + +**Interfaces:** +- Consumes: the four lower-layer methods; the `Signature:` grammar; the fingerprint row for `base-class` in Task 2's table. +- Produces: the rule ids `trigger-base-class-dispatching-class`, `trigger-base-class-bypass-restored`, `trigger-base-class-loop-count`; the discriminator pair `incrementCheckLoopCount` / `addToLoopCount`. + +Facts below were read from the two frameworks' sources on 2026-09-08: `kevinohara80/sfdc-trigger-framework` (`src/classes/TriggerHandler.cls`) and `dschach/salesforce-trigger-framework` (`force-app/main/default/classes/TriggerHandler.cls`, `README.md`). + +- [ ] **Step 1: Write the file** + +````markdown +# Base-class trigger framework + +Framework id `base-class`. One approach, two products: the original +`kevinohara80/sfdc-trigger-framework` and its maintained fork +`dschach/salesforce-trigger-framework`. Both are a `virtual` class +`TriggerHandler` copied into the org or installed as a package; a +handler extends it and overrides the context methods it needs. Every +rule here is cited +`(standard: salesforce-triggers, rule: trigger-base-class-)`. + +## Dispatch + +### `OrderTrigger.trigger` + +```apex +trigger OrderTrigger on Order (before insert, before update, after update) { + new OrderTriggerHandler().run(); +} +``` + +### `OrderTriggerHandler.cls` + +Casts the context once in the constructor, then routes each context to +the domain and service layers of `salesforce-apex`'s +`reference/order-layers.md`; no SOQL, no DML, no business logic of its +own. `with sharing`, the default for the handler layer. + +```apex +public with sharing class OrderTriggerHandler extends TriggerHandler { + + private List newOrders; + private Map oldMap; + + public OrderTriggerHandler() { + this.newOrders = (List) Trigger.new; + this.oldMap = (Map) Trigger.oldMap; + } + + public override void beforeInsert() { + OrderDomain.applyDefaults(newOrders); + } + + public override void beforeUpdate() { + OrderDomain.validateStatusTransitions(newOrders, oldMap); + } + + public override void afterUpdate() { + List activated = OrderDomain.filterNewlyActivated(newOrders, oldMap); + if (!activated.isEmpty()) { + OrderService.activateFulfillment(activated); + } + } +} +``` + +### Signature + +``` +Signature: + header: extends TriggerHandler +Framework types: TriggerHandler +``` + +`TriggerHandler` is the framework's own type: read to recognise the +framework, graded by no rule here, and exempt from +`trigger-context-below-handler`. + +## Context access + +The base class reads `Trigger.isExecuting` and `Trigger.operationType` +in `run()` to pick the context method — which is why it is a framework +type. The handler reads `Trigger.new` and `Trigger.oldMap` once, in its +constructor, into typed fields; the context methods and everything +below them read nothing from `Trigger.*`. + +## Bypass and recursion + +Common to both products, all static on `TriggerHandler` unless noted: + +| Call | Effect | +|---|---| +| `TriggerHandler.bypass('OrderTriggerHandler')` | skips that handler for the rest of the transaction | +| `TriggerHandler.clearBypass('OrderTriggerHandler')` | restores it | +| `TriggerHandler.isBypassed('OrderTriggerHandler')` | asks | +| `TriggerHandler.clearAllBypasses()` | clears every bypass | +| `this.setMaxLoopCount(n)`, `this.clearMaxLoopCount()` | instance methods: cap how many times this handler's `run()` executes in one transaction | + +The fork adds `bypass(Type)` and `bypass(List)` overloads, +`bypassAll()` with `clearGlobalBypass()`, `setBypass(name, Boolean)`, +`bypassList()` and `getLoopCount(name)`, and its `setMaxLoopCount` +returns the handler so calls chain. The fork's README documents +`TriggerHandler.setGlobalBypass()`, which its source does not define — +the method is `bypassAll()`. + +**What happens when the loop count is exceeded** is where the two +products part, and the difference sits in one private method of +`TriggerHandler.cls`: + +| Product | Method | Behaviour | +|---|---|---| +| original | `addToLoopCount()` | throws `TriggerHandlerException('Maximum loop count of N reached in ')`, and the DML fails | +| fork | `incrementCheckLoopCount()` | returns without running the handler; a `System.debug` line is the only trace, and the `throw` sits commented out beside `// Do not throw an exception if we exceed the loop count - just stop executing` | + +So `setMaxLoopCount(1)` is a recursion guard under the original and +silences automation under the fork. The discriminator is read from +`TriggerHandler.cls` when this document loads, and the variant goes on +the resolution record's first line. + +Where that file is unreadable — a managed package — a namespace or the +package version may still tell the two apart. Where nothing does, an +authoring session states both semantics and says the installed package +decides which holds; a review run grades every rule below except the +one keyed to error semantics, `trigger-base-class-loop-count`, and +names in the Summary that the discriminator was unreadable. Nobody +guesses between silencing and throwing. + +## Test isolation + +`run()` requires trigger context — the base class checks +`Trigger.isExecuting`, and the handler's constructor casts +`Trigger.new` — so the handler cannot be called without DML. Test the +layers below directly (`OrderDomain`, `OrderService`), and give the +handler one DML-backed test per context that asserts the outcome. + +To disable the handler in a test of other code that inserts orders: + +```apex +Account acc = TestDataFactory.createAccounts(1)[0]; +TriggerHandler.bypass('OrderTriggerHandler'); +try { + TestDataFactory.createOrders(200, acc.Id); +} finally { + TriggerHandler.clearBypass('OrderTriggerHandler'); +} +``` + +The factory inserts the records itself, as `salesforce-apex-testing` +requires, and its `createOrders` takes the parent account's id; the +bypass wraps the call that fires the trigger. + +## Fingerprint + +`extends TriggerHandler` in a class header together with +`new X().run()` in a trigger body, an optional namespace prefix before +`TriggerHandler`. The discriminator: `incrementCheckLoopCount` in +`TriggerHandler.cls` means the fork, which silences; +`addToLoopCount` means the original, which throws. + +## Rules + +**Dispatching class.** The handler is named `TriggerHandler`. +Pre-existing non-conforming names stay; the convention binds new code. + +(id: `trigger-base-class-dispatching-class`; severity: minor; source: +this standard) + +**A bypass is restored.** In non-test code every `bypass(...)` is +paired with the matching `clearBypass(...)` in a `finally` block. A +bypass left set silences the object's automation for every caller +later in the same transaction. + +(id: `trigger-base-class-bypass-restored`; severity: important; source: +this standard) + +**The loop count is not the recursion logic.** Under the fork, +`setMaxLoopCount` drops the handler's work without a signal, so +re-entry is controlled by state — a static set of processed ids — and +any `setMaxLoopCount` call carries a comment naming the silencing. +Under the original, `setMaxLoopCount` is a legitimate guard, and a +count above 1 is justified in a comment. This rule is keyed to the +error semantics above and goes ungraded where the discriminator is +unreadable. + +(id: `trigger-base-class-loop-count`; severity: important; source: this +standard) + +## Gotchas + +- The handler name a bypass string must match is derived from the + class: the fork reads `toString()` (`getHandlerName()`) unless the + constructor passed `super('OrderTriggerHandler')`. Prefer the fork's + `bypass(OrderTriggerHandler.class)` overload where it exists — a Type + cannot be misspelt. +- The context methods are `protected virtual` in the base class; + overriding them `public override` widens access and compiles under + both products, and is the form the fork's README shows. +- A handler constructed outside a trigger has null `Trigger.new`; the + casts above are safe only because `run()` refuses to execute outside + trigger context. +```` + +- [ ] **Step 2: Check the file's shape** + +```bash +f=plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-base-class.md +grep -c '(id: `trigger-base-class-' "$f" +grep -c 'severity: critical' "$f" +grep -c '^Signature:$' "$f" +grep -c '^ header: extends TriggerHandler$' "$f" +grep -c '^Framework types: TriggerHandler$' "$f" +grep -c '^### `Order' "$f" +grep -c 'incrementCheckLoopCount\|addToLoopCount' "$f" +``` + +Expected: `3`, `0`, `1`, `1`, `1`, `2`, `4`. The last counts lines naming a discriminator method: two table rows and two lines in the Fingerprint section. + +- [ ] **Step 3: Validate and commit** + +```bash +claude plugin validate . && claude plugin validate plugins/salesforce-standards +git add plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-base-class.md +git commit -m "feat(salesforce-standards): add the base-class trigger framework document" +``` + +Expected: validation passes; one commit. + +--- + +### Task 5: The metadata-driven framework document + +**Files:** +- Create: `plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-metadata-driven.md` + +**Interfaces:** +- Consumes: the four lower-layer methods; the `Signature:` grammar; the fingerprint row for `metadata-driven`. +- Produces: the rule ids `trigger-metadata-driven-dispatching-class`, `trigger-metadata-driven-action-registered`, `trigger-metadata-driven-bypass-restored`, `trigger-metadata-driven-order-explicit`; the signature `header: TriggerAction.`. + +Facts below were read from `mitchspano/apex-trigger-actions-framework` on 2026-09-08: `TriggerBase.cls`, `TriggerAction.cls`, `MetadataTriggerHandler.cls`, and the field directories of `Trigger_Action__mdt` and `sObject_Trigger_Setting__mdt`. + +- [ ] **Step 1: Write the file** + +````markdown +# Metadata-driven trigger framework + +Framework id `metadata-driven`. The reference implementation is +`mitchspano/apex-trigger-actions-framework`. One trigger per object +calls `new MetadataTriggerHandler().run()`, and Custom Metadata records +name the classes to run and their order — one +`sObject_Trigger_Setting__mdt` record per object, one +`Trigger_Action__mdt` record per action and context. An action is any +class implementing one of the `TriggerAction.*` interfaces; changing +the order is a metadata deployment, not a code one. Every rule here is +cited +`(standard: salesforce-triggers, rule: trigger-metadata-driven-)`. + +## Dispatch + +### `OrderTrigger.trigger` + +```apex +trigger OrderTrigger on Order (before insert, before update, after update) { + new MetadataTriggerHandler().run(); +} +``` + +### `OrderTriggerHandler.cls` + +One class carrying the object's three actions, each an interface +method the framework calls with `List` arguments. It routes to +the domain and service layers of `salesforce-apex`'s +`reference/order-layers.md`; no SOQL, no DML, no business logic of its +own. `with sharing`, the default for the handler layer. + +```apex +public with sharing class OrderTriggerHandler implements TriggerAction.BeforeInsert, TriggerAction.BeforeUpdate, TriggerAction.AfterUpdate { + + public void beforeInsert(List triggerNew) { + OrderDomain.applyDefaults((List) triggerNew); + } + + public void beforeUpdate(List triggerNew, List triggerOld) { + Map oldMap = new Map((List) triggerOld); + OrderDomain.validateStatusTransitions((List) triggerNew, oldMap); + } + + public void afterUpdate(List triggerNew, List triggerOld) { + Map oldMap = new Map((List) triggerOld); + List activated = OrderDomain.filterNewlyActivated((List) triggerNew, oldMap); + if (!activated.isEmpty()) { + OrderService.activateFulfillment(activated); + } + } +} +``` + +### The metadata + +Nothing in code names the class; the records do. One +`sObject_Trigger_Setting__mdt` record and three `Trigger_Action__mdt` +records deploy with the code: + +| Record | Type | Key fields | +|---|---|---| +| `Order` | `sObject_Trigger_Setting__mdt` | `Object_API_Name__c = Order` | +| `Order_Before_Insert` | `Trigger_Action__mdt` | `Apex_Class_Name__c = OrderTriggerHandler`, `Before_Insert__c = Order`, `Order__c = 1` | +| `Order_Before_Update` | `Trigger_Action__mdt` | `Apex_Class_Name__c = OrderTriggerHandler`, `Before_Update__c = Order`, `Order__c = 1` | +| `Order_After_Update` | `Trigger_Action__mdt` | `Apex_Class_Name__c = OrderTriggerHandler`, `After_Update__c = Order`, `Order__c = 1` | + +Each context field (`Before_Insert__c`, `Before_Update__c`, +`After_Insert__c`, `After_Update__c`, `Before_Delete__c`, +`After_Delete__c`, `After_Undelete__c`) is a lookup to the object's +setting record; a record enables exactly the contexts it fills. + +### Signature + +``` +Signature: + header: TriggerAction. +Framework types: TriggerBase, MetadataTriggerHandler, TriggerAction, FinalizerHandler +``` + +The pattern is `TriggerAction.` rather than `implements TriggerAction.` +on purpose: a class declared `implements Queueable, +TriggerAction.BeforeInsert` names the framework interface second, and a +header test is a substring match. Any header naming a `TriggerAction.*` +interface, wherever it sits in the `implements` list, selects the +class. `MetadataTriggerHandler` itself matches — it implements all +seven context interfaces — and sits under `Framework types:`, which +removes it from the handler set. The framework types appear in the +repository only where the framework arrives as source; listing them +costs nothing where it does not. + +## Context access + +The framework reads `Trigger.*` in `TriggerBase.run()` and hands each +action `triggerNew` and `triggerOld` as `List`. An action +reads nothing from `Trigger.*`; it casts the lists and, where it needs +the old records by id, builds `new Map((List) +triggerOld)` — the framework passes no map. + +## Bypass and recursion + +| Call | Effect | +|---|---| +| `TriggerBase.bypass('Order')`, `clearBypass`, `isBypassed` | every action on the object; `Schema.SObjectType` overloads exist | +| `TriggerBase.clearAllBypasses()` | clears every object bypass | +| `MetadataTriggerHandler.bypass('OrderTriggerHandler')`, `clearBypass`, `isBypassed` | one action, by class name or `System.Type` | +| `MetadataTriggerHandler.clearAllBypasses()` | clears every action bypass | +| `Bypass_Execution__c` on either record type | disables the object or the action without a deployment of code | +| `Bypass_Permission__c`, `Required_Permission__c` | a custom permission whose holders skip the action, or without which it does not run | + +**Recursion.** No loop count exists here. The framework counts how +often each record id has been seen in the update contexts — +`TriggerBase.idToNumberOfTimesSeenBeforeUpdate` and +`TriggerBase.idToNumberOfTimesSeenAfterUpdate`, both +`Map` — and leaves the decision to the action: one that +must act once checks that the count for the record is 1. What happens +when a limit is exceeded: nothing is exceeded. An action ignoring the +maps re-enters until a governor limit throws `System.LimitException` +and the transaction fails. + +**A stale record fails the object.** `MetadataTriggerHandler` +instantiates each action with `Type.forName(Apex_Class_Name__c)`; a +name resolving to no class, or to a class not implementing the +context's interface, throws `MetadataTriggerHandlerException` — and +every DML on that object fails until the record is fixed. + +## Test isolation + +An action is a plain class: instantiate it and call the interface +method with records built in memory, no DML and no metadata read: + +```apex +List orders = new List{ new Order(Status = null) }; +new OrderTriggerHandler().beforeInsert(orders); +System.assertEquals('Draft', orders[0].Status); +``` + +To disable the handler in a test of other code that inserts orders, +call `MetadataTriggerHandler.bypass('OrderTriggerHandler')` before the +DML and `clearBypass` in a `finally` block; `TriggerBase.bypass('Order')` +silences every action on the object the same way. + +## Fingerprint + +`new MetadataTriggerHandler().run()` in a trigger body together with +`Trigger_Action__mdt` and `sObject_Trigger_Setting__mdt` records in the +repository — an optional `ns.` before the class and `ns__` before the +metadata API names. The row keys on records and the trigger body, never +on the framework's classes: the records sit in the repository whether +the framework arrives as an unlocked package or as source, while the +classes appear only in the second case. + +## Inner classes + +`Type.forName('Outer.Inner')` resolves, so an inner class can be +registered as an action. The signature grammar selects top-level types +only; an inner-class action is therefore not graded by the rules below, +and this document says so rather than leaving it to a reader. The +convention here is a top-level class per action or per object. + +## Rules + +**Dispatching class.** A class carrying an object's actions is named +`TriggerHandler`; where actions split by feature, each is +`Action`. Pre-existing non-conforming names stay; the +convention binds new code. + +(id: `trigger-metadata-driven-dispatching-class`; severity: minor; +source: this standard) + +**Every action is registered, and every record resolves.** Each +`Trigger_Action__mdt` record's `Apex_Class_Name__c` names a class in +the repository implementing the interface of every context the record +enables, and every class implementing one of the seven context +interfaces — `BeforeInsert` through `AfterUndelete` — has a record. A +stale name fails every DML on the object at runtime; an unregistered +action never runs. A `TriggerAction.DmlFinalizer` implementer is +selected as a handler by the signature but registered through a +different record type, so this rule leaves it alone. + +(id: `trigger-metadata-driven-action-registered`; severity: important; +source: this standard) + +**A bypass is restored.** In non-test code every `TriggerBase.bypass` +or `MetadataTriggerHandler.bypass` is paired with the matching +`clearBypass` in a `finally` block. A bypass left set silences +automation for every caller later in the same transaction. + +(id: `trigger-metadata-driven-bypass-restored`; severity: important; +source: this standard) + +**Order is explicit.** Every `Trigger_Action__mdt` record carries an +`Order__c` value, unique among the records enabling the same object and +context. Two actions on one order leave their sequence to the query. + +(id: `trigger-metadata-driven-order-explicit`; severity: minor; source: +this standard) + +## Gotchas + +- `triggerOld` is a `List`, not a map; build the map in the + action. +- The Custom Metadata type and its records deploy together with the + framework; a record naming a class that is not yet deployed fails the + object's DML from the first save. +- Flow actions (`Flow_Name__c`) run through the same records and are + outside this document. +```` + +- [ ] **Step 2: Check the file's shape** + +```bash +f=plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-metadata-driven.md +grep -c '(id: `trigger-metadata-driven-' "$f" +grep -c 'severity: critical' "$f" +grep -c '^Signature:$' "$f" +grep -c '^ header: TriggerAction\.$' "$f" +grep -c '^### `Order' "$f" +grep -c '^| `Order' "$f" +``` + +Expected: `4`, `0`, `1`, `1`, `2`, `4`. The last counts the metadata table's record rows: one setting and three actions. + +- [ ] **Step 3: Validate and commit** + +```bash +claude plugin validate . && claude plugin validate plugins/salesforce-standards +git add plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-metadata-driven.md +git commit -m "feat(salesforce-standards): add the metadata-driven trigger framework document" +``` + +Expected: validation passes; one commit. + +--- + +### Task 6: The step (c) comparison + +**Files:** +- Create: `plugins/salesforce-standards/skills/salesforce-triggers/reference/choosing-a-framework.md` + +**Interfaces:** +- Consumes: the three framework ids and the recognition labels from Task 2. + +- [ ] **Step 1: Write the file** + +````markdown +# Choosing a trigger framework + +Read at step (c) of the resolution protocol, when neither a declaration +nor a fingerprint settled the framework. Present the case for and +against each shipped approach; the developer chooses; the session +writes the declaration — a `trigger-framework:` line in the root +`CLAUDE.md` or the trigger directory's — so the question is not asked +again. The developer's existing code decides more than any argument +here: a project already extending a base class has chosen. + +## Base-class (`base-class`) + +**For.** One virtual class copied into the org, no metadata to +maintain, and a handler shape every Salesforce developer recognises. +Bypass and loop-count APIs come with it. Two products share the shape, +so hiring and code review carry over. + +**Against.** Execution order within a context is the order of calls in +the handler, so reordering is a code deployment. The handler cannot be +called without DML, so handler tests are integration tests. The two +products differ in what an exceeded loop count does — one throws, one +silences — and the difference is a private method in a class the +project copied and may have edited. + +## Metadata-driven (`metadata-driven`) + +**For.** Order and bypasses live in Custom Metadata, so an admin can +reorder or disable an action without a deployment of code. An action +is a plain class implementing an interface, callable in a test without +DML. Permission-based bypasses come with it. + +**Against.** A stale metadata record fails every DML on its object. +The dispatch is invisible in code — a reader must open the records to +learn what runs — and a trigger that runs nothing looks identical to +one that runs ten actions. The framework is a dependency the project +installs and upgrades. + +## Frameworkless (`frameworkless`) + +**For.** Nothing to install or upgrade; one static entry point per +object; the handler is callable in a test without DML. The whole +dispatch fits in twenty lines a newcomer reads in one sitting. + +**Against.** Bypass and recursion control are the project's to write, +and each project writes them differently. Nothing recognises the shape +from outside, so the framework must be declared to be resolved. + +## Something else + +A recognition label — fflib, TDTM, a dispatcher — or a framework this +plugin has never seen: the framework is sound and only the document is +missing. Offer to write a project document describing the project's own +code, answering the four questions the `salesforce-triggers` skill +asks, and declare it with a `doc-path:` locator. Never compare it to +the three above — this plugin cannot write that comparison, and a +document written from a pattern found on the internet describes +somebody else's framework. +```` + +- [ ] **Step 2: Check the file's shape and the declaration-key invariant** + +```bash +f=plugins/salesforce-standards/skills/salesforce-triggers/reference/choosing-a-framework.md +grep -c '^## ' "$f" +grep -c '^\*\*For\.\*\*\|^\*\*Against\.\*\*' "$f" +grep -c '^trigger-framework:\|^vendor-paths:' "$f" +``` + +Expected: `4`, `6`, `0`. The third is the invariant: the key is mentioned once, in backticks mid-sentence. + +- [ ] **Step 3: Validate and commit** + +```bash +claude plugin validate . && claude plugin validate plugins/salesforce-standards +git add plugins/salesforce-standards/skills/salesforce-triggers/reference/choosing-a-framework.md +git commit -m "feat(salesforce-standards): add the framework comparison for step (c)" +``` + +Expected: validation passes; one commit. + +--- + +### Task 7: Move trigger content out of `salesforce-apex` + +**Files:** +- Modify: `plugins/salesforce-standards/skills/salesforce-apex/SKILL.md:3`, `:8-17`, `:21-36`, `:43-60` +- Rename: `plugins/salesforce-standards/skills/salesforce-apex/reference/trigger-handler.md` → `order-layers.md`, then rewrite `:1-15` and delete `:17-62` +- Modify: `plugins/salesforce-standards/skills/salesforce-apex/reference/bulkification.md:5` +- Modify: `plugins/salesforce-standards/skills/salesforce-apex-testing/reference/test-patterns.cls:4` +- Modify: `plugins/salesforce-standards/skills/salesforce-apex-testing/SKILL.md:12` — the layering sentence + +**Interfaces:** +- Produces: `reference/order-layers.md`, the file Tasks 1, 3, 4 and 5 already cite; the four remaining example units unchanged in content. + +- [ ] **Step 1: Measure before** + +```bash +a=plugins/salesforce-standards/skills/salesforce-apex/SKILL.md +grep -c 'TriggerHandler' "$a" +grep -c 'trigger-handler.md' "$a" +tr -s '[:space:]' ' ' < "$a" | grep -o 'one trigger per object' | wc -l +grep -c 'order-layers.md' "$a" +grep -c '(id:' "$a" +test -f plugins/salesforce-standards/skills/salesforce-apex/reference/trigger-handler.md && echo present +rg -l 'trigger-handler\.md' plugins/ | wc -l +grep -c 'salesforce-triggers' plugins/salesforce-standards/skills/salesforce-apex-testing/SKILL.md +``` + +Expected: `2`, `1`, `1`, `0`, `10`, `present`, `3`, `0`. The tag pattern carries no trailing space, because a sub-rule tag wraps after `(id:`; ten tags is the file's whole census, groups and sub-rules. + +- [ ] **Step 2: Edit the description** + +Find (line 3): + +``` +description: Use when writing or reviewing Apex backend code — naming, lightweight layering (one trigger handler per object, service, selector, domain), bulkification, governor limits, sharing keywords, and error handling. Apex unit tests belong to salesforce-apex-testing; access-model design to salesforce-security-model. +``` + +Replace with: + +``` +description: Use when writing or reviewing Apex backend code — naming, lightweight layering (service, selector, domain), bulkification, governor limits, sharing keywords, and error handling. Apex triggers and their handlers belong to salesforce-triggers; Apex unit tests to salesforce-apex-testing; access-model design to salesforce-security-model. +``` + +- [ ] **Step 3: Edit the intro** + +Find: + +``` +Standards for Apex classes and triggers: naming, layering, bulk safety, +governor limits, sharing, and error handling. Cite rules in review +``` + +Replace with: + +``` +Standards for Apex classes: naming, layering, bulk safety, governor +limits, sharing, and error handling. Triggers and their handlers — one +trigger per object, the trigger body, the project's trigger framework — +belong to `salesforce-triggers`. Cite rules in review +``` + +- [ ] **Step 4: Edit the naming table and its tag** + +Delete the row: + +``` +| Trigger handler | `TriggerHandler` | `AccountTriggerHandler` | +``` + +Find, in the `apex-naming` tag: + +``` +`LocalVariableNamingConventions`; the handler/selector/exception +patterns are this standard) +``` + +Replace with: + +``` +`LocalVariableNamingConventions`; the selector/exception patterns are +this standard) +``` + +- [ ] **Step 5: Edit the layers table and the bullets** + +Find the two rows: + +``` +| Trigger | One line: hands the trigger context to its handler | Any conditional or business logic | +| Handler (`TriggerHandler`) | Routes each trigger event (before insert, after update, …) to domain and service calls | SOQL, DML, business logic itself | +``` + +Replace with one row: + +``` +| Trigger and handler | Dispatch: hand the trigger context to the layers below — the shape, one trigger per object and the project's framework are `salesforce-triggers`'s | SOQL, DML, business logic itself | +``` + +Delete the bullet: + +``` +- **One handler per trigger; one trigger per object.** The trigger body + is a single delegating call — no `if`/`for`/field logic in the + `.trigger` file. +``` + +Find: + +``` +- A complete worked example (trigger + handler + domain + selector + + service for one object) is in + [reference/trigger-handler.md](reference/trigger-handler.md). +``` + +Replace with: + +``` +- The four framework-independent units for one object — domain, + selector, service, exception — are in + [reference/order-layers.md](reference/order-layers.md); the trigger + and handler that call them are in each `salesforce-triggers` + framework document. +``` + +The `apex-layering` tag below the bullets is untouched. + +- [ ] **Step 6: Rename the reference file and rewrite its head** + +```bash +git mv plugins/salesforce-standards/skills/salesforce-apex/reference/trigger-handler.md plugins/salesforce-standards/skills/salesforce-apex/reference/order-layers.md +``` + +Then replace lines 1–15 (the title through the scenario paragraph) with: + +```markdown +# Domain + selector + service — worked example + +One object (standard `Order`), four compilation units, one fenced block +each — each block is valid, copy-pasteable Apex for the file named +above it. In a real org each block is its own `.cls` file (a `.cls` +file holds exactly one top-level type). The trigger and the handler +that call these units depend on the project's trigger framework and +live in the `salesforce-triggers` skill's framework documents; each of +them calls the same methods below, so the three examples are one +scenario. + +Scenario: when an Order's `Status` field moves to "Activated", create +one fulfillment `Task` per order. The example demonstrates: a domain +class that owns the record-level status-transition rule, a selector +that owns all SOQL for `Order`, and a service that owns the DML +transaction — all bulk-safe, all with an explicit sharing declaration, +errors routed through a custom exception and a logger abstraction +rather than swallowed or left as bare `System.debug` calls. +``` + +Then delete the two sections `## \`OrderTrigger.trigger\`` and `## \`OrderTriggerHandler.cls\`` in full — from the `## \`OrderTrigger.trigger\`` heading up to, not including, the `## \`OrderDomain.cls\`` heading. The four remaining sections are untouched. + +- [ ] **Step 7: Update the two citations** + +`bulkification.md:5` — find `[trigger-handler.md](trigger-handler.md).` and replace with `[order-layers.md](order-layers.md).` + +`test-patterns.cls:4` — find ` * as salesforce-apex's reference/trigger-handler.md): activating an Order` and replace with ` * as salesforce-apex's reference/order-layers.md): activating an Order`. + +`salesforce-apex-testing/SKILL.md:12` — find: + +``` +decision). Layering (handler/domain/service/selector) is +`salesforce-apex`'s concern, not restated here — a test exercises those +layers, it does not redefine them. LWC jest tests are out of scope — +``` + +Replace with: + +``` +decision). Layering (domain/service/selector) is `salesforce-apex`'s +concern and the trigger handler `salesforce-triggers`'s, neither +restated here — a test exercises those layers, it does not redefine +them. LWC jest tests are out of scope — +``` + +- [ ] **Step 8: Measure after** + +```bash +a=plugins/salesforce-standards/skills/salesforce-apex/SKILL.md +o=plugins/salesforce-standards/skills/salesforce-apex/reference/order-layers.md +grep -c 'TriggerHandler' "$a" +grep -c 'trigger-handler.md' "$a" +tr -s '[:space:]' ' ' < "$a" | grep -o 'one trigger per object' | wc -l +grep -c 'order-layers.md' "$a" +grep -c '(id:' "$a" +test -f plugins/salesforce-standards/skills/salesforce-apex/reference/trigger-handler.md && echo present +rg -l 'trigger-handler\.md' plugins/ | wc -l +grep -c '^## ' "$o" +grep -c 'Trigger\.' "$o" +grep -c '^```apex' "$o" +grep -c 'OrderProcessingException extends Exception' "$o" +grep -c 'salesforce-triggers' plugins/salesforce-standards/skills/salesforce-apex-testing/SKILL.md +``` + +Expected: `0`, `0`, `2`, `1`, `10`, no `present` line, `0`, `4`, `0`, `4`, `1`, `1`. The third rises from `1` to `2`: the deleted bullet carried the phrase once, and the intro's pointer sentence and the new layers row each carry it once — both pointing at `salesforce-triggers`, where the rule now lives. The `(id: ` count is an invariant — no rule leaves or arrives; `apex-naming` and `apex-layering` keep their ids. Before this task `order-layers.md` did not exist, so its four values have no before-state; the old file measured `6` headings, `8` `Trigger.` references and `6` Apex fences, and the drop to `4`, `0`, `4` is the two sections leaving. + +- [ ] **Step 9: Validate and commit** + +```bash +claude plugin validate . && claude plugin validate plugins/salesforce-standards +git add -A plugins/salesforce-standards/skills/salesforce-apex plugins/salesforce-standards/skills/salesforce-apex-testing/reference/test-patterns.cls plugins/salesforce-standards/skills/salesforce-apex-testing/SKILL.md +git commit -m "refactor(salesforce-standards): move trigger content out of salesforce-apex" +``` + +Expected: validation passes; one commit carrying a rename and four modified files. + +--- + +### Task 8: Resolve before grading — the review skill and the command + +**Files:** +- Modify: `plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md:51-55` — Procedure step 2 gains a paragraph +- Modify: `plugins/salesforce-standards/commands/salesforce-review.md:9-37` — a new step 2, later steps renumbered + +**Interfaces:** +- Consumes: the step letters, the record shape and the Summary/`## Project` slots from Task 2. + +The review skill's step is added inside step 2 rather than as a new numbered step: the agent card says "per the skill's step 5", the skill's fallback says "exactly as in step 5", and the spec keeps the card unchanged. Renumbering would break the card's citation or force an edit the spec rules out. + +- [ ] **Step 1: Measure before** + +```bash +s=plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md +c=plugins/salesforce-standards/commands/salesforce-review.md +grep -c 'salesforce-triggers' "$s" +grep -c '^[0-9]\. ' "$s" +grep -c 'salesforce-triggers' "$c" +grep -c '^[0-9]\. ' "$c" +grep -c 'from step 2' "$c" +grep -c 'step 5' plugins/salesforce-standards/agents/salesforce-code-reviewer.md +``` + +Expected: `0`, `6`, `0`, `5`, `1`, `1`. + +- [ ] **Step 2: Extend the review skill's step 2** + +Find: + +``` +2. Load the standards skills relevant to the content under review: + salesforce-apex for any Apex; salesforce-apex-testing, + salesforce-lwc, salesforce-flow, salesforce-data-model, + salesforce-security-model, salesforce-aura, salesforce-visualforce + as the content demands. +``` + +Replace with: + +``` +2. Load the standards skills relevant to the content under review: + salesforce-apex for any Apex; salesforce-apex-testing, + salesforce-lwc, salesforce-flow, salesforce-data-model, + salesforce-security-model, salesforce-aura, salesforce-visualforce + as the content demands. + Any Apex file in the run — a `.trigger` or a `.cls` alike — also + loads `salesforce-triggers` and runs its resolution protocol before + grading: steps (0) through (b), then step (d) for each resolved + framework, loading that framework's document by path or name and + selecting its handlers from the grading universe by the document's + signature. Resolution runs per file, so a monorepo running two + frameworks loads two documents and grades every class under the + framework resolved for its own path. Step (c) is never taken here — + a background agent asks nothing — and neither is the offer to write + a declaration. A resolution record carried in the dispatch prompt is + verified against the declarations on disk; on a mismatch the files + are the authority. When resolution fails, grade the + framework-independent rules, skip framework rules rather than guess + them, note `trigger framework: unresolved — framework-specific rules + not graded` among the Summary's out-of-scope notes, and report + `trigger-framework-declared` (nothing declares the path) or + `trigger-framework-declared.ambiguous` (two the protocol cannot rank) + in `## Project`. Where resolution succeeds and the document supplies + no signature, the same note names `trigger-context-below-handler` + beside the framework rules. The skill's failure table names the + other Summary notes: unreadable `extends` parents in aggregate, the + two `vendor-paths:` notes, an unreadable discriminator. +``` + +- [ ] **Step 3: Add the command's resolution step** + +Find the line beginning `2. Pre-dispatch first-create check` and insert before it: + +``` +2. Resolve the trigger framework when the scope holds any Apex file: + load `salesforce-triggers` and run its protocol, steps (0) through + (c) — this is the one review surface where step (c) may ask the + developer. Write the resolution record, one per resolved framework + with the paths it covers, and carry it in the dispatch prompt beside + the directory-mode decision; the agent verifies it against the + declarations on disk. Where step (c) resolved the framework, offer + to write the declaration before dispatching. +``` + +Then renumber: the former step 2 becomes `3.`, 3 becomes `4.`, 4 becomes `5.`, 5 becomes `6.`; and in the new step 4 (the dispatch step) change `the directory-mode decision from step 2` to `the directory-mode decision from step 3`. + +- [ ] **Step 4: Measure after** + +```bash +s=plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md +c=plugins/salesforce-standards/commands/salesforce-review.md +grep -c 'salesforce-triggers' "$s" +grep -c '^[0-9]\. ' "$s" +grep -c 'salesforce-triggers' "$c" +grep -c '^[0-9]\. ' "$c" +grep -c 'from step 2' "$c" +grep -c 'from step 3' "$c" +grep -c 'step 5' plugins/salesforce-standards/agents/salesforce-code-reviewer.md +tr -s '[:space:]' ' ' < "$s" | grep -o 'trigger framework: unresolved' | wc -l +``` + +Expected: `1`, `6`, `1`, `6`, `0`, `1`, `1`, `1`. The skill's numbered-step count is an invariant at `6` — the addition sits inside step 2 — and the agent card's `step 5` is an invariant at `1`, which is the reason for that placement. + +- [ ] **Step 5: Validate and commit** + +```bash +claude plugin validate . && claude plugin validate plugins/salesforce-standards +git add plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md plugins/salesforce-standards/commands/salesforce-review.md +git commit -m "feat(salesforce-standards): resolve the trigger framework before grading Apex" +``` + +Expected: validation passes; one commit. + +--- + +### Task 9: The toolchain rule routes to the skill and documents the keys + +**Files:** +- Modify: `plugins/salesforce-standards/rules/salesforce-toolchain.md:38-45` + +**Interfaces:** +- Consumes: the two keys and the homes from Task 2. + +The installed copy of this rule is not a declaration home — the Rules engine owns it — so the paragraph documents the keys without carrying them: every mention sits in backticks mid-sentence, and no line starts with either key. + +- [ ] **Step 1: Measure before** + +```bash +r=plugins/salesforce-standards/rules/salesforce-toolchain.md +grep -c 'salesforce-triggers' "$r" +grep -c '^trigger-framework:\|^vendor-paths:' "$r" +grep -c '^---$' "$r" +``` + +Expected: `0`, `0`, `2`. The last two are invariants: no key at line start, and the frontmatter block intact. + +- [ ] **Step 2: Edit the routing paragraph and add the declaration paragraph** + +Find: + +``` +When the salesforce-standards plugin's skills are available, load the +matching one before working: `salesforce-apex` (backend code), +`salesforce-apex-testing` (Apex tests), `salesforce-lwc` (Lightning +``` + +Replace with: + +``` +When the salesforce-standards plugin's skills are available, load the +matching one before working: `salesforce-apex` (backend code), +`salesforce-triggers` (Apex triggers and their handlers), +`salesforce-apex-testing` (Apex tests), `salesforce-lwc` (Lightning +``` + +Then append after the paragraph's last line (`available, the toolchain facts above still bind.`), separated by one blank line: + +``` +The project's trigger framework and its vendor code are declared in +the project, never in this rule: a line carrying the `trigger-framework:` +key and one carrying the `vendor-paths:` key, in the body of the root +`CLAUDE.md`, of a trigger directory's `CLAUDE.md`, or of a project rule +outside this payload — one framework per path, vendor prefixes at the +root. When the `salesforce-triggers` skill is available it reads both +keys and says how they rank; when it is not, the lines still record the +choice for the reader. +``` + +- [ ] **Step 3: Measure after** + +```bash +r=plugins/salesforce-standards/rules/salesforce-toolchain.md +grep -c 'salesforce-triggers' "$r" +grep -c '^trigger-framework:\|^vendor-paths:' "$r" +grep -c '^---$' "$r" +grep -c 'trigger-framework:' "$r" +``` + +Expected: `2`, `0`, `2`, `1`. The key appears once, mid-line. + +- [ ] **Step 4: Review the frontmatter by hand** + +`claude plugin validate` does not check `rules/`. Open the file and confirm the `paths:` block is unchanged and the closing `---` still sits on line 23. + +- [ ] **Step 5: Validate and commit** + +```bash +claude plugin validate . && claude plugin validate plugins/salesforce-standards +git add plugins/salesforce-standards/rules/salesforce-toolchain.md +git commit -m "feat(salesforce-standards): route triggers and document the declaration keys in the toolchain rule" +``` + +Expected: validation passes; one commit. + +--- + +### Task 10: The identity surfaces + +**Files:** +- Modify: `plugins/salesforce-standards/README.md:11-25`, `:45-65` +- Modify: `plugins/salesforce-standards/.claude-plugin/plugin.json` — `description` only +- Modify: `.claude-plugin/marketplace.json` — the `salesforce-standards` entry's `description` +- Modify: `README.md:21` — the `salesforce-standards` row + +The marketplace-sync rule binds all four in one commit whenever a plugin's description changes. The version string is untouched. + +- [ ] **Step 1: Measure before** + +```bash +grep -c 'eight area skills' plugins/salesforce-standards/.claude-plugin/plugin.json +grep -c 'salesforce-triggers' plugins/salesforce-standards/README.md +sed -n '/^## Standards skills/,/^## Review/p' plugins/salesforce-standards/README.md | grep -c '^| `' +grep -c 'Apex triggers' .claude-plugin/marketplace.json README.md +grep '"version"' plugins/salesforce-standards/.claude-plugin/plugin.json +``` + +Expected: `1`, `0`, `8`, `.claude-plugin/marketplace.json:0` and `README.md:0`, `"version": "0.3.1",`. + +- [ ] **Step 2: Edit `plugin.json`** + +Find `eight area skills (Apex, Apex testing, LWC, Flow, data model, security model, maintenance-first Aura and Visualforce)` and replace with `nine area skills (Apex, Apex triggers, Apex testing, LWC, Flow, data model, security model, maintenance-first Aura and Visualforce)`. + +- [ ] **Step 3: Edit the plugin README** + +Find the row: + +``` +| `salesforce-apex` | naming, lightweight layers (trigger handler, service, selector, domain), bulkification, governor limits, sharing keywords, error handling | +``` + +Replace with two rows: + +``` +| `salesforce-apex` | naming, lightweight layers (service, selector, domain), bulkification, governor limits, sharing keywords, error handling | +| `salesforce-triggers` | one trigger per object, the trigger body, resolving the project's trigger framework — base-class, metadata-driven, frameworkless, or one the project documents itself — and loading its guidance | +``` + +Then insert a section before `## Review stack`: + +```markdown +## Declaring the trigger framework + +A project declares which trigger framework governs its triggers with a +`trigger-framework:` line in the body of its root `CLAUDE.md` or of the +trigger directory's, and lists code it did not write with a +`vendor-paths:` line at the root — directory prefixes, never globs. The +`salesforce-triggers` skill reads both, ranks nested declarations, +infers from code when nothing declares, and asks only from an +interactive session. A framework this plugin does not ship is declared +with a `doc-path:` or `skill:` locator pointing at the project's own +document. +``` + +- [ ] **Step 4: Edit the catalog and the repo README** + +`.claude-plugin/marketplace.json` — find `area skills for Apex, LWC, Flow, data and security model plus maintenance-first legacy UI` and replace with `area skills for Apex, Apex triggers, LWC, Flow, data and security model plus maintenance-first legacy UI`. + +`README.md:21` — find `area skills (Apex, LWC, Flow, data/security model, legacy UI)` and replace with `area skills (Apex, Apex triggers, LWC, Flow, data/security model, legacy UI)`. + +- [ ] **Step 5: Measure after** + +```bash +grep -c 'eight area skills' plugins/salesforce-standards/.claude-plugin/plugin.json +grep -c 'nine area skills' plugins/salesforce-standards/.claude-plugin/plugin.json +grep -c 'salesforce-triggers' plugins/salesforce-standards/README.md +sed -n '/^## Standards skills/,/^## Review/p' plugins/salesforce-standards/README.md | grep -c '^| `' +grep -c 'Apex triggers' .claude-plugin/marketplace.json README.md +grep -c '^trigger-framework:\|^vendor-paths:' plugins/salesforce-standards/README.md +grep '"version"' plugins/salesforce-standards/.claude-plugin/plugin.json +``` + +Expected: `0`, `1`, `2`, `9`, `.claude-plugin/marketplace.json:1` and `README.md:1`, `0`, `"version": "0.3.1",`. The `salesforce-triggers` count is two: the table row and the new section each name it once. The declaration-key count at line start is the plugin-wide invariant at `0`; the version line is an invariant too. + +- [ ] **Step 6: Validate and commit** + +```bash +claude plugin validate . && claude plugin validate plugins/salesforce-standards +git add plugins/salesforce-standards/README.md plugins/salesforce-standards/.claude-plugin/plugin.json .claude-plugin/marketplace.json README.md +git commit -m "docs(salesforce-standards): list the salesforce-triggers skill on the identity surfaces" +``` + +Expected: validation passes; one commit touching four files. + +--- + +### Task 11: End-state sweep + +**Files:** none modified unless a check fails; a failure is fixed in the task that owns the text, and this task re-runs. + +Every check here reports an end state, so none has a before-value; the exemption is claimed on each. + +- [ ] **Step 1: Structural validation** + +```bash +claude plugin validate . && claude plugin validate plugins/salesforce-standards +``` + +Expected: both pass. + +- [ ] **Step 2: No declaration key at line start outside a fence, anywhere in the plugin** + +```bash +for f in $(rg -l 'trigger-framework:|vendor-paths:' plugins/salesforce-standards); do + printf '%s %s\n' "$f" "$(awk '/^```/{f=!f;next} !f' "$f" | grep -c '^trigger-framework:\|^vendor-paths:')" +done +``` + +Expected: five lines, every one ending in ` 0`. Files listed: the hub `SKILL.md`, `choosing-a-framework.md`, the toolchain rule, the plugin README, and the code-review skill (which names `vendor-paths:` in its Summary-notes sentence). The command and the three framework documents name neither key and do not appear. + +- [ ] **Step 3: Rule-tag census across the new skill** + +```bash +d=plugins/salesforce-standards/skills/salesforce-triggers +grep -c '(id:' "$d/SKILL.md" "$d"/reference/*.md +grep -rc 'severity: critical' "$d" +grep -rn 'severity: critical' "$d" | grep -c 'kind: defect' +grep -rn 'severity: critical' "$d" | grep -vc 'kind: defect' +grep -rho 'severity: [a-z]*' "$d" | sort | uniq -c +``` + +Expected: `SKILL.md:7`, `choosing-a-framework.md:0`, `framework-base-class.md:3`, `framework-frameworkless.md:3`, `framework-metadata-driven.md:4`; one file with `1` critical and the rest `0`; `1`; `0`; and the tally `1 severity: critical`, `10 severity: important`, `6 severity: minor` — re-derived: important are `.disjoint-contexts`, `trigger-body-delegates`, `trigger-context-below-handler`, `.ambiguous`, two frameworkless, two base-class, two metadata-driven = 10; minor are `trigger-naming`, `trigger-framework-declared`, three dispatching-class rules, `order-explicit` = 6. The hub's prose sentence "`setMaxLoopCount(1)` guards …" and Task 1's sub-rule boundary prose carry no `severity:` word, so the tally counts tags alone. + +- [ ] **Step 4: Sub-rule severities differ from their group default** + +```bash +f=plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md +grep -n 'trigger-one-per-object`; severity\|disjoint-contexts`; severity\|trigger-framework-declared`; severity\|ambiguous`; severity' "$f" +``` + +Expected: four lines — `critical`, `important`, `minor`, `important`, in that order. Each sub-rule differs from its group. + +- [ ] **Step 5: The renamed file has no survivors and every framework document cites it** + +```bash +rg -l 'trigger-handler\.md' plugins/ +rg -l 'order-layers\.md' plugins/salesforce-standards | sort +``` + +Expected: the first prints nothing. The second lists exactly: `plugins/salesforce-standards/skills/salesforce-apex-testing/reference/test-patterns.cls`, `plugins/salesforce-standards/skills/salesforce-apex/SKILL.md`, `plugins/salesforce-standards/skills/salesforce-apex/reference/bulkification.md`, `plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md`, `plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-base-class.md`, `plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-frameworkless.md`, `plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-metadata-driven.md` — seven files. + +- [ ] **Step 6: Disjoint descriptions** + +```bash +grep -h '^description:' plugins/salesforce-standards/skills/salesforce-apex/SKILL.md plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md | grep -c 'trigger handler' +grep -h '^description:' plugins/salesforce-standards/skills/*/SKILL.md | grep -c '^description: .*: ' +``` + +Expected: `0` — neither description says "trigger handler", the phrase that made them compete; `0` — no description carries a second colon-space after the key's own. The second value was `0` before Task 1 as well and is an invariant. + +- [ ] **Step 7: The four lower-layer methods are the only ones a handler calls** + +```bash +grep -rho 'Order\(Domain\|Service\)\.[a-zA-Z]*' plugins/salesforce-standards/skills/salesforce-triggers/reference/ | sort | uniq -c +``` + +Expected: exactly four names — `OrderDomain.applyDefaults`, `OrderDomain.filterNewlyActivated`, `OrderDomain.validateStatusTransitions`, `OrderService.activateFulfillment` — each with count `3`, one per framework document. Every one is defined in `order-layers.md`: + +```bash +grep -c 'public static void applyDefaults\|public static void validateStatusTransitions\|public static List filterNewlyActivated\|public static void activateFulfillment' plugins/salesforce-standards/skills/salesforce-apex/reference/order-layers.md +``` + +Expected: `4`. + +- [ ] **Step 8: Hygiene and version** + +```bash +rg -n '/home/' plugins/salesforce-standards .claude-plugin/marketplace.json README.md +grep '"version"' plugins/salesforce-standards/.claude-plugin/plugin.json +git log --format=%s develop..HEAD -- plugins/salesforce-standards .claude-plugin README.md | grep -c '(salesforce-standards)' +git log --format=%B develop..HEAD | grep -c 'Co-Authored-By' +``` + +Expected: the first prints nothing; `"version": "0.3.1",`; `10`, or more where a failed check sent a fix back through an owning task — the path filter keeps the document branch's `docs/` commits out of the count, and a commit under `## Developer rulings` touches no path listed; `0`. + +- [ ] **Step 8a: The hub's document table names files that exist** + +```bash +d=plugins/salesforce-standards/skills/salesforce-triggers +for p in $(grep -o 'reference/framework-[a-z-]*\.md' "$d/SKILL.md" | sort -u); do test -f "$d/$p" && echo "$p"; done +``` + +Expected: three lines — `reference/framework-base-class.md`, `reference/framework-frameworkless.md`, `reference/framework-metadata-driven.md`. An end-state check, so no before-value. + +- [ ] **Step 9: Report** + +State the end state to the developer: ten commits on `feature/trigger-frameworks`, every check above at its expected value, the spec notes this plan surfaced (Deviations 1, 2, 4 and 5) for the spec's ledger, and one item for the release PR's notes — a project on the frameworkless shape this plugin shipped before now earns `trigger-body-delegates` per trigger until it adds the `handle` entry point (the Gotcha in `framework-frameworkless.md` names the fix). Do not move the plan's `status`; the developer flips it. + +## Developer rulings + +None yet. Rulings taken during execution are recorded here, one line each, dated. + +## Review rounds + +### 2026-09-08 — plan-adversary, fable 5.1, blocking (round 1, full-document) + +- fixed 2026-09-08 — [Important] Task 4's test-isolation block calls `TestDataFactory.createOrders(200)`, which the plugin's own factory does not define; license: `salesforce-apex-testing/reference/TestDataFactory.cls:53` (`createOrders(Integer count, Id accountId)`) and the Global Constraint that every Apex block is copy-pasteable; the block now creates the account first and calls `createOrders(200, acc.Id)` +- fixed 2026-09-08 — [Important] `header: TriggerAction.` selects `MetadataTriggerHandler` itself and every `DmlFinalizer` implementer, and the hub orders nothing between "a handler on that match alone" and the `Framework types:` exemption, so `trigger-metadata-driven-action-registered` fires on the framework class wherever it is vendored as source; license: the spec's sentence that framework types are "graded by no framework rule"; the hub grammar now says a `Framework types:` entry leaves the handler set even where it matches, Task 5 names `MetadataTriggerHandler` as the instance, and `action-registered` is scoped to the seven context interfaces with `DmlFinalizer` implementers left alone. Recorded as Deviation 4, since the spec never orders the two sentences +- fixed 2026-09-08 — [Important] The hub restates a grade outside a tag, "as a minor rule", which no other surface may do; license: `.claude/rules/standards-rule-tags.md:24`; the hub now reads "as a rule of its own", and Deviation 5 records that the spec's phrase does not travel into shipped text +- fixed 2026-09-08 — [Minor] `trigger-frameworkless-entry-point` could never fire on a class, since a class without the entry point matches no signature; license: the rule's own text, which said so; the rule is now graded on the trigger — its single call is the `handle` entry point, and a per-context call leaves the handler unselectable +- fixed 2026-09-08 — [Minor] A consumer who followed 0.3.1's frameworkless shape earns `trigger-body-delegates` per trigger and matches no signature, and nothing told them; license: the spec's stance that pre-existing code is left alone where a rule says so, and the plan's own migration silence; `framework-frameworkless.md` gains a Gotcha naming the old shape and its mechanical fix, and Task 11 Step 9 flags it for the release notes +- fixed 2026-09-08 — [Minor] `salesforce-apex-testing/SKILL.md:12` still routes the handler layer to `salesforce-apex` while the plan declared the file unchanged; license: the spec's parts table, which gives the handler to `salesforce-triggers`; Task 7 now edits the sentence, adds the file to its commit, and the File-structure note says so +- fixed 2026-09-08 — [Minor] Task 11 Step 8 counted the ten most recent subjects, which a rulings commit or a re-run fix would make wrong in either direction; license: the plan's own Developer-rulings section and Task 11's re-run clause, which the check contradicted; the count is now path-filtered over `develop..HEAD` and expects ten or more +- fixed 2026-09-08 — [Minor] No check confirmed that the three document paths the hub table names exist on disk; license: Task 11's stated job as the end-state sweep; Step 8a now tests each path the table names +- signal 2026-09-08 — another round earns its cost only diff-scoped over the fixes to the second and third Important findings, the hub grammar change being the class that breeds repair-born defects; the five Minor leftovers are worth one fix wave, and the before-values and the API facts of Tasks 4 and 5 need no re-read From bcdd4d1c6011f5eb1d53c47564223b6204d1f6fe Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 20:12:32 +0200 Subject: [PATCH 019/120] docs: apply adversary round two to the trigger-frameworks plan --- ...026-09-08-salesforce-trigger-frameworks.md | 64 ++++++++++++------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md index ef25588..fc39198 100644 --- a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md +++ b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md @@ -2,7 +2,7 @@ ticket: none date: 2026-09-08 status: draft -adversary: blocking +adversary: concerns spec: ../specs/2026-09-08-salesforce-trigger-frameworks-design.md branch: feature/trigger-frameworks base: develop @@ -43,7 +43,7 @@ Recorded here and beside the text they concern, so a reviewer trips over the rea 1. **The metadata-driven signature reads `header: TriggerAction.`, not `header: implements TriggerAction.`** The spec's example says `implements TriggerAction.` "matches … a class implementing something else besides", which holds only when the framework interface comes first: `implements Queueable, TriggerAction.BeforeInsert` does not contain that substring. The spec's own constraint — every test reduces to a grep — decides it. The shorter pattern matches every header naming a `TriggerAction.*` interface wherever it sits. Task 5 carries the sentence. 2. **One citation of the renamed file lives outside `salesforce-apex`.** The spec says "nothing outside the skill cites the filename"; `salesforce-apex-testing/reference/test-patterns.cls:4` does. Task 7 updates it, and the spec's sentence is reported as a spec defect rather than silently outrun — the plan corrects the file, not the claim. 3. **Context methods are overridden `public override`**, the form the fork's README shows, where the base class declares them `protected virtual`. Both compile; the spec prescribes neither. -4. **A `Framework types:` entry leaves the handler set even where its header matches the signature.** The spec says such types are "graded by no framework rule" and, separately, that "a class matching the signature is a handler on that match alone", without ordering the two. The plan orders them — the exemption wins — because `MetadataTriggerHandler` implements the framework's own seven interfaces and would otherwise be graded as an action wherever the framework is vendored as source outside `vendor-paths:`. The first spec sentence licenses the order; the spec's silence on it is reported. Task 1 carries the sentence and Task 5 names the instance. +4. **A `Framework types:` entry leaves the handler set even where its header matches the signature.** The spec says such types are "graded by no framework rule" and, separately, that "a class matching the signature is a handler on that match alone", without ordering the two. The plan orders them — the exemption wins — because every framework-owned class implementing the framework's own context interfaces would otherwise be graded as an action wherever the framework is vendored as source outside `vendor-paths:`; the metadata-driven framework ships two such classes, `MetadataTriggerHandler` and `TriggerActionFlow` (`TriggerActionFlow.cls:24`), and the second has a `Trigger_Action__mdt` record only where a Flow action is registered. The first spec sentence licenses the order; the spec's silence on it is reported. Task 1 carries the sentence in both the `Framework types:` bullet and the handler-set definition, and Task 5 names the instances. 5. **The hub does not say "as a minor rule".** The spec's Naming rule reads "each shipped document carries that name as a rule of its own, at minor severity"; a shipped surface restating a grade outside the tag is what `.claude/rules/standards-rule-tags.md` forbids, so the hub says "as a rule of its own" and the three documents' tags carry the grade. Not a spec defect — the tag rule binds plugin content, not design documents — but the phrase does not travel. ## File structure @@ -61,12 +61,13 @@ Modified: - `plugins/salesforce-standards/skills/salesforce-apex/SKILL.md` — description, intro, naming table, layers table, example pointer. - `plugins/salesforce-standards/skills/salesforce-apex/reference/trigger-handler.md` → renamed `order-layers.md`, two sections removed. - `plugins/salesforce-standards/skills/salesforce-apex/reference/bulkification.md:5` and `plugins/salesforce-standards/skills/salesforce-apex-testing/reference/test-patterns.cls:4` — the filename citation. +- `plugins/salesforce-standards/skills/salesforce-apex-testing/SKILL.md:12` — the layering sentence, which routed the handler to `salesforce-apex`. - `plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md` — Procedure step 2 gains the resolution step. - `plugins/salesforce-standards/commands/salesforce-review.md` — a resolution step before dispatch. - `plugins/salesforce-standards/rules/salesforce-toolchain.md` — routing line and the two keys. - `plugins/salesforce-standards/README.md`, `plugins/salesforce-standards/.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `README.md` — the identity surfaces, in one commit. -Unchanged on purpose: `agents/salesforce-code-reviewer.md` (the skill owns the step; the card's "step 5" reference is why Task 8 adds to step 2 rather than renumbering). `skills/salesforce-apex-testing/SKILL.md` changes one sentence only, in Task 7: its layering pointer still routes the handler to `salesforce-apex`. +Unchanged on purpose: `agents/salesforce-code-reviewer.md` (the skill owns the step; the card's "step 5" reference is why Task 8 adds to step 2 rather than renumbering). --- @@ -286,11 +287,12 @@ label. A signature reads one class, matches in a named place, and yields membership. **The handler set** is the smallest fixed point of the classes matching -the signature together with the classes whose header `extends` a type -already in the set — so `OrderHandler extends BaseTriggerHandler`, an -org's own layer over the framework's, is graded. A class matching the -signature is a handler on that match alone. The parent rules govern -**closure candidates** only — a class matching no signature whose header +the signature, less the types `Framework types:` names, together with +the classes whose header `extends` a type already in the set — so +`OrderHandler extends BaseTriggerHandler`, an org's own layer over the +framework's, is graded. A class matching the signature is a handler on +that match alone unless `Framework types:` names it. The parent rules +govern **closure candidates** only — a class matching no signature whose header carries `extends`: - the parent is read from the repository even where it lies outside a @@ -750,13 +752,13 @@ Pre-existing non-conforming names stay; the convention binds new code. (id: `trigger-frameworkless-dispatching-class`; severity: minor; source: this standard) -**One entry point.** The trigger's single call is the handler's static -`handle` method taking `System.TriggerOperation` and the records. A -trigger calling a per-context method instead — `handleBeforeInsert`, -with or without a branch around it — leaves its handler outside the -signature, where no framework rule reaches it. The rule is graded on -the trigger for that reason: the class it would grade cannot be -selected. +**One entry point.** Every call in the trigger body is a handler's +static `handle` method taking `System.TriggerOperation` and the +records — several handlers, several such calls. A call to a per-context +method instead — `handleBeforeInsert`, with or without a branch around +it — leaves that handler outside the signature, where no framework rule +reaches it. The rule is graded on the trigger for that reason: the +class it would grade cannot be selected. (id: `trigger-frameworkless-entry-point`; severity: important; source: this standard) @@ -781,10 +783,12 @@ source: this standard) objects disables each handler it must silence. - The shape this plugin shipped before `salesforce-triggers` existed — static `handleBeforeInsert` methods and a trigger branching on - `Trigger.isBefore` — now earns `trigger-body-delegates` on the - trigger and matches no signature. The fix is mechanical: keep the - methods, add one `handle` entry point that switches on the operation - and calls them, and make the trigger call it. + `Trigger.isBefore` — now earns two findings on the trigger, + `trigger-body-delegates` and, once the project resolves as + `frameworkless`, `trigger-frameworkless-entry-point`, and its handler + matches no signature. One fix clears all three: keep the methods, add + one `handle` entry point that switches on the operation and calls + them, and make the trigger call it. ```` - [ ] **Step 2: Check the file's shape** @@ -1135,7 +1139,7 @@ setting record; a record enables exactly the contexts it fills. ``` Signature: header: TriggerAction. -Framework types: TriggerBase, MetadataTriggerHandler, TriggerAction, FinalizerHandler +Framework types: TriggerBase, MetadataTriggerHandler, TriggerAction, TriggerActionFlow, FinalizerHandler ``` The pattern is `TriggerAction.` rather than `implements TriggerAction.` @@ -1143,9 +1147,12 @@ on purpose: a class declared `implements Queueable, TriggerAction.BeforeInsert` names the framework interface second, and a header test is a substring match. Any header naming a `TriggerAction.*` interface, wherever it sits in the `implements` list, selects the -class. `MetadataTriggerHandler` itself matches — it implements all -seven context interfaces — and sits under `Framework types:`, which -removes it from the handler set. The framework types appear in the +class. Two framework classes match it themselves — +`MetadataTriggerHandler` and `TriggerActionFlow`, each implementing all +seven context interfaces — and both sit under `Framework types:`, which +removes them from the handler set; `TriggerActionFlowChangeEvent +extends TriggerActionFlow` is then a closure candidate whose parent is +not in the set, and stays out too. The framework types appear in the repository only where the framework arrives as source; listing them costs nothing where it does not. @@ -1984,7 +1991,7 @@ Expected: three lines — `reference/framework-base-class.md`, `reference/framew - [ ] **Step 9: Report** -State the end state to the developer: ten commits on `feature/trigger-frameworks`, every check above at its expected value, the spec notes this plan surfaced (Deviations 1, 2, 4 and 5) for the spec's ledger, and one item for the release PR's notes — a project on the frameworkless shape this plugin shipped before now earns `trigger-body-delegates` per trigger until it adds the `handle` entry point (the Gotcha in `framework-frameworkless.md` names the fix). Do not move the plan's `status`; the developer flips it. +State the end state to the developer: ten commits on `feature/trigger-frameworks`, every check above at its expected value, the spec notes this plan surfaced (Deviations 1, 2, 4 and 5) for the spec's ledger, and one item for the release PR's notes — a project on the frameworkless shape this plugin shipped before now earns `trigger-body-delegates` and `trigger-frameworkless-entry-point` per trigger until it adds the `handle` entry point (the Gotcha in `framework-frameworkless.md` names the fix). Do not move the plan's `status`; the developer flips it. ## Developer rulings @@ -1992,6 +1999,15 @@ None yet. Rulings taken during execution are recorded here, one line each, dated ## Review rounds +### 2026-09-08 — plan-adversary, fable 5.1, concerns (round 2, diff-scoped) + +- fixed 2026-09-08 — [Important] The round-one fix named one instance of the class it repaired while the framework ships a second top-level class matching `header: TriggerAction.` — `TriggerActionFlow`, implementing all seven context interfaces at `TriggerActionFlow.cls:24` (the report cited `:23`), with a `Trigger_Action__mdt` record only where a Flow action is registered — so `action-registered` still fired on the framework vendored as source; license: Deviation 4's own reason, which the finding shows was incomplete rather than wrong; `TriggerActionFlow` joins `Framework types:`, Task 5 and Deviation 4 name the class of instance and both instances, and `TriggerActionFlowChangeEvent extends TriggerActionFlow` is named as a closure candidate that stays out +- fixed 2026-09-08 — [Minor] The handler-set definition still stated the pre-fix rule — "a handler on that match alone", unqualified — so the ordering lived only in the bullet above it (the report cited line 280; the bullet ends there); license: the round-one fix's own sentence; the definition now subtracts the `Framework types:` names in the fixed point and qualifies the match-alone sentence +- fixed 2026-09-08 — [Minor] `trigger-frameworkless-entry-point` said "the trigger's single call", colliding with the hub's `trigger-one-per-object` prose that several handler calls in one file are correct; license: that prose; the rule now grades every call's shape, not their number +- fixed 2026-09-08 — [Minor] The migration Gotcha and Task 11 Step 9 named one finding where the rewritten `entry-point` rule fires a second on the same trigger — two fixes from one wave describing one trigger, unreconciled; license: the rule's own text; both now name both ids and say one fix clears all three signals +- fixed 2026-09-08 — [Minor] The File-structure section kept `salesforce-apex-testing/SKILL.md` under "Unchanged on purpose" with a clause reading as a post-edit state opposite to what Task 7 writes; license: Task 7 Step 7; the file is in the Modified list with its line, and the clause is gone +- signal 2026-09-08 — another diff-scoped round does not repay its cost: the Important is a one-token list edit plus two sentences and its class is now bounded by the framework's class listing rather than by memory, and the four Minors are wording; the leftovers are worth one fix wave, then the confirming full-document round the rules mandate, where a re-introduced contradiction between the grammar bullet and the handler-set definition would surface. Not an all-Minor round + ### 2026-09-08 — plan-adversary, fable 5.1, blocking (round 1, full-document) - fixed 2026-09-08 — [Important] Task 4's test-isolation block calls `TestDataFactory.createOrders(200)`, which the plugin's own factory does not define; license: `salesforce-apex-testing/reference/TestDataFactory.cls:53` (`createOrders(Integer count, Id accountId)`) and the Global Constraint that every Apex block is copy-pasteable; the block now creates the account first and calls `createOrders(200, acc.Id)` From 482791978bdefea7ec33a8c0e9f206feffb14dba Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 20:34:30 +0200 Subject: [PATCH 020/120] docs: apply adversary round three to the trigger-frameworks plan --- ...026-09-08-salesforce-trigger-frameworks.md | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md index fc39198..8ed6db5 100644 --- a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md +++ b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md @@ -43,8 +43,9 @@ Recorded here and beside the text they concern, so a reviewer trips over the rea 1. **The metadata-driven signature reads `header: TriggerAction.`, not `header: implements TriggerAction.`** The spec's example says `implements TriggerAction.` "matches … a class implementing something else besides", which holds only when the framework interface comes first: `implements Queueable, TriggerAction.BeforeInsert` does not contain that substring. The spec's own constraint — every test reduces to a grep — decides it. The shorter pattern matches every header naming a `TriggerAction.*` interface wherever it sits. Task 5 carries the sentence. 2. **One citation of the renamed file lives outside `salesforce-apex`.** The spec says "nothing outside the skill cites the filename"; `salesforce-apex-testing/reference/test-patterns.cls:4` does. Task 7 updates it, and the spec's sentence is reported as a spec defect rather than silently outrun — the plan corrects the file, not the claim. 3. **Context methods are overridden `public override`**, the form the fork's README shows, where the base class declares them `protected virtual`. Both compile; the spec prescribes neither. -4. **A `Framework types:` entry leaves the handler set even where its header matches the signature.** The spec says such types are "graded by no framework rule" and, separately, that "a class matching the signature is a handler on that match alone", without ordering the two. The plan orders them — the exemption wins — because every framework-owned class implementing the framework's own context interfaces would otherwise be graded as an action wherever the framework is vendored as source outside `vendor-paths:`; the metadata-driven framework ships two such classes, `MetadataTriggerHandler` and `TriggerActionFlow` (`TriggerActionFlow.cls:24`), and the second has a `Trigger_Action__mdt` record only where a Flow action is registered. The first spec sentence licenses the order; the spec's silence on it is reported. Task 1 carries the sentence in both the `Framework types:` bullet and the handler-set definition, and Task 5 names the instances. +4. **A `Framework types:` entry leaves the handler set even where its header matches the signature.** The spec says such types are "graded by no framework rule" and, separately, that "a class matching the signature is a handler on that match alone", without ordering the two. The plan orders them — the exemption wins — because every framework-owned class implementing the framework's own context interfaces would otherwise be graded as an action wherever the framework is vendored as source outside `vendor-paths:`; the metadata-driven framework ships two such classes, `MetadataTriggerHandler` and `TriggerActionFlow` (`TriggerActionFlow.cls:23`), and the second has a `Trigger_Action__mdt` record only where a Flow action is registered. The first spec sentence licenses the order; the spec's silence on it is reported. Task 1 carries the sentence in both the `Framework types:` bullet and the handler-set definition, and Task 5 names the instances. 5. **The hub does not say "as a minor rule".** The spec's Naming rule reads "each shipped document carries that name as a rule of its own, at minor severity"; a shipped surface restating a grade outside the tag is what `.claude/rules/standards-rule-tags.md` forbids, so the hub says "as a rule of its own" and the three documents' tags carry the grade. Not a spec defect — the tag rule binds plugin content, not design documents — but the phrase does not travel. +6. **The fork's silent stop is placed where the source puts it.** The spec's Evidence section says the commented-out `throw` "sits commented out beside the line `// Do not throw an exception…`". In the fork's source the comment sits in `run()` and the commented `throw` inside `incrementCheckLoopCount()`, and the `System.debug` there fires only with `showDebug` on, which defaults to `false` — so by default nothing is logged at all. Task 4's table says so; the spec's sentence is reported as a provenance correction for its ledger. ## File structure @@ -731,7 +732,7 @@ trigger context: ```apex List orders = new List{ new Order(Status = null) }; OrderTriggerHandler.handle(System.TriggerOperation.BEFORE_INSERT, orders, null); -System.assertEquals('Draft', orders[0].Status); +Assert.areEqual('Draft', orders[0].Status); ``` To disable the handler in a test of other code that inserts orders, @@ -900,9 +901,10 @@ framework, graded by no rule here, and exempt from ## Context access -The base class reads `Trigger.isExecuting` and `Trigger.operationType` -in `run()` to pick the context method — which is why it is a framework -type. The handler reads `Trigger.new` and `Trigger.oldMap` once, in its +The base class reads `Trigger.isExecuting` and the context flags in +`run()` to pick the context method — `Trigger.operationType` in the +fork, `Trigger.isBefore`, `Trigger.isInsert` and their siblings in the +original — which is why it is a framework type. The handler reads `Trigger.new` and `Trigger.oldMap` once, in its constructor, into typed fields; the context methods and everything below them read nothing from `Trigger.*`. @@ -932,7 +934,7 @@ products part, and the difference sits in one private method of | Product | Method | Behaviour | |---|---|---| | original | `addToLoopCount()` | throws `TriggerHandlerException('Maximum loop count of N reached in ')`, and the DML fails | -| fork | `incrementCheckLoopCount()` | returns without running the handler; a `System.debug` line is the only trace, and the `throw` sits commented out beside `// Do not throw an exception if we exceed the loop count - just stop executing` | +| fork | `incrementCheckLoopCount()` | returns without running the handler; the `throw` sits commented out inside that method, and the `System.debug` line fires only with `showDebug` on — by default nothing is logged | So `setMaxLoopCount(1)` is a recursion guard under the original and silences automation under the fork. The discriminator is read from @@ -1199,7 +1201,7 @@ method with records built in memory, no DML and no metadata read: ```apex List orders = new List{ new Order(Status = null) }; new OrderTriggerHandler().beforeInsert(orders); -System.assertEquals('Draft', orders[0].Status); +Assert.areEqual('Draft', orders[0].Status); ``` To disable the handler in a test of other code that inserts orders, @@ -1487,7 +1489,7 @@ Find the two rows: Replace with one row: ``` -| Trigger and handler | Dispatch: hand the trigger context to the layers below — the shape, one trigger per object and the project's framework are `salesforce-triggers`'s | SOQL, DML, business logic itself | +| Trigger and handler | Dispatch: hand the trigger context to the layers below — the shape, one trigger per object and the project's framework are `salesforce-triggers`'s | SOQL, DML, business logic in the handler; the trigger body is `trigger-body-delegates`'s to grade | ``` Delete the bullet: @@ -1625,7 +1627,7 @@ grep -c 'salesforce-triggers' "$s" grep -c '^[0-9]\. ' "$s" grep -c 'salesforce-triggers' "$c" grep -c '^[0-9]\. ' "$c" -grep -c 'from step 2' "$c" +grep -c 'decision from step 2' "$c" grep -c 'step 5' plugins/salesforce-standards/agents/salesforce-code-reviewer.md ``` @@ -1661,8 +1663,9 @@ Replace with: framework resolved for its own path. Step (c) is never taken here — a background agent asks nothing — and neither is the offer to write a declaration. A resolution record carried in the dispatch prompt is - verified against the declarations on disk; on a mismatch the files - are the authority. When resolution fails, grade the + verified against the declarations on disk; where they disagree, the + run reports the mismatch and grades by the declaration, the files + being the authority. When resolution fails, grade the framework-independent rules, skip framework rules rather than guess them, note `trigger framework: unresolved — framework-specific rules not graded` among the Summary's out-of-scope notes, and report @@ -1690,7 +1693,7 @@ Find the line beginning `2. Pre-dispatch first-create check` and insert before i to write the declaration before dispatching. ``` -Then renumber: the former step 2 becomes `3.`, 3 becomes `4.`, 4 becomes `5.`, 5 becomes `6.`; and in the new step 4 (the dispatch step) change `the directory-mode decision from step 2` to `the directory-mode decision from step 3`. +Then renumber: the former step 2 becomes `3.`, 3 becomes `4.`, 4 becomes `5.`, 5 becomes `6.`; and in the new step 4 (the dispatch step) change `the directory-mode decision from step 2` to `the resolution record from step 2, and the directory-mode decision from step 3` — the step enumerates what the prompt carries, and the record is now one of those things. - [ ] **Step 4: Measure after** @@ -1701,8 +1704,8 @@ grep -c 'salesforce-triggers' "$s" grep -c '^[0-9]\. ' "$s" grep -c 'salesforce-triggers' "$c" grep -c '^[0-9]\. ' "$c" -grep -c 'from step 2' "$c" -grep -c 'from step 3' "$c" +grep -c 'decision from step 2' "$c" +grep -c 'record from step 2, and the directory-mode decision from step 3' "$c" grep -c 'step 5' plugins/salesforce-standards/agents/salesforce-code-reviewer.md tr -s '[:space:]' ' ' < "$s" | grep -o 'trigger framework: unresolved' | wc -l ``` @@ -1765,13 +1768,15 @@ Then append after the paragraph's last line (`available, the toolchain facts abo ``` The project's trigger framework and its vendor code are declared in -the project, never in this rule: a line carrying the `trigger-framework:` -key and one carrying the `vendor-paths:` key, in the body of the root -`CLAUDE.md`, of a trigger directory's `CLAUDE.md`, or of a project rule -outside this payload — one framework per path, vendor prefixes at the -root. When the `salesforce-triggers` skill is available it reads both -keys and says how they rank; when it is not, the lines still record the -choice for the reader. +the project, never in this rule. A line carrying the `trigger-framework:` +key sits in the body of the root `CLAUDE.md`, of a trigger directory's +`CLAUDE.md`, or of a project rule outside this payload — one framework +per path. A line carrying the `vendor-paths:` key sits in a root home +alone — the root `CLAUDE.md` files or a project rule — as directory +prefixes; a copy in a subdirectory is not read. When the +`salesforce-triggers` skill is available it reads both keys and says how +they rank; when it is not, the lines still record the choice for the +reader. ``` - [ ] **Step 3: Measure after** @@ -1991,7 +1996,7 @@ Expected: three lines — `reference/framework-base-class.md`, `reference/framew - [ ] **Step 9: Report** -State the end state to the developer: ten commits on `feature/trigger-frameworks`, every check above at its expected value, the spec notes this plan surfaced (Deviations 1, 2, 4 and 5) for the spec's ledger, and one item for the release PR's notes — a project on the frameworkless shape this plugin shipped before now earns `trigger-body-delegates` and `trigger-frameworkless-entry-point` per trigger until it adds the `handle` entry point (the Gotcha in `framework-frameworkless.md` names the fix). Do not move the plan's `status`; the developer flips it. +State the end state to the developer: ten commits on `feature/trigger-frameworks`, every check above at its expected value, the spec notes this plan surfaced (Deviations 1, 2, 4, 5 and 6) for the spec's ledger, and one item for the release PR's notes — a project on the frameworkless shape this plugin shipped before now earns `trigger-body-delegates` and `trigger-frameworkless-entry-point` per trigger until it adds the `handle` entry point (the Gotcha in `framework-frameworkless.md` names the fix). Do not move the plan's `status`; the developer flips it. ## Developer rulings @@ -1999,6 +2004,20 @@ None yet. Rulings taken during execution are recorded here, one line each, dated ## Review rounds +### 2026-09-08 — plan-adversary, fable 5.1, concerns (round 3, full-document) + +- fixed 2026-09-08 — [Important] Both new test-isolation blocks asserted with legacy `System.assertEquals`, which the sibling skill grades in new tests while the reference file the plan tells the author to imitate uses `Assert.areEqual`; license: `salesforce-apex-testing/SKILL.md:86-87` (`apex-test-assertions.legacy-assert`) and `reference/test-patterns.cls:50`; both lines now read `Assert.areEqual` +- fixed 2026-09-08 — [Minor] The review skill's record-verification sentence dropped the spec's duty to report a mismatch; license: spec, "The resolution record", "reports the mismatch and grades by the declaration, the files being the authority"; the sentence now carries both halves. The finding's second half is held below +- held 2026-09-08 — [Minor] What a record whose source is `asked` or `inferred` means to the review run when no declaration covers the path — the developer may decline the offer to declare, and the spec never says whether such a record stands or the run reports the framework unresolved; the session reads the spec's "Dispatched any other way it resolves for itself" as implying a passed record stands, and puts the reading to the developer rather than writing it +- fixed 2026-09-08 — [Minor] The toolchain rule's paragraph named a trigger directory's `CLAUDE.md` and a project rule as homes for the `vendor-paths:` line too, while the hub reads that key at the root alone; license: the hub's own `vendor-paths:` section and the spec's ruling that the key resolves at the repository root; the paragraph now gives each key its own homes +- fixed 2026-09-08 — [Minor] The command's renumbered dispatch step still enumerated the prompt's contents without the resolution record the new step 2 adds — the same defect one cell below the edit; license: the spec's review-surface paragraph, which passes the record in the prompt; the enumeration now names it, and Task 8's two checks read the new phrase +- fixed 2026-09-08 — [Minor] The merged layers row kept a gradeable trigger-body clause in `apex-layering`, so a trigger body with SOQL earned two findings from two skills; license: `.claude/rules/standards-rule-tags.md`, "Cross-skill mentions of a rule cite the owning rule id and defer to it"; the Never-does cell now scopes to the handler and defers the body to `trigger-body-delegates` by id +- held 2026-09-08 — [Minor] `member: handle(System.TriggerOperation` silently unselects a handler written `handle(TriggerOperation …)`, a compile-identical form; the spec prescribes the pattern (question 1's frameworkless example), so widening it to `member: TriggerOperation` on Deviation 1's own grounds is the developer's call against adding a Gotcha that says the entry point is written `System.TriggerOperation` verbatim; the session recommends the Gotcha, which conforms to the spec +- fixed 2026-09-08 — [Minor] Deviation 4 cited `TriggerActionFlow.cls:24`, off by one: the header sits on line 23, which round two's report cited correctly, and the session's own citation check — a fetch summarised by a small model — reported 24 and rewrote a correct citation into a wrong one; license: the fetched source, now in the session's scratchpad, line 23; the citation reads `:23` again, and the round-two ledger line's parenthesis stands as the record of the slip +- fixed 2026-09-08 — [Minor] Task 4's fork row placed the commented-out `throw` beside the `// Do not throw…` comment, where the source puts the comment in `run()` and the `throw` inside `incrementCheckLoopCount()`, and called a `System.debug` the only trace where it fires only with `showDebug` on, default `false`; license: the fork's source (`TriggerHandler.cls:125`, `:636-646`, `:71`); the row now says so, and Deviation 6 reports the spec's matching sentence as a provenance correction +- fixed 2026-09-08 — [Minor] Task 4's Context-access paragraph said the base class reads `Trigger.operationType`, true of the fork alone — the original picks the context from `Trigger.isBefore`, `Trigger.isInsert` and their siblings and never reads `operationType`; license: both sources; the paragraph names both forms +- signal 2026-09-08 — another round does not repay its cost as diff-scoped: the Important was a two-token edit licensed by a sibling skill's tag, six Minors are lexical and each checkable against its cited line, and two Minors are decisions the spec did not take and belong to the developer. If a round follows the developer's answers it is full-document only — a diff-scoped LGTM on a plan renews the confirming debt rather than discharging it. The class this round hunted, the same defect one cell from the previous fix, appeared three times and once in reverse (a fix wave rewrote a correct citation into a wrong one); the round-two risk — a re-introduced contradiction between the grammar bullet and the handler-set definition — did not return + ### 2026-09-08 — plan-adversary, fable 5.1, concerns (round 2, diff-scoped) - fixed 2026-09-08 — [Important] The round-one fix named one instance of the class it repaired while the framework ships a second top-level class matching `header: TriggerAction.` — `TriggerActionFlow`, implementing all seven context interfaces at `TriggerActionFlow.cls:24` (the report cited `:23`), with a `Trigger_Action__mdt` record only where a Flow action is registered — so `action-registered` still fired on the framework vendored as source; license: Deviation 4's own reason, which the finding shows was incomplete rather than wrong; `TriggerActionFlow` joins `Framework types:`, Task 5 and Deviation 4 name the class of instance and both instances, and `TriggerActionFlowChangeEvent extends TriggerActionFlow` is named as a closure candidate that stays out From 94fd21ca672776062d6fcb877fe3526897658468 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 20:37:37 +0200 Subject: [PATCH 021/120] docs: record two developer rulings on the trigger-frameworks plan --- ...2026-09-08-salesforce-trigger-frameworks.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md index 8ed6db5..664cb12 100644 --- a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md +++ b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md @@ -782,6 +782,10 @@ source: this standard) a context the trigger does not declare. - The `disabled` flag is per handler class. A test that inserts several objects disables each handler it must silence. +- The entry point is written `handle(System.TriggerOperation …)` + verbatim. The signature is a substring test, so + `handle(TriggerOperation …)` — identical to the compiler — matches no + signature and leaves the class unselected. - The shape this plugin shipped before `salesforce-triggers` existed — static `handleBeforeInsert` methods and a trigger branching on `Trigger.isBefore` — now earns two findings on the trigger, @@ -1665,7 +1669,10 @@ Replace with: a declaration. A resolution record carried in the dispatch prompt is verified against the declarations on disk; where they disagree, the run reports the mismatch and grades by the declaration, the files - being the authority. When resolution fails, grade the + being the authority. A record whose source is `asked` or `inferred` + stands where no declaration covers the path — step (c) already ran + in the session that dispatched — and `trigger-framework-declared` + still lands in `## Project`. When resolution fails, grade the framework-independent rules, skip framework rules rather than guess them, note `trigger framework: unresolved — framework-specific rules not graded` among the Summary's out-of-scope notes, and report @@ -2000,7 +2007,10 @@ State the end state to the developer: ten commits on `feature/trigger-frameworks ## Developer rulings -None yet. Rulings taken during execution are recorded here, one line each, dated. +One line each, dated. Rulings taken during execution join the list. + +- 2026-09-08 — A resolution record whose source is `asked` or `inferred`, passed in the review dispatch prompt, stands where no declaration covers the path; the run grades framework rules by it and still reports `trigger-framework-declared` in `## Project`. +- 2026-09-08 — The frameworkless signature stays `member: handle(System.TriggerOperation` as the spec prescribes; the document carries a Gotcha that the entry point is written verbatim, rather than the pattern widening to `member: TriggerOperation`. ## Review rounds @@ -2008,11 +2018,11 @@ None yet. Rulings taken during execution are recorded here, one line each, dated - fixed 2026-09-08 — [Important] Both new test-isolation blocks asserted with legacy `System.assertEquals`, which the sibling skill grades in new tests while the reference file the plan tells the author to imitate uses `Assert.areEqual`; license: `salesforce-apex-testing/SKILL.md:86-87` (`apex-test-assertions.legacy-assert`) and `reference/test-patterns.cls:50`; both lines now read `Assert.areEqual` - fixed 2026-09-08 — [Minor] The review skill's record-verification sentence dropped the spec's duty to report a mismatch; license: spec, "The resolution record", "reports the mismatch and grades by the declaration, the files being the authority"; the sentence now carries both halves. The finding's second half is held below -- held 2026-09-08 — [Minor] What a record whose source is `asked` or `inferred` means to the review run when no declaration covers the path — the developer may decline the offer to declare, and the spec never says whether such a record stands or the run reports the framework unresolved; the session reads the spec's "Dispatched any other way it resolves for itself" as implying a passed record stands, and puts the reading to the developer rather than writing it +- fixed 2026-09-08 — [Minor] What a record whose source is `asked` or `inferred` means to the review run when no declaration covers the path — the developer may decline the offer to declare, and the spec never says whether such a record stands or the run reports the framework unresolved; ruling: 2026-09-08 — the record stands, step (c) having run in the session that dispatched, and the declaration finding still lands in `## Project`; Task 8's paragraph now says so - fixed 2026-09-08 — [Minor] The toolchain rule's paragraph named a trigger directory's `CLAUDE.md` and a project rule as homes for the `vendor-paths:` line too, while the hub reads that key at the root alone; license: the hub's own `vendor-paths:` section and the spec's ruling that the key resolves at the repository root; the paragraph now gives each key its own homes - fixed 2026-09-08 — [Minor] The command's renumbered dispatch step still enumerated the prompt's contents without the resolution record the new step 2 adds — the same defect one cell below the edit; license: the spec's review-surface paragraph, which passes the record in the prompt; the enumeration now names it, and Task 8's two checks read the new phrase - fixed 2026-09-08 — [Minor] The merged layers row kept a gradeable trigger-body clause in `apex-layering`, so a trigger body with SOQL earned two findings from two skills; license: `.claude/rules/standards-rule-tags.md`, "Cross-skill mentions of a rule cite the owning rule id and defer to it"; the Never-does cell now scopes to the handler and defers the body to `trigger-body-delegates` by id -- held 2026-09-08 — [Minor] `member: handle(System.TriggerOperation` silently unselects a handler written `handle(TriggerOperation …)`, a compile-identical form; the spec prescribes the pattern (question 1's frameworkless example), so widening it to `member: TriggerOperation` on Deviation 1's own grounds is the developer's call against adding a Gotcha that says the entry point is written `System.TriggerOperation` verbatim; the session recommends the Gotcha, which conforms to the spec +- fixed 2026-09-08 — [Minor] `member: handle(System.TriggerOperation` silently unselects a handler written `handle(TriggerOperation …)`, a compile-identical form; the spec prescribes the pattern (question 1's frameworkless example), so widening it was the developer's call against a Gotcha; ruling: 2026-09-08 — the pattern stays as the spec prescribes, and `framework-frameworkless.md` gains a Gotcha saying the entry point is written `System.TriggerOperation` verbatim because the signature is a substring test - fixed 2026-09-08 — [Minor] Deviation 4 cited `TriggerActionFlow.cls:24`, off by one: the header sits on line 23, which round two's report cited correctly, and the session's own citation check — a fetch summarised by a small model — reported 24 and rewrote a correct citation into a wrong one; license: the fetched source, now in the session's scratchpad, line 23; the citation reads `:23` again, and the round-two ledger line's parenthesis stands as the record of the slip - fixed 2026-09-08 — [Minor] Task 4's fork row placed the commented-out `throw` beside the `// Do not throw…` comment, where the source puts the comment in `run()` and the `throw` inside `incrementCheckLoopCount()`, and called a `System.debug` the only trace where it fires only with `showDebug` on, default `false`; license: the fork's source (`TriggerHandler.cls:125`, `:636-646`, `:71`); the row now says so, and Deviation 6 reports the spec's matching sentence as a provenance correction - fixed 2026-09-08 — [Minor] Task 4's Context-access paragraph said the base class reads `Trigger.operationType`, true of the fork alone — the original picks the context from `Trigger.isBefore`, `Trigger.isInsert` and their siblings and never reads `operationType`; license: both sources; the paragraph names both forms From 18a87c40e4ad3bf80d18a2340673de6989ed0602 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 20:40:28 +0200 Subject: [PATCH 022/120] docs: approve the trigger-frameworks implementation plan --- docs/plans/2026-09-08-salesforce-trigger-frameworks.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md index 664cb12..f63e7d2 100644 --- a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md +++ b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md @@ -1,8 +1,8 @@ --- ticket: none date: 2026-09-08 -status: draft -adversary: concerns +status: approved +adversary: concerns (resolved 2026-09-08) spec: ../specs/2026-09-08-salesforce-trigger-frameworks-design.md branch: feature/trigger-frameworks base: develop @@ -2027,6 +2027,7 @@ One line each, dated. Rulings taken during execution join the list. - fixed 2026-09-08 — [Minor] Task 4's fork row placed the commented-out `throw` beside the `// Do not throw…` comment, where the source puts the comment in `run()` and the `throw` inside `incrementCheckLoopCount()`, and called a `System.debug` the only trace where it fires only with `showDebug` on, default `false`; license: the fork's source (`TriggerHandler.cls:125`, `:636-646`, `:71`); the row now says so, and Deviation 6 reports the spec's matching sentence as a provenance correction - fixed 2026-09-08 — [Minor] Task 4's Context-access paragraph said the base class reads `Trigger.operationType`, true of the fork alone — the original picks the context from `Trigger.isBefore`, `Trigger.isInsert` and their siblings and never reads `operationType`; license: both sources; the paragraph names both forms - signal 2026-09-08 — another round does not repay its cost as diff-scoped: the Important was a two-token edit licensed by a sibling skill's tag, six Minors are lexical and each checkable against its cited line, and two Minors are decisions the spec did not take and belong to the developer. If a round follows the developer's answers it is full-document only — a diff-scoped LGTM on a plan renews the confirming debt rather than discharging it. The class this round hunted, the same defect one cell from the previous fix, appeared three times and once in reverse (a fix wave rewrote a correct citation into a wrong one); the round-two risk — a re-introduced contradiction between the grammar bullet and the handler-set definition — did not return +- resolved 2026-09-08 — the developer closed the loop by annotation rather than a fourth round: every disposition above is applied, the two held items carry their rulings, the simulated end state passes every check, and the round's own signal said another read would not repay its cost. `status` moves to `approved` in the same edit ### 2026-09-08 — plan-adversary, fable 5.1, concerns (round 2, diff-scoped) From be3e62acabcdc0607189c2b3efd0a70df8ce95e7 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 20:43:51 +0200 Subject: [PATCH 023/120] feat(salesforce-standards): add the salesforce-triggers skill with its framework-independent rules --- .../skills/salesforce-triggers/SKILL.md | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md diff --git a/plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md b/plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md new file mode 100644 index 0000000..c51ef78 --- /dev/null +++ b/plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md @@ -0,0 +1,223 @@ +--- +name: salesforce-triggers +description: Use when writing or reviewing Apex triggers — one trigger per object, what belongs in the trigger body, resolving which trigger framework the project uses, and loading the matching framework guidance. Apex classes, layering and bulkification belong to salesforce-apex; Apex tests to salesforce-apex-testing. +--- + +# Salesforce triggers + +Standards for Apex triggers that hold whatever trigger framework a +project uses, and the protocol that resolves which framework governs +the file at hand. Cite rules in review findings as +`(standard: salesforce-triggers, rule: )`, the rules of a framework +document included; each rule names its source. Framework-specific +guidance — dispatch shape, bypass API, test isolation — lives in a +per-framework document under `reference/`, loaded once the framework is +resolved. Before the first trigger edit, and before grading the first +Apex file, run the protocol under "Resolving the framework" and write +the resolution record. + +The layers below the handler — domain, service, selector — and +bulkification belong to `salesforce-apex`; Apex tests to +`salesforce-apex-testing`. + +## Rules + +These rules hold under every framework. Their ids carry the `trigger-` +prefix; a framework document's rules carry `trigger--`. + +### One trigger per object + +An object has one `.trigger` file. Several handler calls inside that +file are correct, in the order they must run; the violation is a second +trigger file, never a second line. Scope: triggers under the +directories `packageDirectories` lists and outside `vendor-paths:` — +stated by path, because a vendor's trigger on a standard object belongs +to the vendor by path and to nobody by ownership. + +(id: `trigger-one-per-object`; severity: critical; kind: defect; source: +Salesforce Well-Architected, record-triggered decision guide; Apex +Developer Guide p. 282) + +Sub-rules: +- the two triggers declare disjoint contexts (id: + `trigger-one-per-object.disjoint-contexts`; severity: important) + +Two triggers whose declared contexts overlap fire in an order the +platform leaves undefined, so the same code yields different results +between runs. Disjoint contexts carry no ordering risk today, yet they +split an object's automation across files and sit one context +declaration away from the critical case. + +### The trigger body delegates + +The body contains delegating calls and nothing else — no conditional, +loop or field logic. + +(id: `trigger-body-delegates`; severity: important; source: Apex +Developer Guide, "Document Your Apex Code") + +### Context stops at the handler + +Nothing below the handler reads `Trigger.*`; context descends as +parameters, which keeps the logic testable without DML. The types a +framework document lists under `Framework types:` are exempt — a base +class reading `Trigger.*` is the framework doing its job. This rule +needs a handler set, so it goes ungraded wherever no signature supplies +one. + +(id: `trigger-context-below-handler`; severity: important; source: this +standard) + +### Naming + +A trigger is named `Trigger`. Pre-existing non-conforming names +stay; the convention binds new code. The dispatching class is named by +the framework document, which carries that name as a rule of its own. + +(id: `trigger-naming`; severity: minor; source: this standard) + +### The framework is declared + +The project declares its trigger framework — the declaration under +"Resolving the framework". The finding lands in the report's +`## Project` section, and lands there even when inference succeeded: +inference resolves a session, a declaration resolves the project. + +(id: `trigger-framework-declared`; severity: minor; source: this +standard) + +Sub-rules: +- two declarations cover one path with different values and neither is + a subtree strictly containing the other, so neither wins (id: + `trigger-framework-declared.ambiguous`; severity: important) + +The group default grades an absent declaration, which announces itself. +The sub-rule grades every case the protocol cannot rank — a glob-scoped +rule against a subtree-scoped `CLAUDE.md`, two rules whose `paths:` +differ, `CLAUDE.md` beside `.claude/CLAUDE.md` in one directory. A root +default with a nearer subtree override is not this finding: the inner +one wins, and the disagreement is stated in the resolution record. +Declarations that agree never reach the rule. The two dispositions are +mutually exclusive: a path has no declaration, or it has two the +protocol cannot rank. + +### Bulkification + +Owned by `salesforce-apex`: `apex-bulkification` and +`apex-bulkification.loop-on-trigger-path` grade the trigger path, and +this skill carries no grade of its own for it. + +## Framework documents + +One framework id names each shipped framework everywhere it appears — +declaration value, fingerprint row, document filename, rule prefix. A +shipped id names an approach, never a product: two base-class +frameworks share one document and one prefix. + +| `framework-id` | Document | Rule prefix | +|---|---|---| +| `base-class` | `reference/framework-base-class.md` | `trigger-base-class-` | +| `metadata-driven` | `reference/framework-metadata-driven.md` | `trigger-metadata-driven-` | +| `frameworkless` | `reference/framework-frameworkless.md` | `trigger-frameworkless-` | + +An id absent from this table is a framework this plugin does not ship. +Its declaration carries a `doc-path:` or a `skill:` locator, and this +table never grows to hold one. A `doc-path:` is read as a file relative +to the repository root; a `skill:` is invoked by its `plugin:skill` +name. A locator naming nothing takes the missing-document row of the +failure table. A document from outside this plugin is graded on the +rules above alone, never on its own framework rules; a signature +written in the grammar below buys it `trigger-context-below-handler`. + +### What a document answers + +Four questions, and a fifth for the documents this plugin ships. A +missing answer is stated to the developer, never filled from another +framework. + +1. **Dispatch** — the trigger body and the handler shape, as two files + calling the same lower-layer methods `salesforce-apex`'s + `reference/order-layers.md` defines, plus the **signature** that + makes a class one of this framework's handlers and the framework's + **own types**, which are neither handlers nor layers below one. +2. **Context access** — where the handler reads `Trigger.*`. +3. **Bypass and recursion** — the API names *and* what happens when a + limit is exceeded. `setMaxLoopCount(1)` guards under one base-class + framework and silences automation under another; names alone cannot + tell a reviewer which. +4. **Test isolation** — how to call the handler without DML, and how to + disable it in a test. +5. **Fingerprint** — what identifies this framework in code: the row + the document contributes to the fingerprint table below. The + frameworkless answer is that nothing identifies it. + +A document may add framework-specific rules, tagged like any rule here +under its prefix, and each shipped document carries the name of its +dispatching class as a rule of its own. A document carrying no rules is +graded by the rules above alone, and the report says so. + +### The signature + +The answer to question 1 is a block under a fixed heading, so a +session loading a document written elsewhere can tell whether the +contract is met — the base-class answer in full: + +``` +Signature: + header: extends TriggerHandler +Framework types: TriggerHandler +``` + +- `header:` matches the type's declaration header — from the `class` + or `interface` keyword to the opening brace, however the lines break. + Modifiers before the keyword (`abstract`, `virtual`, `global`) sit + outside the match. A comment, a string or a javadoc mention never + counts. +- `member:` matches a method declaration line inside the type — where + `handle(System.TriggerOperation`, the frameworkless answer, belongs. +- Matching ignores case, because Apex does. An optional namespace + prefix is allowed before every type name: `extends acme.TriggerHandler` + matches `extends TriggerHandler`. +- `Framework types:` names what the framework itself owns — a copied + base class, a dispatcher. Absent and empty say the same thing: + nothing. Its types are read to recognise the framework, graded by no + framework rule, and exempt from `trigger-context-below-handler`. A + type named here leaves the handler set even where its own header + matches the signature: a framework class implementing the framework's + own interfaces is the framework, not a handler. +- A filename glob may be added as a hint, never as the test. +- Only a malformed `Signature:` block fails the grammar. + +A signature and a fingerprint share a technique and nothing else. A +fingerprint reads the whole project, matches anywhere, and yields a +label. A signature reads one class, matches in a named place, and +yields membership. + +**The handler set** is the smallest fixed point of the classes matching +the signature, less the types `Framework types:` names, together with +the classes whose header `extends` a type already in the set — so +`OrderHandler extends BaseTriggerHandler`, an org's own layer over the +framework's, is graded. A class matching the signature is a handler on +that match alone unless `Framework types:` names it. The parent rules +govern **closure candidates** only — a class matching no signature whose header +carries `extends`: + +- the parent is read from the repository even where it lies outside a + diff-scoped run, so a full run and a diff-scoped run select the same + handlers; +- a parent under a vendor path is read to recognise it and never + graded; +- a parent absent from the repository leaves the candidate a + non-handler. Such parents are reported in aggregate: one Summary line + naming the distinct parents with a count each, ungraded. Most are + platform types, `extends Exception` first. A namespace-qualified + parent is the one worth a second look — as a reading hint, never as + the test, since an unqualified absent parent can be the project's own + base class where the repository is a partial view of the org. + +Only top-level types are handlers. A document whose framework can +dispatch an inner type says so; such a type goes ungraded, and the +document says that too. + +Every test in this skill reduces to a grep, a prefix comparison or a +glob match — never to parsing. From d7d0f4950f38a39636d3a2bf1f7041bc2e1ae51b Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 21:30:54 +0200 Subject: [PATCH 024/120] fix(salesforce-standards): keep the disjoint-contexts prose free of a grade word --- .../salesforce-standards/skills/salesforce-triggers/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md b/plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md index c51ef78..0f183f4 100644 --- a/plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md +++ b/plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md @@ -46,7 +46,7 @@ Two triggers whose declared contexts overlap fire in an order the platform leaves undefined, so the same code yields different results between runs. Disjoint contexts carry no ordering risk today, yet they split an object's automation across files and sit one context -declaration away from the critical case. +declaration away from the case the group default grades. ### The trigger body delegates From e34dcc000b69c0675a07dcdee4bd79be97ba930e Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 21:32:50 +0200 Subject: [PATCH 025/120] docs: reword the disjoint-contexts prose in the trigger-frameworks plan per the task-one review --- docs/plans/2026-09-08-salesforce-trigger-frameworks.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md index f63e7d2..7448496 100644 --- a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md +++ b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md @@ -140,7 +140,7 @@ Two triggers whose declared contexts overlap fire in an order the platform leaves undefined, so the same code yields different results between runs. Disjoint contexts carry no ordering risk today, yet they split an object's automation across files and sit one context -declaration away from the critical case. +declaration away from the case the group default grades. ### The trigger body delegates @@ -2011,6 +2011,7 @@ One line each, dated. Rulings taken during execution join the list. - 2026-09-08 — A resolution record whose source is `asked` or `inferred`, passed in the review dispatch prompt, stands where no declaration covers the path; the run grades framework rules by it and still reports `trigger-framework-declared` in `## Project`. - 2026-09-08 — The frameworkless signature stays `member: handle(System.TriggerOperation` as the spec prescribes; the document carries a Gotcha that the entry point is written verbatim, rather than the pattern widening to `member: TriggerOperation`. +- 2026-09-08 — Task 1's prose under `.disjoint-contexts` no longer reads "the critical case", which the Task 1 reviewer flagged as a grade restated outside a tag; it reads "the case the group default grades". The spec's Framework-independent rules carry the original phrase and are reported, not edited. ## Review rounds From fd33d17cc028e8b8cf4f6a5be02b1d7aab8f5715 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 21:36:10 +0200 Subject: [PATCH 026/120] feat(salesforce-standards): add the trigger-framework resolution protocol --- .../skills/salesforce-triggers/SKILL.md | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md b/plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md index 0f183f4..82db0af 100644 --- a/plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md +++ b/plugins/salesforce-standards/skills/salesforce-triggers/SKILL.md @@ -107,6 +107,212 @@ Owned by `salesforce-apex`: `apex-bulkification` and `apex-bulkification.loop-on-trigger-path` grade the trigger path, and this skill carries no grade of its own for it. +## Resolving the framework + +Three things are read from outside this skill, and they are not one +mechanism. A **signature** selects over a class's content and comes +from the framework document. A **declaration** resolves over a path, +comes from the project in several places, and carries a scope. +**`vendor-paths:`** filters over a path, comes from the repository root, +and carries no scope. Each is specified on its own; none inherits a +neighbour's rules. + +### The declaration + +One line, one key, in the body of a home file at the start of a line: + +``` +trigger-framework: base-class +trigger-framework: metadata-driven +trigger-framework: frameworkless +trigger-framework: acme-dispatcher; doc-path: docs/acme-triggers.md +trigger-framework: acme-dispatcher; skill: acme-plugin:acme-triggers +vendor-paths: force-app/nebula, force-app/vendor +``` + +- The key sits in the file's body, never in a rule's frontmatter, and a + line inside a fenced code block does not count — which is how this + file quotes the key without declaring one. +- One declaration per home, one value per key. The value is everything + after the key, locator included: two homes naming one framework + through different documents disagree. +- A shipped id carrying a locator is a contradiction: the shipped + document is used and the locator reported. An id absent from the + table below carrying no locator is a homegrown framework without a + document. +- Two `trigger-framework:` lines in one home make that home + unreadable; it is reported with the file, never resolved by picking a + line. + +**Homes** — the exact set a grep reads: `CLAUDE.md` and +`.claude/CLAUDE.md` at the repository root or in any directory above +the file, plus any rule file under `.claude/rules/` that sits outside a +directory carrying a Rules-engine manifest (`.manifest.json`). Grep +these files rather than waiting for the platform to load them: a +`CLAUDE.md` in a subdirectory loads only when a file there is read, +which a session about to write its first trigger has not done. + +Not homes: a personal rule under `~/.claude/rules/`, which would follow +the developer between projects that disagree; `CLAUDE.local.md`, +personal and git-ignored, so a declaration there resolves the author's +sessions while every teammate and CI run reports it missing; and the +installed `salesforce-toolchain.md` copy, which the Rules engine owns +and overwrites at its next sync. + +**Scope.** A `CLAUDE.md` scopes by position — the subtree of its +directory, and for `.claude/CLAUDE.md` the subtree of the directory +holding `.claude/`. A rule declaring `paths:` scopes by glob; a rule +declaring none scopes the whole repository, which is the root subtree +and ranks as one. Where a glob's coverage cannot be settled, the +pattern counts as covering: apply the matching the platform applies and +lean on `**`, `*`, `{a,b}` and root-relative paths alone. + +**Ranking**, in three moves, every declaration keeping its own scope: + +1. Count the distinct values. A root `CLAUDE.md` and a repository-wide + rule naming the same framework agree; the record names both. +2. One distinct value — resolved. +3. More than one — the winner is the declaration whose scope is + strictly contained in the scope of every declaration carrying a + different value: subtree within subtree by path prefix, never a pair + involving a glob. No such declaration means no winner, which the + failure table turns into behaviour. `CLAUDE.md` beside + `.claude/CLAUDE.md` in one directory carry equal subtrees, so + neither contains the other; two rules with differing `paths:` are + the same case. + +### `vendor-paths:` + +The second key, and the only other thing read from the project. It +lists what belongs to a package the project did not write, and the +scope is **declared, never inferred**: a package installed unlocked and +without a namespace, vendored as source, carries no platform signal +that marks it foreign. + +- It resolves at the **repository root** alone. Every root home + carries it — both root `CLAUDE.md` files and every rule file outside + a payload directory, whatever its `paths:` — and several homes union. + A scope rule would get an exclusion backwards. +- Its value is a list of **directory prefixes** relative to the root, + not globs: a file is vendor when its path equals an entry or begins + with the entry and a slash. That is the shape `packageDirectories` + uses, so the two exclusion layers compose in one arithmetic. +- An absent key and an empty one say the same thing: nothing is vendor. + An entry matching nothing on disk earns a note, the only signal an + agent can give for a typo. An entry outside every package directory + is redundant and passes in silence. +- Vendor code is never graded and never fingerprinted, and it is still + **read** where recognition needs it: the discriminator separating the + two base-class frameworks is a method name inside + `TriggerHandler.cls`, which a project vendoring the framework as + source puts under a vendor path. + +### Steps + +Resolution runs per file. For a trigger it decides which framework's +rules grade that trigger; for a class, the same from the declarations +covering the class's own path. Each step consumes what the one before +it produced. + +| Step | Action | +|---|---| +| (0) | read `vendor-paths:` from the root homes and subtract it, with the directories outside `packageDirectories` (read from `sfdx-project.json`; where that file is absent this layer excludes nothing), from the repository's Apex files — what remains is the grading universe | +| (a) | grep the covering homes for `trigger-framework:` and rank them | +| (b) | fingerprint the grading universe, reading a framework's own types outside it where a fingerprint row points at a type `Framework types:` names | +| (c) | ask the developer, with `reference/choosing-a-framework.md` | +| (d) | select the resolved framework's handlers from the grading universe by its signature, closed over `extends` — the review surface alone | + +Steps (0) through (c) serve a session writing a trigger and a review +run alike. A session about to write a trigger stops at the resolved +document; a review run takes step (d). + +**Who repairs what.** Step (c) and a collision the protocol cannot rank +both need a person, so both belong to an interactive session. A +background agent asks nothing: it performs (0), (a) and (b) as +mechanical reads, enforces the rules it can resolve, and reports what +it cannot — step (c) and the offer to write a declaration are what it +never takes. A session does not settle an ambiguity in conversation +either, where the answer dies at the next compaction: it rescopes or +removes one of the competing homes until the declaration surface states +one framework per path. The fix is an edit, not an answer. + +### Fingerprints + +| Framework | Pattern | +|---|---| +| base-class | `extends TriggerHandler` with `new X().run()` | +| metadata-driven | `new MetadataTriggerHandler().run()` with `Trigger_Action__mdt` and `sObject_Trigger_Setting__mdt` records | +| fflib | `fflib_SObjectDomain.triggerHandler(` in the trigger body | +| TDTM | `TDTM_Config_API.run(` | +| dispatcher | `TriggerDispatcher.Run(`, or a handler implementing an interface with `IsDisabled()` | +| frameworkless | no pattern is not a fingerprint — go to step (c) | + +Every pattern tolerates an optional namespace prefix in the form the +token's kind takes: `ns.` before a class name, `ns__` before an object +or Custom Metadata API name. Only the first three rows carry shipped +ids; `fflib`, `TDTM` and `dispatcher` are recognition labels naming what +was found, and a label becomes an id only when a declaration carries +it. The TDTM row is unverified against a real NPSP org: the pattern may +sit inside the package's own triggers under a vendor path, and the +subscriber-side signal may be `extends npsp.TDTM_Runnable`. A miss there +drops to step (c) and asks. + +The base-class document separates two frameworks identical in the +trigger body and different in error semantics; its discriminator is a +method name inside `TriggerHandler.cls`, read when that document loads. + +### The resolution record + +Up to three lines, before the first trigger edit or the first graded +file: + +``` +Trigger framework: base-class — declared in force-app/billing/triggers/CLAUDE.md — loading framework-base-class.md +Vendor paths: 2 entries from CLAUDE.md +Handlers: 7 selected by the signature in framework-base-class.md +``` + +- The source reads `declared `, `inferred ` or `asked`. + It names every file that declares: where homes agree, all of them; + where a nearer declaration beat a repository-wide default, both with + the winner first. Where the document turns on a discriminator — the + base-class case — the variant is named on this line. +- Any source other than `declared` ends with an offer to write the + declaration, since a line in conversation dies at the next + compaction. This skill never remembers a resolution; after compaction + it reads the files again. +- The second and third lines say what the filter and the selector did, + degrading in place — `none declared`, or the reason no handler set + was selected. The `Handlers:` line appears only where step (d) ran. + Without them "no framework findings" and "framework rules not graded" + read identically. +- A monorepo yields one record per resolved framework with the paths it + covers. + +### Failure modes + +| Situation | Behaviour | +|---|---| +| No declaration, fingerprint matches | infer, cite the file and pattern, offer to write the declaration | +| No declaration, no fingerprint | go to (c), saying no pattern matched in N triggers and asking whether the project is frameworkless or on a framework this plugin does not know | +| No triggers at all | go to (c) | +| A fingerprint matches a framework this plugin ships nothing for — fflib, TDTM, a dispatcher | the label is sound and only the document is missing: offer to write a project document, never substitute another framework's | +| Two subtree-scoped declarations disagree | the inner one wins, and the disagreement is always stated — never resolved silently | +| Two declarations cover the file with different values and neither is a subtree containing the other | no winner: name every file that declares, then work out with the developer which home survives and write that change — picking one silently would answer a question only the project can | +| The resolved document answers question 1 without a signature, or with one outside the grammar | skip the framework rules and `trigger-context-below-handler`, grade the rest, and name the document in the Summary — guessing a signature would repeat the mistake a filename pattern makes | +| A class matches two frameworks' signatures | grade it under the framework resolved for its own path — step (a) over the class — and name the other in the Summary | +| A closure candidate's `extends` parent is not in the repository | treat the class as a non-handler and count it on the Summary's one line of distinct unreadable parents; a class matching the signature itself is unaffected | +| The base-class framework arrives as a managed package, so `TriggerHandler.cls` cannot be read | select handlers as usual, grade every rule except those keyed to error semantics, and name in the Summary that the discriminator was unreadable — a guess between silencing and throwing is the one guess that turns a correct bypass into silenced automation | +| A home carries two `trigger-framework:` lines, or a shipped id with a locator | the home resolves nothing and is named; a shipped id keeps its shipped document and the stray locator is reported | +| `vendor-paths:` is absent and a directory is plainly third-party | grade it as the project's, since nothing declares otherwise, and note the directory with its evidence in the Summary — a note, not a finding, because no rule requires the key | +| A `vendor-paths:` entry matches nothing on disk | note it; nothing distinguishes a typo from a directory yet to be added | +| The declared document is missing | say so and grade by the framework-independent rules; never substitute another framework's document, since guidance for the wrong framework writes code that does not compile | +| A homegrown framework with no document | offer to write one, describing their code rather than a pattern from the internet | + +Every note above lands among the Summary's out-of-scope notes, the slot +the review-report contract already defines; the declaration findings +land in `## Project`. + ## Framework documents One framework id names each shipped framework everywhere it appears — From e2be323e274d7cce7b0a5d45787d98069f242480 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 8 Sep 2026 21:42:29 +0200 Subject: [PATCH 027/120] feat(salesforce-standards): add the frameworkless trigger document --- .../reference/framework-frameworkless.md | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-frameworkless.md diff --git a/plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-frameworkless.md b/plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-frameworkless.md new file mode 100644 index 0000000..ac259e5 --- /dev/null +++ b/plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-frameworkless.md @@ -0,0 +1,163 @@ +# Frameworkless triggers + +Framework id `frameworkless`. No base class and no metadata: the +trigger hands its context to one static entry point, and the handler +switches on the operation. Every rule here is cited +`(standard: salesforce-triggers, rule: trigger-frameworkless-)`. + +## Dispatch + +The trigger reads the context once and passes it on. The handler owns +the switch, so `trigger-body-delegates` holds, and it takes the +operation as a parameter, so it is callable from a test without DML. + +### `OrderTrigger.trigger` + +```apex +trigger OrderTrigger on Order (before insert, before update, after update) { + OrderTriggerHandler.handle(Trigger.operationType, Trigger.new, Trigger.oldMap); +} +``` + +### `OrderTriggerHandler.cls` + +Routes each operation to the domain and service layers of +`salesforce-apex`'s `reference/order-layers.md`; no SOQL, no DML, no +business logic of its own. `with sharing`, the default for the handler +layer. + +```apex +public with sharing class OrderTriggerHandler { + + @TestVisible + private static Boolean disabled = false; + + public static void handle(System.TriggerOperation operation, List newOrders, Map oldMap) { + if (disabled) { + return; + } + switch on operation { + when BEFORE_INSERT { + OrderDomain.applyDefaults(newOrders); + } + when BEFORE_UPDATE { + OrderDomain.validateStatusTransitions(newOrders, oldMap); + } + when AFTER_UPDATE { + List activated = OrderDomain.filterNewlyActivated(newOrders, oldMap); + if (!activated.isEmpty()) { + OrderService.activateFulfillment(activated); + } + } + } + } +} +``` + +### Signature + +``` +Signature: + member: handle(System.TriggerOperation +``` + +No `Framework types:` — nothing here belongs to a framework. A handler +is any top-level class declaring a method whose line carries +`handle(System.TriggerOperation`. + +## Context access + +The trigger reads `Trigger.operationType`, `Trigger.new` and +`Trigger.oldMap` once and passes them as parameters. The handler reads +nothing from `Trigger.*`, and neither does anything below it. On +insert `Trigger.oldMap` is null, so the `BEFORE_INSERT` branch never +touches `oldMap`. + +## Bypass and recursion + +Nothing in this shape bypasses or counts. The `disabled` flag above is +the bypass: `@TestVisible`, so a test can set it, and private, so +production code cannot. Recursion is the handler's own duty. A handler +whose after-context work performs DML that re-enters the same trigger +keeps a static set of ids already processed in this transaction and +skips them: + +```apex +private static Set activatedThisTransaction = new Set(); +``` + +What happens when a limit is exceeded: there is no limit to exceed. An +unguarded re-entry loops until a governor limit throws +`System.LimitException`, which no code catches, and the whole +transaction fails. + +## Test isolation + +Call the entry point with records built in memory — no `insert`, no +trigger context: + +```apex +List orders = new List{ new Order(Status = null) }; +OrderTriggerHandler.handle(System.TriggerOperation.BEFORE_INSERT, orders, null); +Assert.areEqual('Draft', orders[0].Status); +``` + +To disable the handler in a test of other code that inserts orders, +set `OrderTriggerHandler.disabled = true` in the test and reset it in a +`finally` block. + +## Fingerprint + +Nothing identifies frameworkless code. The absence of every other +fingerprint is not a match: step (b) of the resolution protocol falls +through to step (c), which asks. + +## Rules + +**Dispatching class.** The handler is named `TriggerHandler`. +Pre-existing non-conforming names stay; the convention binds new code. + +(id: `trigger-frameworkless-dispatching-class`; severity: minor; +source: this standard) + +**One entry point.** Every call in the trigger body is a handler's +static `handle` method taking `System.TriggerOperation` and the +records — several handlers, several such calls. A call to a per-context +method instead — `handleBeforeInsert`, with or without a branch around +it — leaves that handler outside the signature, where no framework rule +reaches it. The rule is graded on the trigger for that reason: the +class it would grade cannot be selected. + +(id: `trigger-frameworkless-entry-point`; severity: important; source: +this standard) + +**Recursion guard.** A handler whose after-context work performs DML on +its own object, or on an object whose automation writes back, keeps a +static processed-id set and skips ids already seen. Nothing else in +this shape stops re-entry. + +(id: `trigger-frameworkless-recursion-guard`; severity: important; +source: this standard) + +## Gotchas + +- `Trigger.new` is a `List` and is assignable to a + `List` parameter without a cast; `Trigger.oldMap` to + `Map` likewise. +- A `switch on` an enum needs no `when else`; an unlisted operation + falls through and does nothing, which is the intended behaviour for + a context the trigger does not declare. +- The `disabled` flag is per handler class. A test that inserts several + objects disables each handler it must silence. +- The entry point is written `handle(System.TriggerOperation …)` + verbatim. The signature is a substring test, so + `handle(TriggerOperation …)` — identical to the compiler — matches no + signature and leaves the class unselected. +- The shape this plugin shipped before `salesforce-triggers` existed — + static `handleBeforeInsert` methods and a trigger branching on + `Trigger.isBefore` — now earns two findings on the trigger, + `trigger-body-delegates` and, once the project resolves as + `frameworkless`, `trigger-frameworkless-entry-point`, and its handler + matches no signature. One fix clears all three: keep the methods, add + one `handle` entry point that switches on the operation and calls + them, and make the trigger call it. From 1dc19f0673f758486a7d1a984fd024e3938a053e Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 13:48:07 +0200 Subject: [PATCH 028/120] docs: record the illustrative-fragment ruling in the trigger-frameworks plan --- docs/plans/2026-09-08-salesforce-trigger-frameworks.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md index 7448496..504cc72 100644 --- a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md +++ b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md @@ -27,7 +27,7 @@ base: develop - **Frontmatter safety:** no `description:` contains `: ` (colon-space); the two new descriptions are checked for it in Task 1 and Task 11. - **No version bump.** `plugins/salesforce-standards/.claude-plugin/plugin.json` stays at `0.3.1`; the release PR mints the number, and this topic does not dogfood. - **No eval files.** Neither `skill-creator` nor any agent writes `evals/trigger-evals.json` or any other eval file under the plugin — a standing decision of this repo. `skill-creator` may be used for `description:` tuning only. -- **Apex examples are valid, copy-pasteable Apex** for the file named above them, in the style of the existing `reference/` files: four-space indent, one class per block, explicit sharing keyword. No compiler runs here, so every block is written once into this plan and copied verbatim into the file — a fix to a shipped block is back-ported into this plan in the same wave. +- **Apex examples are valid, copy-pasteable Apex** for the file named above them, in the style of the existing `reference/` files: four-space indent, one class per block, explicit sharing keyword. The constraint binds the blocks a `### \`\`` heading names; an illustrative fragment under a prose heading — a field, a test snippet — is not a file and need not compile alone, as the shipped `bulkification.md` before/after pairs already do not (developer ruling, 2026-09-09). No compiler runs here, so every block is written once into this plan and copied verbatim into the file — a fix to a shipped block is back-ported into this plan in the same wave. - **Every framework document calls the same four lower-layer methods** the renamed `order-layers.md` defines: `OrderDomain.applyDefaults(List)`, `OrderDomain.validateStatusTransitions(List, Map)`, `OrderDomain.filterNewlyActivated(List, Map)` returning `List`, and `OrderService.activateFulfillment(List)`. Nothing else is called from a handler. - **`claude plugin validate .` and `claude plugin validate plugins/salesforce-standards` must both pass** after every task. - **Public-repo hygiene:** English only, no machine-specific paths, no company or client names, in files and commit subjects alike. @@ -2012,6 +2012,7 @@ One line each, dated. Rulings taken during execution join the list. - 2026-09-08 — A resolution record whose source is `asked` or `inferred`, passed in the review dispatch prompt, stands where no declaration covers the path; the run grades framework rules by it and still reports `trigger-framework-declared` in `## Project`. - 2026-09-08 — The frameworkless signature stays `member: handle(System.TriggerOperation` as the spec prescribes; the document carries a Gotcha that the entry point is written verbatim, rather than the pattern widening to `member: TriggerOperation`. - 2026-09-08 — Task 1's prose under `.disjoint-contexts` no longer reads "the critical case", which the Task 1 reviewer flagged as a grade restated outside a tag; it reads "the case the group default grades". The spec's Framework-independent rules carry the original phrase and are reported, not edited. +- 2026-09-09 — Illustrative Apex fragments under prose headings (the frameworkless recursion-guard field, the three test-isolation snippets) stay fragments; the copy-pasteable constraint binds the blocks a file heading names. Task 3's reviewer flagged the fragments as plan-mandated; the Global Constraint now says so. ## Review rounds From 6f802b63d396ef48371061a86bd180512447bfed Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 13:49:22 +0200 Subject: [PATCH 029/120] feat(salesforce-standards): add the base-class trigger framework document --- .../reference/framework-base-class.md | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-base-class.md diff --git a/plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-base-class.md b/plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-base-class.md new file mode 100644 index 0000000..5d88fd2 --- /dev/null +++ b/plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-base-class.md @@ -0,0 +1,190 @@ +# Base-class trigger framework + +Framework id `base-class`. One approach, two products: the original +`kevinohara80/sfdc-trigger-framework` and its maintained fork +`dschach/salesforce-trigger-framework`. Both are a `virtual` class +`TriggerHandler` copied into the org or installed as a package; a +handler extends it and overrides the context methods it needs. Every +rule here is cited +`(standard: salesforce-triggers, rule: trigger-base-class-)`. + +## Dispatch + +### `OrderTrigger.trigger` + +```apex +trigger OrderTrigger on Order (before insert, before update, after update) { + new OrderTriggerHandler().run(); +} +``` + +### `OrderTriggerHandler.cls` + +Casts the context once in the constructor, then routes each context to +the domain and service layers of `salesforce-apex`'s +`reference/order-layers.md`; no SOQL, no DML, no business logic of its +own. `with sharing`, the default for the handler layer. + +```apex +public with sharing class OrderTriggerHandler extends TriggerHandler { + + private List newOrders; + private Map oldMap; + + public OrderTriggerHandler() { + this.newOrders = (List) Trigger.new; + this.oldMap = (Map) Trigger.oldMap; + } + + public override void beforeInsert() { + OrderDomain.applyDefaults(newOrders); + } + + public override void beforeUpdate() { + OrderDomain.validateStatusTransitions(newOrders, oldMap); + } + + public override void afterUpdate() { + List activated = OrderDomain.filterNewlyActivated(newOrders, oldMap); + if (!activated.isEmpty()) { + OrderService.activateFulfillment(activated); + } + } +} +``` + +### Signature + +``` +Signature: + header: extends TriggerHandler +Framework types: TriggerHandler +``` + +`TriggerHandler` is the framework's own type: read to recognise the +framework, graded by no rule here, and exempt from +`trigger-context-below-handler`. + +## Context access + +The base class reads `Trigger.isExecuting` and the context flags in +`run()` to pick the context method — `Trigger.operationType` in the +fork, `Trigger.isBefore`, `Trigger.isInsert` and their siblings in the +original — which is why it is a framework type. The handler reads `Trigger.new` and `Trigger.oldMap` once, in its +constructor, into typed fields; the context methods and everything +below them read nothing from `Trigger.*`. + +## Bypass and recursion + +Common to both products, all static on `TriggerHandler` unless noted: + +| Call | Effect | +|---|---| +| `TriggerHandler.bypass('OrderTriggerHandler')` | skips that handler for the rest of the transaction | +| `TriggerHandler.clearBypass('OrderTriggerHandler')` | restores it | +| `TriggerHandler.isBypassed('OrderTriggerHandler')` | asks | +| `TriggerHandler.clearAllBypasses()` | clears every bypass | +| `this.setMaxLoopCount(n)`, `this.clearMaxLoopCount()` | instance methods: cap how many times this handler's `run()` executes in one transaction | + +The fork adds `bypass(Type)` and `bypass(List)` overloads, +`bypassAll()` with `clearGlobalBypass()`, `setBypass(name, Boolean)`, +`bypassList()` and `getLoopCount(name)`, and its `setMaxLoopCount` +returns the handler so calls chain. The fork's README documents +`TriggerHandler.setGlobalBypass()`, which its source does not define — +the method is `bypassAll()`. + +**What happens when the loop count is exceeded** is where the two +products part, and the difference sits in one private method of +`TriggerHandler.cls`: + +| Product | Method | Behaviour | +|---|---|---| +| original | `addToLoopCount()` | throws `TriggerHandlerException('Maximum loop count of N reached in ')`, and the DML fails | +| fork | `incrementCheckLoopCount()` | returns without running the handler; the `throw` sits commented out inside that method, and the `System.debug` line fires only with `showDebug` on — by default nothing is logged | + +So `setMaxLoopCount(1)` is a recursion guard under the original and +silences automation under the fork. The discriminator is read from +`TriggerHandler.cls` when this document loads, and the variant goes on +the resolution record's first line. + +Where that file is unreadable — a managed package — a namespace or the +package version may still tell the two apart. Where nothing does, an +authoring session states both semantics and says the installed package +decides which holds; a review run grades every rule below except the +one keyed to error semantics, `trigger-base-class-loop-count`, and +names in the Summary that the discriminator was unreadable. Nobody +guesses between silencing and throwing. + +## Test isolation + +`run()` requires trigger context — the base class checks +`Trigger.isExecuting`, and the handler's constructor casts +`Trigger.new` — so the handler cannot be called without DML. Test the +layers below directly (`OrderDomain`, `OrderService`), and give the +handler one DML-backed test per context that asserts the outcome. + +To disable the handler in a test of other code that inserts orders: + +```apex +Account acc = TestDataFactory.createAccounts(1)[0]; +TriggerHandler.bypass('OrderTriggerHandler'); +try { + TestDataFactory.createOrders(200, acc.Id); +} finally { + TriggerHandler.clearBypass('OrderTriggerHandler'); +} +``` + +The factory inserts the records itself, as `salesforce-apex-testing` +requires, and its `createOrders` takes the parent account's id; the +bypass wraps the call that fires the trigger. + +## Fingerprint + +`extends TriggerHandler` in a class header together with +`new X().run()` in a trigger body, an optional namespace prefix before +`TriggerHandler`. The discriminator: `incrementCheckLoopCount` in +`TriggerHandler.cls` means the fork, which silences; +`addToLoopCount` means the original, which throws. + +## Rules + +**Dispatching class.** The handler is named `TriggerHandler`. +Pre-existing non-conforming names stay; the convention binds new code. + +(id: `trigger-base-class-dispatching-class`; severity: minor; source: +this standard) + +**A bypass is restored.** In non-test code every `bypass(...)` is +paired with the matching `clearBypass(...)` in a `finally` block. A +bypass left set silences the object's automation for every caller +later in the same transaction. + +(id: `trigger-base-class-bypass-restored`; severity: important; source: +this standard) + +**The loop count is not the recursion logic.** Under the fork, +`setMaxLoopCount` drops the handler's work without a signal, so +re-entry is controlled by state — a static set of processed ids — and +any `setMaxLoopCount` call carries a comment naming the silencing. +Under the original, `setMaxLoopCount` is a legitimate guard, and a +count above 1 is justified in a comment. This rule is keyed to the +error semantics above and goes ungraded where the discriminator is +unreadable. + +(id: `trigger-base-class-loop-count`; severity: important; source: this +standard) + +## Gotchas + +- The handler name a bypass string must match is derived from the + class: the fork reads `toString()` (`getHandlerName()`) unless the + constructor passed `super('OrderTriggerHandler')`. Prefer the fork's + `bypass(OrderTriggerHandler.class)` overload where it exists — a Type + cannot be misspelt. +- The context methods are `protected virtual` in the base class; + overriding them `public override` widens access and compiles under + both products, and is the form the fork's README shows. +- A handler constructed outside a trigger has null `Trigger.new`; the + casts above are safe only because `run()` refuses to execute outside + trigger context. From 0225d663925cc06df577f1d75b0d1729fefdd50f Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 13:54:18 +0200 Subject: [PATCH 030/120] feat(salesforce-standards): add the metadata-driven trigger framework document --- .../reference/framework-metadata-driven.md | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-metadata-driven.md diff --git a/plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-metadata-driven.md b/plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-metadata-driven.md new file mode 100644 index 0000000..74d5478 --- /dev/null +++ b/plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-metadata-driven.md @@ -0,0 +1,209 @@ +# Metadata-driven trigger framework + +Framework id `metadata-driven`. The reference implementation is +`mitchspano/apex-trigger-actions-framework`. One trigger per object +calls `new MetadataTriggerHandler().run()`, and Custom Metadata records +name the classes to run and their order — one +`sObject_Trigger_Setting__mdt` record per object, one +`Trigger_Action__mdt` record per action and context. An action is any +class implementing one of the `TriggerAction.*` interfaces; changing +the order is a metadata deployment, not a code one. Every rule here is +cited +`(standard: salesforce-triggers, rule: trigger-metadata-driven-)`. + +## Dispatch + +### `OrderTrigger.trigger` + +```apex +trigger OrderTrigger on Order (before insert, before update, after update) { + new MetadataTriggerHandler().run(); +} +``` + +### `OrderTriggerHandler.cls` + +One class carrying the object's three actions, each an interface +method the framework calls with `List` arguments. It routes to +the domain and service layers of `salesforce-apex`'s +`reference/order-layers.md`; no SOQL, no DML, no business logic of its +own. `with sharing`, the default for the handler layer. + +```apex +public with sharing class OrderTriggerHandler implements TriggerAction.BeforeInsert, TriggerAction.BeforeUpdate, TriggerAction.AfterUpdate { + + public void beforeInsert(List triggerNew) { + OrderDomain.applyDefaults((List) triggerNew); + } + + public void beforeUpdate(List triggerNew, List triggerOld) { + Map oldMap = new Map((List) triggerOld); + OrderDomain.validateStatusTransitions((List) triggerNew, oldMap); + } + + public void afterUpdate(List triggerNew, List triggerOld) { + Map oldMap = new Map((List) triggerOld); + List activated = OrderDomain.filterNewlyActivated((List) triggerNew, oldMap); + if (!activated.isEmpty()) { + OrderService.activateFulfillment(activated); + } + } +} +``` + +### The metadata + +Nothing in code names the class; the records do. One +`sObject_Trigger_Setting__mdt` record and three `Trigger_Action__mdt` +records deploy with the code: + +| Record | Type | Key fields | +|---|---|---| +| `Order` | `sObject_Trigger_Setting__mdt` | `Object_API_Name__c = Order` | +| `Order_Before_Insert` | `Trigger_Action__mdt` | `Apex_Class_Name__c = OrderTriggerHandler`, `Before_Insert__c = Order`, `Order__c = 1` | +| `Order_Before_Update` | `Trigger_Action__mdt` | `Apex_Class_Name__c = OrderTriggerHandler`, `Before_Update__c = Order`, `Order__c = 1` | +| `Order_After_Update` | `Trigger_Action__mdt` | `Apex_Class_Name__c = OrderTriggerHandler`, `After_Update__c = Order`, `Order__c = 1` | + +Each context field (`Before_Insert__c`, `Before_Update__c`, +`After_Insert__c`, `After_Update__c`, `Before_Delete__c`, +`After_Delete__c`, `After_Undelete__c`) is a lookup to the object's +setting record; a record enables exactly the contexts it fills. + +### Signature + +``` +Signature: + header: TriggerAction. +Framework types: TriggerBase, MetadataTriggerHandler, TriggerAction, TriggerActionFlow, FinalizerHandler +``` + +The pattern is `TriggerAction.` rather than `implements TriggerAction.` +on purpose: a class declared `implements Queueable, +TriggerAction.BeforeInsert` names the framework interface second, and a +header test is a substring match. Any header naming a `TriggerAction.*` +interface, wherever it sits in the `implements` list, selects the +class. Two framework classes match it themselves — +`MetadataTriggerHandler` and `TriggerActionFlow`, each implementing all +seven context interfaces — and both sit under `Framework types:`, which +removes them from the handler set; `TriggerActionFlowChangeEvent +extends TriggerActionFlow` is then a closure candidate whose parent is +not in the set, and stays out too. The framework types appear in the +repository only where the framework arrives as source; listing them +costs nothing where it does not. + +## Context access + +The framework reads `Trigger.*` in `TriggerBase.run()` and hands each +action `triggerNew` and `triggerOld` as `List`. An action +reads nothing from `Trigger.*`; it casts the lists and, where it needs +the old records by id, builds `new Map((List) +triggerOld)` — the framework passes no map. + +## Bypass and recursion + +| Call | Effect | +|---|---| +| `TriggerBase.bypass('Order')`, `clearBypass`, `isBypassed` | every action on the object; `Schema.SObjectType` overloads exist | +| `TriggerBase.clearAllBypasses()` | clears every object bypass | +| `MetadataTriggerHandler.bypass('OrderTriggerHandler')`, `clearBypass`, `isBypassed` | one action, by class name or `System.Type` | +| `MetadataTriggerHandler.clearAllBypasses()` | clears every action bypass | +| `Bypass_Execution__c` on either record type | disables the object or the action without a deployment of code | +| `Bypass_Permission__c`, `Required_Permission__c` | a custom permission whose holders skip the action, or without which it does not run | + +**Recursion.** No loop count exists here. The framework counts how +often each record id has been seen in the update contexts — +`TriggerBase.idToNumberOfTimesSeenBeforeUpdate` and +`TriggerBase.idToNumberOfTimesSeenAfterUpdate`, both +`Map` — and leaves the decision to the action: one that +must act once checks that the count for the record is 1. What happens +when a limit is exceeded: nothing is exceeded. An action ignoring the +maps re-enters until a governor limit throws `System.LimitException` +and the transaction fails. + +**A stale record fails the object.** `MetadataTriggerHandler` +instantiates each action with `Type.forName(Apex_Class_Name__c)`; a +name resolving to no class, or to a class not implementing the +context's interface, throws `MetadataTriggerHandlerException` — and +every DML on that object fails until the record is fixed. + +## Test isolation + +An action is a plain class: instantiate it and call the interface +method with records built in memory, no DML and no metadata read: + +```apex +List orders = new List{ new Order(Status = null) }; +new OrderTriggerHandler().beforeInsert(orders); +Assert.areEqual('Draft', orders[0].Status); +``` + +To disable the handler in a test of other code that inserts orders, +call `MetadataTriggerHandler.bypass('OrderTriggerHandler')` before the +DML and `clearBypass` in a `finally` block; `TriggerBase.bypass('Order')` +silences every action on the object the same way. + +## Fingerprint + +`new MetadataTriggerHandler().run()` in a trigger body together with +`Trigger_Action__mdt` and `sObject_Trigger_Setting__mdt` records in the +repository — an optional `ns.` before the class and `ns__` before the +metadata API names. The row keys on records and the trigger body, never +on the framework's classes: the records sit in the repository whether +the framework arrives as an unlocked package or as source, while the +classes appear only in the second case. + +## Inner classes + +`Type.forName('Outer.Inner')` resolves, so an inner class can be +registered as an action. The signature grammar selects top-level types +only; an inner-class action is therefore not graded by the rules below, +and this document says so rather than leaving it to a reader. The +convention here is a top-level class per action or per object. + +## Rules + +**Dispatching class.** A class carrying an object's actions is named +`TriggerHandler`; where actions split by feature, each is +`Action`. Pre-existing non-conforming names stay; the +convention binds new code. + +(id: `trigger-metadata-driven-dispatching-class`; severity: minor; +source: this standard) + +**Every action is registered, and every record resolves.** Each +`Trigger_Action__mdt` record's `Apex_Class_Name__c` names a class in +the repository implementing the interface of every context the record +enables, and every class implementing one of the seven context +interfaces — `BeforeInsert` through `AfterUndelete` — has a record. A +stale name fails every DML on the object at runtime; an unregistered +action never runs. A `TriggerAction.DmlFinalizer` implementer is +selected as a handler by the signature but registered through a +different record type, so this rule leaves it alone. + +(id: `trigger-metadata-driven-action-registered`; severity: important; +source: this standard) + +**A bypass is restored.** In non-test code every `TriggerBase.bypass` +or `MetadataTriggerHandler.bypass` is paired with the matching +`clearBypass` in a `finally` block. A bypass left set silences +automation for every caller later in the same transaction. + +(id: `trigger-metadata-driven-bypass-restored`; severity: important; +source: this standard) + +**Order is explicit.** Every `Trigger_Action__mdt` record carries an +`Order__c` value, unique among the records enabling the same object and +context. Two actions on one order leave their sequence to the query. + +(id: `trigger-metadata-driven-order-explicit`; severity: minor; source: +this standard) + +## Gotchas + +- `triggerOld` is a `List`, not a map; build the map in the + action. +- The Custom Metadata type and its records deploy together with the + framework; a record naming a class that is not yet deployed fails the + object's DML from the first save. +- Flow actions (`Flow_Name__c`) run through the same records and are + outside this document. From 7924876edf10f170e4287b220c01447d0f39b7e2 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 13:58:09 +0200 Subject: [PATCH 031/120] feat(salesforce-standards): add the framework comparison for step (c) --- .../reference/choosing-a-framework.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 plugins/salesforce-standards/skills/salesforce-triggers/reference/choosing-a-framework.md diff --git a/plugins/salesforce-standards/skills/salesforce-triggers/reference/choosing-a-framework.md b/plugins/salesforce-standards/skills/salesforce-triggers/reference/choosing-a-framework.md new file mode 100644 index 0000000..958b65a --- /dev/null +++ b/plugins/salesforce-standards/skills/salesforce-triggers/reference/choosing-a-framework.md @@ -0,0 +1,57 @@ +# Choosing a trigger framework + +Read at step (c) of the resolution protocol, when neither a declaration +nor a fingerprint settled the framework. Present the case for and +against each shipped approach; the developer chooses; the session +writes the declaration — a `trigger-framework:` line in the root +`CLAUDE.md` or the trigger directory's — so the question is not asked +again. The developer's existing code decides more than any argument +here: a project already extending a base class has chosen. + +## Base-class (`base-class`) + +**For.** One virtual class copied into the org, no metadata to +maintain, and a handler shape every Salesforce developer recognises. +Bypass and loop-count APIs come with it. Two products share the shape, +so hiring and code review carry over. + +**Against.** Execution order within a context is the order of calls in +the handler, so reordering is a code deployment. The handler cannot be +called without DML, so handler tests are integration tests. The two +products differ in what an exceeded loop count does — one throws, one +silences — and the difference is a private method in a class the +project copied and may have edited. + +## Metadata-driven (`metadata-driven`) + +**For.** Order and bypasses live in Custom Metadata, so an admin can +reorder or disable an action without a deployment of code. An action +is a plain class implementing an interface, callable in a test without +DML. Permission-based bypasses come with it. + +**Against.** A stale metadata record fails every DML on its object. +The dispatch is invisible in code — a reader must open the records to +learn what runs — and a trigger that runs nothing looks identical to +one that runs ten actions. The framework is a dependency the project +installs and upgrades. + +## Frameworkless (`frameworkless`) + +**For.** Nothing to install or upgrade; one static entry point per +object; the handler is callable in a test without DML. The whole +dispatch fits in twenty lines a newcomer reads in one sitting. + +**Against.** Bypass and recursion control are the project's to write, +and each project writes them differently. Nothing recognises the shape +from outside, so the framework must be declared to be resolved. + +## Something else + +A recognition label — fflib, TDTM, a dispatcher — or a framework this +plugin has never seen: the framework is sound and only the document is +missing. Offer to write a project document describing the project's own +code, answering the four questions the `salesforce-triggers` skill +asks, and declare it with a `doc-path:` locator. Never compare it to +the three above — this plugin cannot write that comparison, and a +document written from a pattern found on the internet describes +somebody else's framework. From 288af424b4a90d4761dd74d9eed72a8ef67e25b1 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 14:05:54 +0200 Subject: [PATCH 032/120] refactor(salesforce-standards): move trigger content out of salesforce-apex --- .../skills/salesforce-apex-testing/SKILL.md | 7 +- .../reference/test-patterns.cls | 2 +- .../skills/salesforce-apex/SKILL.md | 27 ++++--- .../reference/bulkification.md | 2 +- .../{trigger-handler.md => order-layers.md} | 72 ++++--------------- 5 files changed, 33 insertions(+), 77 deletions(-) rename plugins/salesforce-standards/skills/salesforce-apex/reference/{trigger-handler.md => order-layers.md} (65%) diff --git a/plugins/salesforce-standards/skills/salesforce-apex-testing/SKILL.md b/plugins/salesforce-standards/skills/salesforce-apex-testing/SKILL.md index a959bd4..65bcf1a 100644 --- a/plugins/salesforce-standards/skills/salesforce-apex-testing/SKILL.md +++ b/plugins/salesforce-standards/skills/salesforce-apex-testing/SKILL.md @@ -9,9 +9,10 @@ Standards for Apex test classes: structure, test data, assertions, mocking, and coverage. Cite rules in review findings as `(standard: salesforce-apex-testing, rule: )`; each rule names its source — the Apex Developer Guide, or `this standard` (a recorded house -decision). Layering (handler/domain/service/selector) is -`salesforce-apex`'s concern, not restated here — a test exercises those -layers, it does not redefine them. LWC jest tests are out of scope — +decision). Layering (domain/service/selector) is `salesforce-apex`'s +concern and the trigger handler `salesforce-triggers`'s, neither +restated here — a test exercises those layers, it does not redefine +them. LWC jest tests are out of scope — see `salesforce-lwc`. ## Test-class structure diff --git a/plugins/salesforce-standards/skills/salesforce-apex-testing/reference/test-patterns.cls b/plugins/salesforce-standards/skills/salesforce-apex-testing/reference/test-patterns.cls index 4994963..fe949b9 100644 --- a/plugins/salesforce-standards/skills/salesforce-apex-testing/reference/test-patterns.cls +++ b/plugins/salesforce-standards/skills/salesforce-apex-testing/reference/test-patterns.cls @@ -1,7 +1,7 @@ /** * Worked example of the patterns in SKILL.md, testing an illustrative * `OrderService` / `OrderDomain` pair (the same Order fulfillment scenario - * as salesforce-apex's reference/trigger-handler.md): activating an Order + * as salesforce-apex's reference/order-layers.md): activating an Order * inserts one fulfillment Task per Order, and an Order can only move to * "Activated" from "Draft" — moving to "Activated" from any other status * (e.g. "Cancelled") throws. diff --git a/plugins/salesforce-standards/skills/salesforce-apex/SKILL.md b/plugins/salesforce-standards/skills/salesforce-apex/SKILL.md index 54783c7..2f30373 100644 --- a/plugins/salesforce-standards/skills/salesforce-apex/SKILL.md +++ b/plugins/salesforce-standards/skills/salesforce-apex/SKILL.md @@ -1,12 +1,14 @@ --- name: salesforce-apex -description: Use when writing or reviewing Apex backend code — naming, lightweight layering (one trigger handler per object, service, selector, domain), bulkification, governor limits, sharing keywords, and error handling. Apex unit tests belong to salesforce-apex-testing; access-model design to salesforce-security-model. +description: Use when writing or reviewing Apex backend code — naming, lightweight layering (service, selector, domain), bulkification, governor limits, sharing keywords, and error handling. Apex triggers and their handlers belong to salesforce-triggers; Apex unit tests to salesforce-apex-testing; access-model design to salesforce-security-model. --- # Salesforce Apex -Standards for Apex classes and triggers: naming, layering, bulk safety, -governor limits, sharing, and error handling. Cite rules in review +Standards for Apex classes: naming, layering, bulk safety, governor +limits, sharing, and error handling. Triggers and their handlers — one +trigger per object, the trigger body, the project's trigger framework — +belong to `salesforce-triggers`. Cite rules in review findings as `(standard: salesforce-apex, rule: )`; each rule names its source — the Apex Developer Guide, Salesforce Well-Architected, a PMD Apex rule name, or `this standard` (a recorded house decision). @@ -25,15 +27,14 @@ mechanics. | Method | camelCase, verb-first | `activateFulfillment` | | Variable / parameter | camelCase | `activatedOrders` | | Constant (`static final`) | `UPPER_SNAKE_CASE` | `MAX_BATCH_SIZE` | -| Trigger handler | `TriggerHandler` | `AccountTriggerHandler` | | Selector | `Selector`, methods `select*By*` | `AccountSelector.selectByIds(Set)` | | Custom exception | `Exception` | `OrderProcessingException` | (id: `apex-naming`; severity: minor; source: Apex Developer Guide; PMD `ClassNamingConventions`, `MethodNamingConventions`, `FieldNamingConventions`, `FormalParameterNamingConventions`, -`LocalVariableNamingConventions`; the handler/selector/exception -patterns are this standard) +`LocalVariableNamingConventions`; the selector/exception patterns are +this standard) ## Lightweight layers @@ -42,22 +43,20 @@ a naming-and-structure convention, not a library. | Layer | Owns | Never does | |---|---|---| -| Trigger | One line: hands the trigger context to its handler | Any conditional or business logic | -| Handler (`TriggerHandler`) | Routes each trigger event (before insert, after update, …) to domain and service calls | SOQL, DML, business logic itself | +| Trigger and handler | Dispatch: hand the trigger context to the layers below — the shape, one trigger per object and the project's framework are `salesforce-triggers`'s | SOQL, DML, business logic in the handler; the trigger body is `trigger-body-delegates`'s to grade | | Domain (`Domain`, optional) | Per-record rules on the in-memory set: validation, defaulting, state-transition checks | SOQL, DML, callouts | | Service (`Service`) | Orchestration: the business transaction, DML, callouts, chaining async work | Ad-hoc SOQL (asks the selector) | | Selector (`Selector`) | ALL SOQL for its object — query methods only | DML, business logic | -- **One handler per trigger; one trigger per object.** The trigger body - is a single delegating call — no `if`/`for`/field logic in the - `.trigger` file. - The Domain layer holds an object's own business rules (e.g. "an Order can only move from Draft to Activated, never backward"), kept independent of Handler/Service so those rules stay testable without full orchestration. -- A complete worked example (trigger + handler + domain + selector + - service for one object) is in - [reference/trigger-handler.md](reference/trigger-handler.md). +- The four framework-independent units for one object — domain, + selector, service, exception — are in + [reference/order-layers.md](reference/order-layers.md); the trigger + and handler that call them are in each `salesforce-triggers` + framework document. (id: `apex-layering`; severity: important; source: Salesforce Well-Architected; Apex Enterprise Patterns / fflib cited as diff --git a/plugins/salesforce-standards/skills/salesforce-apex/reference/bulkification.md b/plugins/salesforce-standards/skills/salesforce-apex/reference/bulkification.md index 8d6d9e8..b156677 100644 --- a/plugins/salesforce-standards/skills/salesforce-apex/reference/bulkification.md +++ b/plugins/salesforce-standards/skills/salesforce-apex/reference/bulkification.md @@ -2,7 +2,7 @@ One pair per rule in SKILL.md's "Bulkification" and "Governor limits" sections. All examples use the `Order` / `Task` objects from -[trigger-handler.md](trigger-handler.md). +[order-layers.md](order-layers.md). ## apex-bulkification: SOQL in a loop → collection-driven query diff --git a/plugins/salesforce-standards/skills/salesforce-apex/reference/trigger-handler.md b/plugins/salesforce-standards/skills/salesforce-apex/reference/order-layers.md similarity index 65% rename from plugins/salesforce-standards/skills/salesforce-apex/reference/trigger-handler.md rename to plugins/salesforce-standards/skills/salesforce-apex/reference/order-layers.md index a0bacb9..fe3557d 100644 --- a/plugins/salesforce-standards/skills/salesforce-apex/reference/trigger-handler.md +++ b/plugins/salesforce-standards/skills/salesforce-apex/reference/order-layers.md @@ -1,65 +1,21 @@ -# Trigger + handler + domain + selector + service — worked example +# Domain + selector + service — worked example -One object (standard `Order`), six compilation units, one fenced block +One object (standard `Order`), four compilation units, one fenced block each — each block is valid, copy-pasteable Apex for the file named -above it. In a real org each block is its own `.trigger`/`.cls` file -(a `.cls` file holds exactly one top-level type). +above it. In a real org each block is its own `.cls` file (a `.cls` +file holds exactly one top-level type). The trigger and the handler +that call these units depend on the project's trigger framework and +live in the `salesforce-triggers` skill's framework documents; each of +them calls the same methods below, so the three examples are one +scenario. Scenario: when an Order's `Status` field moves to "Activated", create -one fulfillment `Task` per order. The example demonstrates: one -delegating trigger, a handler that routes but never queries or writes, -a domain class that owns the record-level status-transition rule, a -selector that owns all SOQL for `Order`, and a service that owns the -DML transaction — all bulk-safe, all with an explicit sharing -declaration, errors routed through a custom exception and a logger -abstraction rather than swallowed or left as bare `System.debug` calls. - -## `OrderTrigger.trigger` - -A single delegating call per trigger context — no conditional or field -logic in the trigger body itself. - -```apex -trigger OrderTrigger on Order (before insert, before update, after update) { - if (Trigger.isBefore) { - if (Trigger.isInsert) { - OrderTriggerHandler.handleBeforeInsert(Trigger.new); - } - if (Trigger.isUpdate) { - OrderTriggerHandler.handleBeforeUpdate(Trigger.new, Trigger.oldMap); - } - } - if (Trigger.isAfter && Trigger.isUpdate) { - OrderTriggerHandler.handleAfterUpdate(Trigger.new, Trigger.oldMap); - } -} -``` - -## `OrderTriggerHandler.cls` - -Routes trigger events to Domain and Service; no SOQL, no DML, no -business logic of its own. `with sharing` — the default for the -handler layer. - -```apex -public with sharing class OrderTriggerHandler { - - public static void handleBeforeInsert(List newOrders) { - OrderDomain.applyDefaults(newOrders); - } - - public static void handleBeforeUpdate(List newOrders, Map oldMap) { - OrderDomain.validateStatusTransitions(newOrders, oldMap); - } - - public static void handleAfterUpdate(List newOrders, Map oldMap) { - List activated = OrderDomain.filterNewlyActivated(newOrders, oldMap); - if (!activated.isEmpty()) { - OrderService.activateFulfillment(activated); - } - } -} -``` +one fulfillment `Task` per order. The example demonstrates: a domain +class that owns the record-level status-transition rule, a selector +that owns all SOQL for `Order`, and a service that owns the DML +transaction — all bulk-safe, all with an explicit sharing declaration, +errors routed through a custom exception and a logger abstraction +rather than swallowed or left as bare `System.debug` calls. ## `OrderDomain.cls` From 7ce3b072ddfb5113ffb636548fc9465b9ed0c68f Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 14:16:31 +0200 Subject: [PATCH 033/120] feat(salesforce-standards): resolve the trigger framework before grading Apex --- .../commands/salesforce-review.md | 26 ++++++++++++------- .../skills/salesforce-code-review/SKILL.md | 26 +++++++++++++++++++ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/plugins/salesforce-standards/commands/salesforce-review.md b/plugins/salesforce-standards/commands/salesforce-review.md index c1ed265..57e35d2 100644 --- a/plugins/salesforce-standards/commands/salesforce-review.md +++ b/plugins/salesforce-standards/commands/salesforce-review.md @@ -9,7 +9,15 @@ review must never block this session. 1. Resolve the scope: `$ARGUMENTS` when given (named files); otherwise the current diff — staged plus unstaged changes, or, on a clean tree, the diff of the current branch against its base. -2. Pre-dispatch first-create check, gated on the contract probe: +2. Resolve the trigger framework when the scope holds any Apex file: + load `salesforce-triggers` and run its protocol, steps (0) through + (c) — this is the one review surface where step (c) may ask the + developer. Write the resolution record, one per resolved framework + with the paths it covers, and carry it in the dispatch prompt beside + the directory-mode decision; the agent verifies it against the + declarations on disk. Where step (c) resolved the framework, offer + to write the declaration before dispatching. +3. Pre-dispatch first-create check, gated on the contract probe: check `/.claude/rules/working-process/review-reports.md`, then `$HOME/.claude/rules/working-process/review-reports.md` — first hit wins (paths owned by the review-reports contract; @@ -20,19 +28,19 @@ review must never block this session. explicit project instruction declaring the mode (signal list owned by the process-artifacts rule) — ask the developer now: ignored or tracked mode. -3. Dispatch the `salesforce-code-reviewer` agent in the BACKGROUND +4. Dispatch the `salesforce-code-reviewer` agent in the BACKGROUND with the resolved scope. Salesforce files only, per the skill's run scope; the agent notes out-of-domain files in the report Summary as out of scope and writes the one report itself (`mode: agent` under the installed contract). The dispatch prompt - carries the scope, the prior report's runid for a rerun - (`rerun-of`), and the directory-mode decision from step 2 — never - report-shaping instructions of its own: aggregation and counting - policy, layout, and severity policy belong to the contract and - the reviewer's skill, and a whole-project scope is no exception. -4. Tell the developer: the review is running in the background; the + carries the scope, the prior report's runid for a rerun (`rerun-of`), + and the resolution record from step 2, and the directory-mode decision from step 3 + — never report-shaping instructions of its own: aggregation and + counting policy, layout, and severity policy belong to the contract + and the reviewer's skill, and a whole-project scope is no exception. +5. Tell the developer: the review is running in the background; the summary arrives as a task notification, not inline; progress via `/tasks`; the report will land under `docs/code-review/`. -5. When the run's notification arrives, relay its reply to the +6. When the run's notification arrives, relay its reply to the developer: report path, findings by severity, and the candidate-gap offers verbatim. diff --git a/plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md b/plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md index 8d410b5..cff6210 100644 --- a/plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md +++ b/plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md @@ -53,6 +53,32 @@ provision. salesforce-lwc, salesforce-flow, salesforce-data-model, salesforce-security-model, salesforce-aura, salesforce-visualforce as the content demands. + Any Apex file in the run — a `.trigger` or a `.cls` alike — also + loads `salesforce-triggers` and runs its resolution protocol before + grading: steps (0) through (b), then step (d) for each resolved + framework, loading that framework's document by path or name and + selecting its handlers from the grading universe by the document's + signature. Resolution runs per file, so a monorepo running two + frameworks loads two documents and grades every class under the + framework resolved for its own path. Step (c) is never taken here — + a background agent asks nothing — and neither is the offer to write + a declaration. A resolution record carried in the dispatch prompt is + verified against the declarations on disk; where they disagree, the + run reports the mismatch and grades by the declaration, the files + being the authority. A record whose source is `asked` or `inferred` + stands where no declaration covers the path — step (c) already ran + in the session that dispatched — and `trigger-framework-declared` + still lands in `## Project`. When resolution fails, grade the + framework-independent rules, skip framework rules rather than guess + them, note `trigger framework: unresolved — framework-specific rules + not graded` among the Summary's out-of-scope notes, and report + `trigger-framework-declared` (nothing declares the path) or + `trigger-framework-declared.ambiguous` (two the protocol cannot rank) + in `## Project`. Where resolution succeeds and the document supplies + no signature, the same note names `trigger-context-below-handler` + beside the framework rules. The skill's failure table names the + other Summary notes: unreadable `extends` parents in aggregate, the + two `vendor-paths:` notes, an unreadable discriminator. 3. Grade every finding by its rule tag — the tag is binding; no general intuition overrules it: - the finding matches a listed sub-rule → that sub-rule's severity; From f0d590cdac1267d37a9fb3bb45c798870239c4c1 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 14:18:27 +0200 Subject: [PATCH 034/120] docs: normalize whitespace in the trigger-frameworks plan's phrase checks --- ...026-09-08-salesforce-trigger-frameworks.md | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md index 504cc72..cc04f3d 100644 --- a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md +++ b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md @@ -1631,7 +1631,7 @@ grep -c 'salesforce-triggers' "$s" grep -c '^[0-9]\. ' "$s" grep -c 'salesforce-triggers' "$c" grep -c '^[0-9]\. ' "$c" -grep -c 'decision from step 2' "$c" +tr -s '[:space:]' ' ' < "$c" | grep -o 'decision from step 2' | wc -l grep -c 'step 5' plugins/salesforce-standards/agents/salesforce-code-reviewer.md ``` @@ -1700,7 +1700,24 @@ Find the line beginning `2. Pre-dispatch first-create check` and insert before i to write the declaration before dispatching. ``` -Then renumber: the former step 2 becomes `3.`, 3 becomes `4.`, 4 becomes `5.`, 5 becomes `6.`; and in the new step 4 (the dispatch step) change `the directory-mode decision from step 2` to `the resolution record from step 2, and the directory-mode decision from step 3` — the step enumerates what the prompt carries, and the record is now one of those things. +Then renumber: the former step 2 becomes `3.`, 3 becomes `4.`, 4 becomes `5.`, 5 becomes `6.`; and in the new step 4 (the dispatch step) rewrite the enumeration of what the prompt carries so it names the record — the step lists those things, and the record is now one of them. The three lines + +``` + carries the scope, the prior report's runid for a rerun + (`rerun-of`), and the directory-mode decision from step 2 — never + report-shaping instructions of its own: aggregation and counting +``` + +become + +``` + carries the scope, the prior report's runid for a rerun + (`rerun-of`), the resolution record from step 2, and the + directory-mode decision from step 3 — never report-shaping + instructions of its own: aggregation and counting +``` + +The wrap moves because the sentence grew; prose wraps at about 72 characters, and reflowing a paragraph whose text changed is not a deviation. - [ ] **Step 4: Measure after** @@ -1711,13 +1728,13 @@ grep -c 'salesforce-triggers' "$s" grep -c '^[0-9]\. ' "$s" grep -c 'salesforce-triggers' "$c" grep -c '^[0-9]\. ' "$c" -grep -c 'decision from step 2' "$c" -grep -c 'record from step 2, and the directory-mode decision from step 3' "$c" +tr -s '[:space:]' ' ' < "$c" | grep -o 'decision from step 2' | wc -l +tr -s '[:space:]' ' ' < "$c" | grep -o 'the resolution record from step 2, and the directory-mode decision from step 3' | wc -l grep -c 'step 5' plugins/salesforce-standards/agents/salesforce-code-reviewer.md tr -s '[:space:]' ' ' < "$s" | grep -o 'trigger framework: unresolved' | wc -l ``` -Expected: `1`, `6`, `1`, `6`, `0`, `1`, `1`, `1`. The skill's numbered-step count is an invariant at `6` — the addition sits inside step 2 — and the agent card's `step 5` is an invariant at `1`, which is the reason for that placement. +Expected: `1`, `6`, `1`, `6`, `0`, `1`, `1`, `1`. The two phrase checks normalize whitespace, because both phrases wrap: a single-line `grep` over wrapped prose returns `0` where the phrase is plainly present, and reflowing the shipped text to satisfy such a check is the tail wagging the dog. The skill's numbered-step count is an invariant at `6` — the addition sits inside step 2 — and the agent card's `step 5` is an invariant at `1`, which is the reason for that placement. - [ ] **Step 5: Validate and commit** From 795d0803456c61e6a1cc391c92e7846d85e9e593 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 14:20:05 +0200 Subject: [PATCH 035/120] fix(salesforce-standards): wrap the review command's dispatch enumeration --- .../salesforce-standards/commands/salesforce-review.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/salesforce-standards/commands/salesforce-review.md b/plugins/salesforce-standards/commands/salesforce-review.md index 57e35d2..fc42263 100644 --- a/plugins/salesforce-standards/commands/salesforce-review.md +++ b/plugins/salesforce-standards/commands/salesforce-review.md @@ -33,10 +33,11 @@ review must never block this session. run scope; the agent notes out-of-domain files in the report Summary as out of scope and writes the one report itself (`mode: agent` under the installed contract). The dispatch prompt - carries the scope, the prior report's runid for a rerun (`rerun-of`), - and the resolution record from step 2, and the directory-mode decision from step 3 - — never report-shaping instructions of its own: aggregation and - counting policy, layout, and severity policy belong to the contract + carries the scope, the prior report's runid for a rerun + (`rerun-of`), the resolution record from step 2, and the + directory-mode decision from step 3 — never report-shaping + instructions of its own: aggregation and counting + policy, layout, and severity policy belong to the contract and the reviewer's skill, and a whole-project scope is no exception. 5. Tell the developer: the review is running in the background; the summary arrives as a task notification, not inline; progress via From ff038c9b7767a5cc5eb851fb90f73c2b3e0442ae Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 14:42:59 +0200 Subject: [PATCH 036/120] feat(salesforce-standards): route triggers and document the declaration keys in the toolchain rule --- .../rules/salesforce-toolchain.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugins/salesforce-standards/rules/salesforce-toolchain.md b/plugins/salesforce-standards/rules/salesforce-toolchain.md index 043e8cb..8ee0d18 100644 --- a/plugins/salesforce-standards/rules/salesforce-toolchain.md +++ b/plugins/salesforce-standards/rules/salesforce-toolchain.md @@ -37,9 +37,21 @@ This project follows the salesforce-standards toolchain: When the salesforce-standards plugin's skills are available, load the matching one before working: `salesforce-apex` (backend code), +`salesforce-triggers` (Apex triggers and their handlers), `salesforce-apex-testing` (Apex tests), `salesforce-lwc` (Lightning Web Components), `salesforce-flow` (Flows and retired automation), `salesforce-data-model` (objects, fields, declarative data config), `salesforce-security-model` (access design), `salesforce-aura` / `salesforce-visualforce` (legacy UI maintenance). When they are not available, the toolchain facts above still bind. + +The project's trigger framework and its vendor code are declared in +the project, never in this rule. A line carrying the `trigger-framework:` +key sits in the body of the root `CLAUDE.md`, of a trigger directory's +`CLAUDE.md`, or of a project rule outside this payload — one framework +per path. A line carrying the `vendor-paths:` key sits in a root home +alone — the root `CLAUDE.md` files or a project rule — as directory +prefixes; a copy in a subdirectory is not read. When the +`salesforce-triggers` skill is available it reads both keys and says how +they rank; when it is not, the lines still record the choice for the +reader. From c37861ed965f38b38724222d2f8bfc5d6744b90a Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 14:46:59 +0200 Subject: [PATCH 037/120] docs(salesforce-standards): list the salesforce-triggers skill on the identity surfaces --- .claude-plugin/marketplace.json | 2 +- README.md | 2 +- .../.claude-plugin/plugin.json | 2 +- plugins/salesforce-standards/README.md | 15 ++++++++++++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7579d56..00e943e 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -17,7 +17,7 @@ { "name": "salesforce-standards", "source": "./plugins/salesforce-standards", - "description": "Salesforce coding standards for the sf CLI toolchain: area skills for Apex, LWC, Flow, data and security model plus maintenance-first legacy UI, a code-review stack, a plan-review checklist, and a salesforce-toolchain rule" + "description": "Salesforce coding standards for the sf CLI toolchain: area skills for Apex, Apex triggers, LWC, Flow, data and security model plus maintenance-first legacy UI, a code-review stack, a plan-review checklist, and a salesforce-toolchain rule" }, { "name": "project-memory", diff --git a/README.md b/README.md index bedc117..db29988 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Marketplace of [Claude Code](https://code.claude.com) plugins by Missing Bits. |--------|------|-------------| | `working-process` | [README](plugins/working-process/README.md) | Spec-driven working process: grilling-session, architect-session, system-designer-session, process-status and sync-rules skills, architect and plan-adversary verdict agents, two verdict-free consultation agents, propagation-auditor and integrity-auditor audit agents, distributed process rules | | `python-standards` | [README](plugins/python-standards/README.md) | Python coding standards for uv + ruff + pytest + pyright: area skills, code-review stack, plan-review checklist, distributed toolchain rule | -| `salesforce-standards` | [README](plugins/salesforce-standards/README.md) | Salesforce coding standards for the sf CLI toolchain: area skills (Apex, LWC, Flow, data/security model, legacy UI), code-review stack, plan-review checklist, distributed toolchain rule | +| `salesforce-standards` | [README](plugins/salesforce-standards/README.md) | Salesforce coding standards for the sf CLI toolchain: area skills (Apex, Apex triggers, LWC, Flow, data/security model, legacy UI), code-review stack, plan-review checklist, distributed toolchain rule | | `project-memory` | [README](plugins/project-memory/README.md) | In-repo project memory: committed Team memory (`docs/memory/`) and per-user Private memory (`.claude/memory/`), with `memory-review-session`, `migrate-memory`, and `redirect-memory` skills, distributed as a Rules payload | ## License diff --git a/plugins/salesforce-standards/.claude-plugin/plugin.json b/plugins/salesforce-standards/.claude-plugin/plugin.json index c7b394b..e67bbcc 100644 --- a/plugins/salesforce-standards/.claude-plugin/plugin.json +++ b/plugins/salesforce-standards/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "salesforce-standards", - "description": "Salesforce coding standards for the sf CLI + Code Analyzer + Prettier-Apex + sfdx-lwc-jest toolchain: eight area skills (Apex, Apex testing, LWC, Flow, data model, security model, maintenance-first Aura and Visualforce), a code-review stack (salesforce-code-review skill, salesforce-code-reviewer agent, /salesforce-review command) writing review reports to docs/code-review/, a salesforce-plan-review checklist for plan reviews, and a salesforce-toolchain rule shipped as a Rules payload", + "description": "Salesforce coding standards for the sf CLI + Code Analyzer + Prettier-Apex + sfdx-lwc-jest toolchain: nine area skills (Apex, Apex triggers, Apex testing, LWC, Flow, data model, security model, maintenance-first Aura and Visualforce), a code-review stack (salesforce-code-review skill, salesforce-code-reviewer agent, /salesforce-review command) writing review reports to docs/code-review/, a salesforce-plan-review checklist for plan reviews, and a salesforce-toolchain rule shipped as a Rules payload", "version": "0.3.1", "author": { "name": "Missing Bits (Jacek Nakonieczny)" }, "license": "MIT", diff --git a/plugins/salesforce-standards/README.md b/plugins/salesforce-standards/README.md index f0a8a33..497248e 100644 --- a/plugins/salesforce-standards/README.md +++ b/plugins/salesforce-standards/README.md @@ -12,7 +12,8 @@ author's preferences on contested points. | Skill | Scope | |---|---| -| `salesforce-apex` | naming, lightweight layers (trigger handler, service, selector, domain), bulkification, governor limits, sharing keywords, error handling | +| `salesforce-apex` | naming, lightweight layers (service, selector, domain), bulkification, governor limits, sharing keywords, error handling | +| `salesforce-triggers` | one trigger per object, the trigger body, resolving the project's trigger framework — base-class, metadata-driven, frameworkless, or one the project documents itself — and loading its guidance | | `salesforce-apex-testing` | test-class structure, TestDataFactory, assertions, mocking, no SeeAllData, coverage as a floor | | `salesforce-lwc` | component structure, JS conventions, wire vs imperative Apex, labels, events and LMS, styling, jest tests, static-resource placement stance | | `salesforce-flow` | naming, plan-before-build, bulk-safe patterns, fault paths, run context, one record-triggered flow per object and trigger moment, retired-automation stance | @@ -26,6 +27,18 @@ Each skill ships `reference/` examples alongside its SKILL.md. standards; new UI is built in LWC unless the platform forces the legacy technology. +## Declaring the trigger framework + +A project declares which trigger framework governs its triggers with a +`trigger-framework:` line in the body of its root `CLAUDE.md` or of the +trigger directory's, and lists code it did not write with a +`vendor-paths:` line at the root — directory prefixes, never globs. The +`salesforce-triggers` skill reads both, ranks nested declarations, +infers from code when nothing declares, and asks only from an +interactive session. A framework this plugin does not ship is declared +with a `doc-path:` or `skill:` locator pointing at the project's own +document. + ## Review stack - **`salesforce-code-review` skill** — audits Salesforce code and From 2070b14bd0b76186ee0e016d7c977d8b4f970381 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 15:03:25 +0200 Subject: [PATCH 038/120] fix(salesforce-standards): name the document rather than reuse the unresolved note --- plugins/salesforce-standards/.claude-plugin/plugin.json | 2 +- plugins/salesforce-standards/README.md | 7 ++++--- .../skills/salesforce-code-review/SKILL.md | 7 +++++-- .../salesforce-triggers/reference/framework-base-class.md | 7 ++++--- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/plugins/salesforce-standards/.claude-plugin/plugin.json b/plugins/salesforce-standards/.claude-plugin/plugin.json index e67bbcc..35b3224 100644 --- a/plugins/salesforce-standards/.claude-plugin/plugin.json +++ b/plugins/salesforce-standards/.claude-plugin/plugin.json @@ -4,5 +4,5 @@ "version": "0.3.1", "author": { "name": "Missing Bits (Jacek Nakonieczny)" }, "license": "MIT", - "keywords": ["salesforce", "standards", "apex", "lwc", "flow", "aura", "visualforce", "sf-cli", "review"] + "keywords": ["salesforce", "standards", "apex", "triggers", "lwc", "flow", "aura", "visualforce", "sf-cli", "review"] } diff --git a/plugins/salesforce-standards/README.md b/plugins/salesforce-standards/README.md index 497248e..4ab2e7b 100644 --- a/plugins/salesforce-standards/README.md +++ b/plugins/salesforce-standards/README.md @@ -30,9 +30,10 @@ legacy technology. ## Declaring the trigger framework A project declares which trigger framework governs its triggers with a -`trigger-framework:` line in the body of its root `CLAUDE.md` or of the -trigger directory's, and lists code it did not write with a -`vendor-paths:` line at the root — directory prefixes, never globs. The +`trigger-framework:` line in the body of a `CLAUDE.md` — the root one, +the trigger directory's, or `.claude/CLAUDE.md` — or of a project rule, +and lists code it did not write with a `vendor-paths:` line in a root +home — directory prefixes, never globs. The `salesforce-triggers` skill reads both, ranks nested declarations, infers from code when nothing declares, and asks only from an interactive session. A framework this plugin does not ship is declared diff --git a/plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md b/plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md index cff6210..0d9f83a 100644 --- a/plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md +++ b/plugins/salesforce-standards/skills/salesforce-code-review/SKILL.md @@ -75,8 +75,11 @@ provision. `trigger-framework-declared` (nothing declares the path) or `trigger-framework-declared.ambiguous` (two the protocol cannot rank) in `## Project`. Where resolution succeeds and the document supplies - no signature, the same note names `trigger-context-below-handler` - beside the framework rules. The skill's failure table names the + no signature, a separate note names the resolved document and says + it supplied none, so the framework rules and + `trigger-context-below-handler` go ungraded while the rest is + graded — never the unresolved note above, which would report a + framework that did resolve. The skill's failure table names the other Summary notes: unreadable `extends` parents in aggregate, the two `vendor-paths:` notes, an unreadable discriminator. 3. Grade every finding by its rule tag — the tag is binding; no general diff --git a/plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-base-class.md b/plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-base-class.md index 5d88fd2..163a173 100644 --- a/plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-base-class.md +++ b/plugins/salesforce-standards/skills/salesforce-triggers/reference/framework-base-class.md @@ -70,9 +70,10 @@ framework, graded by no rule here, and exempt from The base class reads `Trigger.isExecuting` and the context flags in `run()` to pick the context method — `Trigger.operationType` in the fork, `Trigger.isBefore`, `Trigger.isInsert` and their siblings in the -original — which is why it is a framework type. The handler reads `Trigger.new` and `Trigger.oldMap` once, in its -constructor, into typed fields; the context methods and everything -below them read nothing from `Trigger.*`. +original — which is why it is a framework type. The handler reads +`Trigger.new` and `Trigger.oldMap` once, in its constructor, into typed +fields; the context methods and everything below them read nothing from +`Trigger.*`. ## Bypass and recursion From ba95d0ab0ec64e9bcf872fccc17f0cc96f93027a Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 15:04:14 +0200 Subject: [PATCH 039/120] docs: back-port the final-review fixes into the trigger-frameworks plan --- ...026-09-08-salesforce-trigger-frameworks.md | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md index cc04f3d..282d0f2 100644 --- a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md +++ b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md @@ -908,9 +908,10 @@ framework, graded by no rule here, and exempt from The base class reads `Trigger.isExecuting` and the context flags in `run()` to pick the context method — `Trigger.operationType` in the fork, `Trigger.isBefore`, `Trigger.isInsert` and their siblings in the -original — which is why it is a framework type. The handler reads `Trigger.new` and `Trigger.oldMap` once, in its -constructor, into typed fields; the context methods and everything -below them read nothing from `Trigger.*`. +original — which is why it is a framework type. The handler reads +`Trigger.new` and `Trigger.oldMap` once, in its constructor, into typed +fields; the context methods and everything below them read nothing from +`Trigger.*`. ## Bypass and recursion @@ -1679,8 +1680,11 @@ Replace with: `trigger-framework-declared` (nothing declares the path) or `trigger-framework-declared.ambiguous` (two the protocol cannot rank) in `## Project`. Where resolution succeeds and the document supplies - no signature, the same note names `trigger-context-below-handler` - beside the framework rules. The skill's failure table names the + no signature, a separate note names the resolved document and says + it supplied none, so the framework rules and + `trigger-context-below-handler` go ungraded while the rest is + graded — never the unresolved note above, which would report a + framework that did resolve. The skill's failure table names the other Summary notes: unreadable `extends` parents in aggregate, the two `vendor-paths:` notes, an unreadable discriminator. ``` @@ -1878,9 +1882,10 @@ Then insert a section before `## Review stack`: ## Declaring the trigger framework A project declares which trigger framework governs its triggers with a -`trigger-framework:` line in the body of its root `CLAUDE.md` or of the -trigger directory's, and lists code it did not write with a -`vendor-paths:` line at the root — directory prefixes, never globs. The +`trigger-framework:` line in the body of a `CLAUDE.md` — the root one, +the trigger directory's, or `.claude/CLAUDE.md` — or of a project rule, +and lists code it did not write with a `vendor-paths:` line in a root +home — directory prefixes, never globs. The `salesforce-triggers` skill reads both, ranks nested declarations, infers from code when nothing declares, and asks only from an interactive session. A framework this plugin does not ship is declared @@ -1888,6 +1893,8 @@ with a `doc-path:` or `skill:` locator pointing at the project's own document. ``` +Then add a keyword for the new skill: find `"apex", "lwc"` in the same file and replace it with `"apex", "triggers", "lwc"`. + - [ ] **Step 4: Edit the catalog and the repo README** `.claude-plugin/marketplace.json` — find `area skills for Apex, LWC, Flow, data and security model plus maintenance-first legacy UI` and replace with `area skills for Apex, Apex triggers, LWC, Flow, data and security model plus maintenance-first legacy UI`. @@ -2033,6 +2040,17 @@ One line each, dated. Rulings taken during execution join the list. ## Review rounds +### 2026-09-09 — final whole-branch code review, fable 5.1 (implementation range 18a87c4..c37861e) + +The plan's own text carried four of the findings, so each fix landed in the shipped file and was back-ported into the block above that prescribes it. + +- fixed 2026-09-09 — [Important] Task 8's paragraph told a run to reuse the `trigger framework: unresolved` Summary note where the framework did resolve and only the signature was missing, so the report would contradict itself and the hub's own failure row; license: that failure row, which names the document instead; the paragraph now writes a separate note naming the resolved document and saying it supplied no signature, and says outright that the unresolved note is never the one +- fixed 2026-09-09 — [Minor] `framework-base-class.md:73` ran 113 characters against the file's wrap, a defect this loop's round-three ledger already owed a back-port; license: the plan's own wrap constraint; the Context-access paragraph is rewrapped in Task 4's block and in the shipped file +- fixed 2026-09-09 — [Minor] Task 10's README section narrowed the declaration homes to two where the toolchain rule and the hub allow `.claude/CLAUDE.md` and a project rule; license: the hub's homes paragraph; the section now names the wider set and puts `vendor-paths:` in a root home +- fixed 2026-09-09 — [Minor] The plugin manifest's keyword list gained no term for the new skill; license: the marketplace-sync rule's canonical-description duty, which the keywords serve; `triggers` joins the list in Task 10's block and in the manifest +- declined 2026-09-09 — [Minor] Step (b) fingerprints the grading universe, which holds Apex files, while the metadata-driven row keys on `Trigger_Action__mdt` and `sObject_Trigger_Setting__mdt` records — `.md-meta.xml` files the universe does not contain, so a literal reader never matches that half of the pattern; ruling: 2026-09-09; the defect is the spec's (its Steps table and Fingerprints section say the same), the plan implements the spec faithfully, and rewriting the universe here would be design work this branch has no license for — it goes to the spec's ledger as a note with the other five +- signal 2026-09-09 — the reviewer judged the branch ready to merge after the one Important, triaged all five deferred items and the parked one as shippable, and named the release-notes item the plan already carries. No further round earns its cost: the fix wave is four prescribed replacements, each checked against its cited line, and one scoped re-review closes it + ### 2026-09-08 — plan-adversary, fable 5.1, concerns (round 3, full-document) - fixed 2026-09-08 — [Important] Both new test-isolation blocks asserted with legacy `System.assertEquals`, which the sibling skill grades in new tests while the reference file the plan tells the author to imitate uses `Assert.areEqual`; license: `salesforce-apex-testing/SKILL.md:86-87` (`apex-test-assertions.legacy-assert`) and `reference/test-patterns.cls:50`; both lines now read `Assert.areEqual` From 634b8436d639d5b96d2a55f69dcdf877dc5e78ab Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 15:06:57 +0200 Subject: [PATCH 040/120] docs: record the toolchain-rule homes narrowing in the trigger-frameworks plan --- docs/plans/2026-09-08-salesforce-trigger-frameworks.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md index 282d0f2..2f9c33e 100644 --- a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md +++ b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md @@ -2049,6 +2049,7 @@ The plan's own text carried four of the findings, so each fix landed in the ship - fixed 2026-09-09 — [Minor] Task 10's README section narrowed the declaration homes to two where the toolchain rule and the hub allow `.claude/CLAUDE.md` and a project rule; license: the hub's homes paragraph; the section now names the wider set and puts `vendor-paths:` in a root home - fixed 2026-09-09 — [Minor] The plugin manifest's keyword list gained no term for the new skill; license: the marketplace-sync rule's canonical-description duty, which the keywords serve; `triggers` joins the list in Task 10's block and in the manifest - declined 2026-09-09 — [Minor] Step (b) fingerprints the grading universe, which holds Apex files, while the metadata-driven row keys on `Trigger_Action__mdt` and `sObject_Trigger_Setting__mdt` records — `.md-meta.xml` files the universe does not contain, so a literal reader never matches that half of the pattern; ruling: 2026-09-09; the defect is the spec's (its Steps table and Fingerprints section say the same), the plan implements the spec faithfully, and rewriting the universe here would be design work this branch has no license for — it goes to the spec's ledger as a note with the other five +- open — [Minor] `rules/salesforce-toolchain.md` states the `trigger-framework:` homes as the root `CLAUDE.md`, a trigger directory's `CLAUDE.md` or a project rule, never `.claude/CLAUDE.md`, which the hub treats as an equal home — the same narrowing the README fix above removed, one file away. Found by the fix wave's scoped re-review as an out-of-scope observation, so it neither extended that loop nor entered it; it is the adjacent-cell class this repo has now measured four times. The fix is one clause in a distributed rule and belongs to the developer's call at the branch's close - signal 2026-09-09 — the reviewer judged the branch ready to merge after the one Important, triaged all five deferred items and the parked one as shippable, and named the release-notes item the plan already carries. No further round earns its cost: the fix wave is four prescribed replacements, each checked against its cited line, and one scoped re-review closes it ### 2026-09-08 — plan-adversary, fable 5.1, concerns (round 3, full-document) From fc0fa9e7d5ab5ad4d91927a470024f9d408785df Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 15:22:13 +0200 Subject: [PATCH 041/120] fix(salesforce-standards): name .claude/CLAUDE.md among the declaration homes --- .../salesforce-standards/rules/salesforce-toolchain.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/salesforce-standards/rules/salesforce-toolchain.md b/plugins/salesforce-standards/rules/salesforce-toolchain.md index 8ee0d18..8c01509 100644 --- a/plugins/salesforce-standards/rules/salesforce-toolchain.md +++ b/plugins/salesforce-standards/rules/salesforce-toolchain.md @@ -47,11 +47,11 @@ available, the toolchain facts above still bind. The project's trigger framework and its vendor code are declared in the project, never in this rule. A line carrying the `trigger-framework:` -key sits in the body of the root `CLAUDE.md`, of a trigger directory's -`CLAUDE.md`, or of a project rule outside this payload — one framework -per path. A line carrying the `vendor-paths:` key sits in a root home -alone — the root `CLAUDE.md` files or a project rule — as directory -prefixes; a copy in a subdirectory is not read. When the +key sits in the body of a `CLAUDE.md` — the root one, a trigger +directory's, or `.claude/CLAUDE.md` — or of a project rule outside this +payload, one framework per path. A line carrying the `vendor-paths:` key +sits in a root home alone — either root `CLAUDE.md` or a project rule — +as directory prefixes; a copy in a subdirectory is not read. When the `salesforce-triggers` skill is available it reads both keys and says how they rank; when it is not, the lines still record the choice for the reader. From a00d2edf2367b38e9a2755f3ba9763c0bc1fc9e0 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 15:22:31 +0200 Subject: [PATCH 042/120] docs: back-port the declaration-homes fix and the dogfood ruling into the plan --- ...026-09-08-salesforce-trigger-frameworks.md | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md index 2f9c33e..84c868b 100644 --- a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md +++ b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md @@ -25,7 +25,7 @@ base: develop - **Rule ids:** `trigger-` in the hub, `trigger-base-class-`, `trigger-metadata-driven-` and `trigger-frameworkless-` in the framework documents. Findings cite `(standard: salesforce-triggers, rule: )` for every one of them — a `reference/` document has no standard of its own. - **The declaration keys never appear at the start of a line outside a fenced code block in any shipped file.** A line starting `trigger-framework:` or `vendor-paths:` in a shipped rule or skill would be read as a declaration by the very protocol it documents. Inline mentions sit in backticks mid-sentence; examples sit in fenced blocks. Task 11 sweeps for it. - **Frontmatter safety:** no `description:` contains `: ` (colon-space); the two new descriptions are checked for it in Task 1 and Task 11. -- **No version bump.** `plugins/salesforce-standards/.claude-plugin/plugin.json` stays at `0.3.1`; the release PR mints the number, and this topic does not dogfood. +- **The version carries a dogfood prerelease.** At the branch's close the developer chose to dogfood this work, so `plugins/salesforce-standards/.claude-plugin/plugin.json` moves to `0.4.0-dev.trigger-frameworks` in a commit of its own (developer ruling, 2026-09-09). The number is the next unreleased minor rather than a suffix on `0.3.1`, because `master` carries `0.3.0`, `develop` already accumulated `0.3.1`, and the plugin cache holds a `0.3.1` directory — a prerelease of an already-present number sorts below it and delivers nothing. The discriminator is the branch short-name, this topic having no issue. Every task before the close leaves the version untouched, and the release PR strips the suffix while minting the final number. - **No eval files.** Neither `skill-creator` nor any agent writes `evals/trigger-evals.json` or any other eval file under the plugin — a standing decision of this repo. `skill-creator` may be used for `description:` tuning only. - **Apex examples are valid, copy-pasteable Apex** for the file named above them, in the style of the existing `reference/` files: four-space indent, one class per block, explicit sharing keyword. The constraint binds the blocks a `### \`\`` heading names; an illustrative fragment under a prose heading — a field, a test snippet — is not a file and need not compile alone, as the shipped `bulkification.md` before/after pairs already do not (developer ruling, 2026-09-09). No compiler runs here, so every block is written once into this plan and copied verbatim into the file — a fix to a shipped block is back-ported into this plan in the same wave. - **Every framework document calls the same four lower-layer methods** the renamed `order-layers.md` defines: `OrderDomain.applyDefaults(List)`, `OrderDomain.validateStatusTransitions(List, Map)`, `OrderDomain.filterNewlyActivated(List, Map)` returning `List`, and `OrderService.activateFulfillment(List)`. Nothing else is called from a handler. @@ -1797,11 +1797,11 @@ Then append after the paragraph's last line (`available, the toolchain facts abo ``` The project's trigger framework and its vendor code are declared in the project, never in this rule. A line carrying the `trigger-framework:` -key sits in the body of the root `CLAUDE.md`, of a trigger directory's -`CLAUDE.md`, or of a project rule outside this payload — one framework -per path. A line carrying the `vendor-paths:` key sits in a root home -alone — the root `CLAUDE.md` files or a project rule — as directory -prefixes; a copy in a subdirectory is not read. When the +key sits in the body of a `CLAUDE.md` — the root one, a trigger +directory's, or `.claude/CLAUDE.md` — or of a project rule outside this +payload, one framework per path. A line carrying the `vendor-paths:` key +sits in a root home alone — either root `CLAUDE.md` or a project rule — +as directory prefixes; a copy in a subdirectory is not read. When the `salesforce-triggers` skill is available it reads both keys and says how they rank; when it is not, the lines still record the choice for the reader. @@ -1815,9 +1815,10 @@ grep -c 'salesforce-triggers' "$r" grep -c '^trigger-framework:\|^vendor-paths:' "$r" grep -c '^---$' "$r" grep -c 'trigger-framework:' "$r" +grep -c '\.claude/CLAUDE\.md' "$r" ``` -Expected: `2`, `0`, `2`, `1`. The key appears once, mid-line. +Expected: `2`, `0`, `2`, `1`, `1`. The key appears once, mid-line, and the homes name `.claude/CLAUDE.md` — the hub treats it as an equal home, and the first draft of this paragraph omitted it. - [ ] **Step 4: Review the frontmatter by hand** @@ -1913,7 +1914,7 @@ grep -c '^trigger-framework:\|^vendor-paths:' plugins/salesforce-standards/READM grep '"version"' plugins/salesforce-standards/.claude-plugin/plugin.json ``` -Expected: `0`, `1`, `2`, `9`, `.claude-plugin/marketplace.json:1` and `README.md:1`, `0`, `"version": "0.3.1",`. The `salesforce-triggers` count is two: the table row and the new section each name it once. The declaration-key count at line start is the plugin-wide invariant at `0`; the version line is an invariant too. +Expected: `0`, `1`, `2`, `9`, `.claude-plugin/marketplace.json:1` and `README.md:1`, `0`, `"version": "0.3.1",` — this task leaves the version alone; the prerelease lands in its own commit at the branch's close. The `salesforce-triggers` count is two: the table row and the new section each name it once. The declaration-key count at line start is the plugin-wide invariant at `0`; the version line is an invariant too. - [ ] **Step 6: Validate and commit** @@ -2036,6 +2037,8 @@ One line each, dated. Rulings taken during execution join the list. - 2026-09-08 — A resolution record whose source is `asked` or `inferred`, passed in the review dispatch prompt, stands where no declaration covers the path; the run grades framework rules by it and still reports `trigger-framework-declared` in `## Project`. - 2026-09-08 — The frameworkless signature stays `member: handle(System.TriggerOperation` as the spec prescribes; the document carries a Gotcha that the entry point is written verbatim, rather than the pattern widening to `member: TriggerOperation`. - 2026-09-08 — Task 1's prose under `.disjoint-contexts` no longer reads "the critical case", which the Task 1 reviewer flagged as a grade restated outside a tag; it reads "the case the group default grades". The spec's Framework-independent rules carry the original phrase and are reported, not edited. +- 2026-09-09 — The toolchain rule names `.claude/CLAUDE.md` among the declaration homes before the merge, rather than leaving the narrowing as an `open` ledger line: the rule ships to projects, and the omission is the same one the README fix removed a file earlier. +- 2026-09-09 — The branch closes by a local `--no-ff` merge into `develop`, with no push and no pull request, and the plugin version takes a dogfood prerelease — `0.4.0-dev.trigger-frameworks`, the next unreleased minor, because a suffix on `0.3.1` would sort below a number the cache already holds. - 2026-09-09 — Illustrative Apex fragments under prose headings (the frameworkless recursion-guard field, the three test-isolation snippets) stay fragments; the copy-pasteable constraint binds the blocks a file heading names. Task 3's reviewer flagged the fragments as plan-mandated; the Global Constraint now says so. ## Review rounds @@ -2049,7 +2052,7 @@ The plan's own text carried four of the findings, so each fix landed in the ship - fixed 2026-09-09 — [Minor] Task 10's README section narrowed the declaration homes to two where the toolchain rule and the hub allow `.claude/CLAUDE.md` and a project rule; license: the hub's homes paragraph; the section now names the wider set and puts `vendor-paths:` in a root home - fixed 2026-09-09 — [Minor] The plugin manifest's keyword list gained no term for the new skill; license: the marketplace-sync rule's canonical-description duty, which the keywords serve; `triggers` joins the list in Task 10's block and in the manifest - declined 2026-09-09 — [Minor] Step (b) fingerprints the grading universe, which holds Apex files, while the metadata-driven row keys on `Trigger_Action__mdt` and `sObject_Trigger_Setting__mdt` records — `.md-meta.xml` files the universe does not contain, so a literal reader never matches that half of the pattern; ruling: 2026-09-09; the defect is the spec's (its Steps table and Fingerprints section say the same), the plan implements the spec faithfully, and rewriting the universe here would be design work this branch has no license for — it goes to the spec's ledger as a note with the other five -- open — [Minor] `rules/salesforce-toolchain.md` states the `trigger-framework:` homes as the root `CLAUDE.md`, a trigger directory's `CLAUDE.md` or a project rule, never `.claude/CLAUDE.md`, which the hub treats as an equal home — the same narrowing the README fix above removed, one file away. Found by the fix wave's scoped re-review as an out-of-scope observation, so it neither extended that loop nor entered it; it is the adjacent-cell class this repo has now measured four times. The fix is one clause in a distributed rule and belongs to the developer's call at the branch's close +- fixed 2026-09-09 — [Minor] `rules/salesforce-toolchain.md` stated the `trigger-framework:` homes as the root `CLAUDE.md`, a trigger directory's `CLAUDE.md` or a project rule, never `.claude/CLAUDE.md`, which the hub treats as an equal home — the same narrowing the README fix above removed, one file away. Found by the fix wave's scoped re-review as an out-of-scope observation, so it neither extended that loop nor entered it; it is the adjacent-cell class this repo has now measured four times; ruling: 2026-09-09 — fixed before the merge because the rule ships to projects; the paragraph now names the wider set in the rule and in Task 9's block, and Task 9's checks gained the anchor - signal 2026-09-09 — the reviewer judged the branch ready to merge after the one Important, triaged all five deferred items and the parked one as shippable, and named the release-notes item the plan already carries. No further round earns its cost: the fix wave is four prescribed replacements, each checked against its cited line, and one scoped re-review closes it ### 2026-09-08 — plan-adversary, fable 5.1, concerns (round 3, full-document) From a8a1e9211fbec43e62ca23120a6ecc3e264d9718 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Wed, 9 Sep 2026 15:22:42 +0200 Subject: [PATCH 043/120] chore(salesforce-standards): mint the trigger-frameworks dogfood prerelease --- plugins/salesforce-standards/.claude-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/salesforce-standards/.claude-plugin/plugin.json b/plugins/salesforce-standards/.claude-plugin/plugin.json index 35b3224..5c6741c 100644 --- a/plugins/salesforce-standards/.claude-plugin/plugin.json +++ b/plugins/salesforce-standards/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "salesforce-standards", "description": "Salesforce coding standards for the sf CLI + Code Analyzer + Prettier-Apex + sfdx-lwc-jest toolchain: nine area skills (Apex, Apex triggers, Apex testing, LWC, Flow, data model, security model, maintenance-first Aura and Visualforce), a code-review stack (salesforce-code-review skill, salesforce-code-reviewer agent, /salesforce-review command) writing review reports to docs/code-review/, a salesforce-plan-review checklist for plan reviews, and a salesforce-toolchain rule shipped as a Rules payload", - "version": "0.3.1", + "version": "0.4.0-dev.trigger-frameworks", "author": { "name": "Missing Bits (Jacek Nakonieczny)" }, "license": "MIT", "keywords": ["salesforce", "standards", "apex", "triggers", "lwc", "flow", "aura", "visualforce", "sf-cli", "review"] From c5b5679389a6814f70b8aeeed1fe8e3be6a42082 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 20:37:48 +0200 Subject: [PATCH 044/120] docs: spec errata wave four, grilled and architect round one --- docs/domain/glossary.md | 16 +- ...2026-09-11-review-loop-errata-wave-four.md | 262 ++++++++++++++++++ 2 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 docs/specs/2026-09-11-review-loop-errata-wave-four.md diff --git a/docs/domain/glossary.md b/docs/domain/glossary.md index 4d0e62f..c3896b8 100644 --- a/docs/domain/glossary.md +++ b/docs/domain/glossary.md @@ -339,7 +339,10 @@ scope. Those dispatch-time fields are immutable — they say what one reviewer was given and returned — while a later event about that round appends after the heading's closing parenthesis, never inside it. The loop's derived state — round count, the all-Minor signal, the -diff-scoped chain — is read by folding these. +diff-scoped chain — is read by folding these. The latest round heading +is the one carrying the highest ordinal, wherever it sits in the +section: the rules prescribe no order for the blocks, and live ledgers +run both ways. _Avoid_: round title, round record (for the heading alone) **Disposition line**: @@ -371,6 +374,17 @@ how or how well. Distinct from the diff-scoped chain itself, the round-one-plus-reviewed-waves structure the LGTM certifies. _Avoid_: chain accepted, chain closed, unclosed chain +**Confirming round**: +The full-document round a plan's loop owes before it may close, whenever +the latest round heading is diff-scoped — whatever path would otherwise +end the rounds. It runs as the same loop's last round under its standing +consent, counts against the round cap like any other, and its verdict is +the one stamped; on a plan the resolution annotation and the +adjudication wait for it. A spec owes none: its diff-scoped chain is +discharged at the consumption gate, where a full-document round is one +arm of the pair offer. +_Avoid_: final round, closing round, full read (as the name) + **Document branch**: The local branch a review loop's per-round commits live on, named `.docs` — git refuses a ref nested under an existing branch, so a diff --git a/docs/specs/2026-09-11-review-loop-errata-wave-four.md b/docs/specs/2026-09-11-review-loop-errata-wave-four.md new file mode 100644 index 0000000..887cf04 --- /dev/null +++ b/docs/specs/2026-09-11-review-loop-errata-wave-four.md @@ -0,0 +1,262 @@ +--- +ticket: none +date: 2026-09-11 +status: draft +grilled: 2026-09-11 +architect: blocking +revises: [./2026-08-17-autonomous-review-loop-design.md, ./2026-09-07-diff-scoped-chain-debt-design.md, ./2026-08-27-audit-agents-design.md] +branch: feature/process-wave-four +base: develop +--- + +# Review loop — errata wave four + +## Problem + +Two outside cycles ran working-process 0.13.0 through 0.16.0 on real +client work and groomed their findings against each release. Seven +items survived 0.16.0. Three of them were decisions the developer had +not yet taken; four were sentences nobody had written; grilling found +two more decisions the rules had left implicit. None needs a +design of its own, and each is cheap to remove — which is what makes +this a wave of errata rather than a fifth follow-up spec. + +The findings live in this repo's Private memory as lifted copies +(`idea-working-process-second-cycle`, `idea-working-process-sweep-verifier`), +each pointing at its authoritative source. This document records the +decisions and the changes; it does not repeat the evidence. + +## Decisions taken 2026-09-11 + +Five questions — three the shipped rules left open and two the +grilling found implicit in them. Each was put to the developer with its +options and cost; the answers below are rulings and carry that clause +where a ledger later cites them. + +1. **A plan's loop closes only on a full-document round.** Today the + confirming round fires on exactly one path — a diff-scoped `LGTM`. + Two measured loops closed by other paths — a resolution annotation + after a diff-scoped `concerns` — and owed no confirming round by the + letter, yet in both the confirming round ran on the reviewer's + recommendation and found the cycle's one Important. The rule now + keys on the latest round heading rather than on the path: whatever + ends a plan's rounds, one full-document round follows unless the + latest verdict round already was full-document. Alternatives + declined: keying on the reviewer's stop signal (a semantic read of + free text that no lint can check, on a line every round writes) and + widening only the annotation path (leaves the cap and all-Minor + escalations uncovered). `ruling: 2026-09-11`. +2. **A round heading's sha, if one is ever written, stands beside the + `integrity:` hash and never in its place.** Per-round commits are + opt-in, so a sha exists only on that path; the `integrity:` hash is + computed from the body and must work on a document nobody has + committed, which is the default path. Something present only + sometimes cannot replace something that must be present always. One + sentence records it so a later session does not "tidy" the hash + away. `ruling: 2026-09-11`. +3. **`process-status` reports the plan co-firing in one line derived + from its mapping, without judging it.** The rule already calls the + Unresolved verdict / Chain debt duplicate on a plan deliberate + (`spec-plan-lifecycle.md`, the Unresolved verdict owner leg). The + report adds one line where both classes hit the same plan — one debt + seen from two sides, both settled through the confirming round, see + that owner leg — derived from two hits sharing a file, in the shape + the skill already uses when Misplaced stamp silences a neighbour. It does not say "deliberate": that is process + knowledge, and the skill's contract is to carry none. Alternative + declined: silence, which leaves the explanation where the confused + reader never looks. `ruling: 2026-09-11`. + +4. **"Latest round heading" means the highest ordinal, wherever it + sits.** Grilling found the phrase undefined in both rules that use + it while W1 makes it the key: the lifecycle rule prescribes the + heading's shape and nothing about block order, and live ledgers run + both ways — ten chronological, four newest-first, and one session + on 2026-09-08 writing a spec one way and its plan the other. The + ordinal is on every heading, including those predating the `scope` + token, so it decides; block order stays unprescribed, since + reordering closed documents is body editing and a state-aware lint + can flag order later. Alternatives declined: mandating newest-first + (leaves ten documents non-compliant) and reading file position + (leaves four). `ruling: 2026-09-11`. + +5. **On a plan, the resolution annotation and the adjudication wait + for the full-document round; the round has no decline path.** + Grilling asked what happens when the developer wants to close a plan + whose latest heading is diff-scoped. The annotation is written only + once the latest heading is full-document; until then the plan stays + `adversary: concerns` unannotated, which the Unresolved verdict + command matches, so the owed round is visible and the confirming + round is the same loop's last round under the same standing consent. + A developer who wants no further round leaves the plan open, and + that is the honest state. Alternatives declined: a decline recorded + with `, debt discharged ` on a `concerns` heading, which uses + the Chain debt token outside the term's definition and would reopen + a five-day-old concept; and treating the confirming round as a new + loop after the annotation, which strips it of standing consent and + leaves the plan clean for the Unfinished-work list between the two. + The cost is stated: plans get no decline path where specs have three + discharge paths. A later erratum may add one, on a measured plan + whose confirming round earned nothing. `ruling: 2026-09-11`. + +A sixth question from the chain-debt design — whether a future +undischarged instance gets the same retroactive decline as the live +one — turned out to be answered by the text that shipped: +`spec-plan-lifecycle.md` already derives the decline on an +`implemented` document from the Chain debt owner leg. No change. + +## Scope + +Seven changes in `plugins/working-process/`, and three in +`docs/domain/glossary.md` — two applied at grilling (the Round heading +sentence, the Confirming round entry) and one owed (W5). + +1. **W1 — plan close needs a full-document round** (decision 1). + `workflow.md`, *What a diff-scoped LGTM certifies*: the plan + paragraph generalizes from "never terminates on a diff-scoped LGTM" + to "closes only when the latest verdict round was full-document", + and the recovery sentence reads "latest round heading is diff-scoped, + whatever its verdict". `spec-plan-lifecycle.md`, Unfinished-work + list, Unresolved verdict owner leg: the plan exception widens the + same way — the confirming round closes the verdict and the annotation + may not, on any plan whose latest heading is diff-scoped. The same + rule's *The disposition ledger* section gains one sentence defining + the latest round heading by ordinal (decision 4); the glossary's + Round heading entry already carries it, applied at grilling. The + rule's resolution-annotation bullet — "Concerns resolved without a + fresh review round keep the verdict and gain a resolution date" — + gains the plan clause of decision 5: on a plan the annotation and + the adjudication are written only once the latest round heading is + full-document. The glossary gains **Confirming round**, minted at + grilling: both rules use the phrase throughout for the object W1 keys + on, and no entry defined it. +2. **W2 — what the cap does not promise.** `workflow.md`, Terminators, + Round cap bullet: one sentence saying a run of `blocking` verdicts is + unbounded by the cap, because the cap counts autonomous rounds and + `blocking` suspends autonomy — each continuation is the developer's + own decision, so consenting to three rounds consents to three + autonomous ones. +3. **W3 — sha beside hash** (decision 2). `spec-plan-lifecycle.md`, + the per-round-commits paragraph: one sentence. +4. **W4 — co-firing line** (decision 3). `process-status/SKILL.md`, + Step 4, after the Misplaced stamp paragraph. +5. **W5 — glossary wrap seam.** `docs/domain/glossary.md`, Chain debt: + the clause "recorded decline — of the gate's pair offer, or, where + …, of the question …" loses its comma pile; the term's meaning does + not move. +6. **W6 — topic-branch naming.** `ticket-frontmatter.md` gains a short + *Branch naming* section: `feature/-`, + `feature/` where the ticket is `none`, auto-generated + worktree names renamed to it before the first commit, and the + `branch:` field recording the result. The rule's sourcing paragraph + already assumes this shape; the section makes the assumption a + convention. The repo-level half landed in this repo's `CLAUDE.md` + on 2026-07-22; this is the cross-project half. +7. **W7 — the propagation duties as an author-facing rule.** A new + rule file, `rules/propagation-duties.md`, scoped by `paths:` to + `docs/specs/**`, `docs/plans/**` and `docs/domain/**` — the first + two as the process-artifacts rule scopes them, the third because + duty 1 fires when a glossary `_Avoid_` ban is minted, and a rule + silent at the glossary would miss the one edit that duty names. It + costs nothing outside authoring. It keys the eight duties by + the edit an author just made — changed an interface, prescribed a + block, added a field or label, asserted a count, used a name the + source does not define, reported another document's state, wrote a + verification command, copied a reviewer's citation — and names the + enumeration each demands, before the next dispatch pays for an + audit. The agent card stays the gate's definition and keeps the + measurements; the rule is the author's checklist and cites the card + rather than restating its evidence. `README.md` moves from "five rule + files" to six and names the new one. + +## What this wave does not do + +- **No decline path for a plan's confirming round** (decision 5). A + plan whose latest heading is diff-scoped stays unannotated and + visible to the Unresolved verdict command until the round runs; the + developer's only lever is to order the round or leave the plan open. + An annotation written before the round would be a rule violation, + and a violated plan is invisible to the list the way any violation + is — the recovery clause in `workflow.md`, re-offer at the document's + next touch, is what catches it. A state-aware lint reading headings by + ordinal is where a mechanical check belongs. +- **The Chain debt term keeps its meaning.** It names what a diff-scoped + `LGTM` leaves. W1 gives a plan a broader obligation, owned by the + workflow rule, and does not fold it into the term. +- **The sync-rules install is the developer's.** Adding a rule file + changes the payload's ruleset hash; the project-level install under + `.claude/rules/working-process/` is Ignored mode and updates by + running `sync-rules`, which is not a commit on this branch. +- **Nothing here reaches `superpowers:writing-plans`.** The mutation + proof, the spec-coverage duty, the seam checklist, the authoring bans + and the platform-API class stay in the valuable package; each needs + a design decision this wave does not take. + +## Verification + +Each change is checked by grep against the shipped file, whitespace +normalized so a wrapped phrase still matches; a check states both its +before and after value. + +- W1: `tr -s '[:space:]' ' ' < plugins/working-process/rules/workflow.md | grep -o 'never terminates on a diff-scoped LGTM' | wc -l` — 1 before, 0 after; `grep -o 'latest verdict round was full-document' | wc -l` over the same stream — 0 before, 1 after. `grep -c 'whose latest round heading is a$' plugins/working-process/rules/spec-plan-lifecycle.md` — the owner leg's line — 1 before, 0 after. Over the whitespace-normalized lifecycle rule: `grep -o 'highest ordinal' | wc -l` — 0 before, 1 after (decision 4); `grep -o 'wait for the confirming round' | wc -l` — 0 before, 1 after (decision 5). `grep -c '^\*\*Confirming round\*\*:' docs/domain/glossary.md` and `grep -c 'highest ordinal' docs/domain/glossary.md` — both 1 before and after: applied at grilling, declared invariants. +- W2: `tr -s '[:space:]' ' ' < plugins/working-process/rules/workflow.md | grep -o 'unbounded by the cap' | wc -l` — 0 before, 1 after. +- W3: `tr -s '[:space:]' ' ' < plugins/working-process/rules/spec-plan-lifecycle.md | grep -o 'never stands in for the' | wc -l` — 0 before, 1 after. +- W4: `grep -c 'Unresolved verdict and Chain debt' plugins/working-process/skills/process-status/SKILL.md` — 0 before, 1 after. +- W5: `grep -c 'decline — of the gate' docs/domain/glossary.md` — 1 before, 0 after. +- W6: `grep -c '^## Branch naming' plugins/working-process/rules/ticket-frontmatter.md` — 0 before, 1 after. +- W7: `ls plugins/working-process/rules/*.md | wc -l` — 5 before, 6 after; `grep -c 'docs/domain/\*\*' plugins/working-process/rules/propagation-duties.md` — no file before, 1 after; `grep -c 'six rule files' plugins/working-process/README.md` — 0 before, 1 after; `claude plugin validate plugins/working-process` passes after. + +## Review rounds + +### 2026-09-11 — architect, fable 5.1, blocking (round 1, full-document) + +The propagation gate ran first and returned `CLEAN`, so this round wrote +no gate lines. Every citation the report supplied — twenty of them, +across four rule files, the glossary, a skill, an agent card, the repo +`CLAUDE.md` and two other specs — was checked against what it names +before anything was written here, and all twenty hold. + +- open — [Important] the confirming round is asserted to run "under the + same standing consent" on every path that can end a plan's rounds, + while three of the four newly covered paths are terminators that + suspend autonomy: `blocking` (`workflow.md:366`), the round cap + (`:371`) and the all-Minor signal (`:389`). The shipped sentence at + `:437` was written for the diff-scoped `LGTM` path alone, where + autonomy still stands +- open — [Important] "latest round heading means the highest ordinal" is + well-defined only if ordinals never restart, which nothing writes + down: the heading grammar is silent and `workflow.md:449` says a later + round on the same document opens a new loop. Practice continues the + count (`2026-08-24-process-status-and-anchor-hardening-design.md:388` + is `LGTM (round 1)`, `:420` is `concerns (round 2)`), but W1 keys on it +- open — [Important] widening the Unresolved verdict owner leg to any + diff-scoped latest heading leaves its own co-firing sentence false: a + `concerns` or `blocking` diff-scoped heading produces no Chain debt + hit, because that class's command keys on `LGTM` + (`spec-plan-lifecycle.md:378-383`). Decision 3 and W4 both rest on + that sentence +- open — [Important] W6 puts the branch-naming convention in + `ticket-frontmatter.md`, whose `paths: docs/**` frontmatter cannot fire + at branch creation — step 1, typically before any `docs/` file exists. + The only unscoped rule in the payload is `workflow.md`, which owns + that step +- open — [Minor] the minted **Confirming round** entry says a spec owes + none, while `workflow.md:416`, `:421` and `:424` use the same words for + the spec's pair-offer arm +- open — [Minor] the sync-rules exclusion describes a project-level + install that no longer exists and calls it "Ignored mode", a term the + glossary defines for Process directories only — which `.claude/rules/` + explicitly is not +- open — [Minor] decision 5's stated cost covers in-flight plans only: a + plan reaching `implemented` with a diff-scoped latest heading becomes a + permanent Unresolved verdict resident with no disposition, where the + sibling Chain debt class already has an implemented-document decline +- open — [Minor] W7's "costs nothing outside authoring" overstates: the + rule loads for every agent that reads a matching document, including + the propagation-auditor, which already carries the same eight duties +- open — [Minor] "None needs a design of its own" is contradicted by the + document's own content, since W1 reshapes what closes a plan and mints + a glossary term +- signal 2026-09-11 — another round earns its cost: F1 to F4 change rule + text the whole wave hangs on and F1 touches an entry minted at + grilling, so the fix wave deserves a diff-scoped read; the leftovers + after it are worth little, being wording and one honest cost statement From 173bdf465d3e69d01af49143199e9ab06a79c63e Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 20:44:21 +0200 Subject: [PATCH 045/120] docs: apply architect round one to the errata wave-four spec --- docs/domain/glossary.md | 10 +- ...2026-09-11-review-loop-errata-wave-four.md | 196 ++++++++++-------- 2 files changed, 113 insertions(+), 93 deletions(-) diff --git a/docs/domain/glossary.md b/docs/domain/glossary.md index c3896b8..7afacb6 100644 --- a/docs/domain/glossary.md +++ b/docs/domain/glossary.md @@ -377,10 +377,12 @@ _Avoid_: chain accepted, chain closed, unclosed chain **Confirming round**: The full-document round a plan's loop owes before it may close, whenever the latest round heading is diff-scoped — whatever path would otherwise -end the rounds. It runs as the same loop's last round under its standing -consent, counts against the round cap like any other, and its verdict is -the one stamped; on a plan the resolution annotation and the -adjudication wait for it. A spec owes none: its diff-scoped chain is +end the rounds. It runs as the same loop's last round, counts against +the round cap like any other, and its verdict is the one stamped; on a +plan the resolution annotation and the adjudication wait for it. It +inherits the gating of whatever ended the rounds: autonomous where +autonomy still stands, the developer's to order after a terminator that +suspends it. A spec owes none: its diff-scoped chain is discharged at the consumption gate, where a full-document round is one arm of the pair offer. _Avoid_: final round, closing round, full read (as the name) diff --git a/docs/specs/2026-09-11-review-loop-errata-wave-four.md b/docs/specs/2026-09-11-review-loop-errata-wave-four.md index 887cf04..19bcc65 100644 --- a/docs/specs/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/specs/2026-09-11-review-loop-errata-wave-four.md @@ -17,9 +17,15 @@ Two outside cycles ran working-process 0.13.0 through 0.16.0 on real client work and groomed their findings against each release. Seven items survived 0.16.0. Three of them were decisions the developer had not yet taken; four were sentences nobody had written; grilling found -two more decisions the rules had left implicit. None needs a -design of its own, and each is cheap to remove — which is what makes -this a wave of errata rather than a fifth follow-up spec. +two more decisions the rules had left implicit. Six are errata: they +remove a contradiction or write down what the rules already implied. +One is not — W1, with decisions 1, 4 and 5 and the term they need, +changes what closes a plan and withdraws a close path the shipped rules +granted. It rides here rather than in a spec of its own because its +alternatives are recorded beside its rulings, which is what a design +document's alternatives section would have carried, and because round +one found its rough edges to be one-clause fixes rather than a reshape +(developer ruling, 2026-09-11). The findings live in this repo's Private memory as lifted copies (`idea-working-process-second-cycle`, `idea-working-process-sweep-verifier`), @@ -75,9 +81,14 @@ where a ledger later cites them. ordinal is on every heading, including those predating the `scope` token, so it decides; block order stays unprescribed, since reordering closed documents is body editing and a state-aware lint - can flag order later. Alternatives declined: mandating newest-first - (leaves ten documents non-compliant) and reading file position - (leaves four). `ruling: 2026-09-11`. + can flag order later. The ordinal decides only while it never + restarts, which nothing said: the heading grammar is silent and the + workflow rule has a later round opening a new loop. So the rule now + says it — ordinals run per document and per field and continue across + loops, which is what the round cap's own derivation from the headings + already assumed and what practice already does. Alternatives + declined: mandating newest-first (leaves ten documents non-compliant) + and reading file position (leaves four). `ruling: 2026-09-11`. 5. **On a plan, the resolution annotation and the adjudication wait for the full-document round; the round has no decline path.** @@ -85,18 +96,28 @@ where a ledger later cites them. whose latest heading is diff-scoped. The annotation is written only once the latest heading is full-document; until then the plan stays `adversary: concerns` unannotated, which the Unresolved verdict - command matches, so the owed round is visible and the confirming - round is the same loop's last round under the same standing consent. - A developer who wants no further round leaves the plan open, and + command matches, so the owed round is visible. The confirming round + is the same loop's last round, and it inherits the gating of whatever + ended the rounds: autonomous where autonomy still stands — a + diff-scoped `LGTM`, or `concerns` inside the cap — and the + developer's to order after `blocking`, the round cap or the all-Minor + signal, each of which suspends autonomy by its own terminator. A + developer who wants no further round leaves the plan open, and that is the honest state. Alternatives declined: a decline recorded with `, debt discharged ` on a `concerns` heading, which uses the Chain debt token outside the term's definition and would reopen a five-day-old concept; and treating the confirming round as a new loop after the annotation, which strips it of standing consent and leaves the plan clean for the Unfinished-work list between the two. - The cost is stated: plans get no decline path where specs have three - discharge paths. A later erratum may add one, on a measured plan - whose confirming round earned nothing. `ruling: 2026-09-11`. + The cost is stated: an in-flight plan gets no decline path where a + spec has three discharge paths, and a later erratum may add one, on a + measured plan whose confirming round earned nothing. A plan already + at `status: implemented` is the exception, on the ground its sibling + class already ships: completed work is not re-reviewed, so there the + round is discharged by recorded decline without any dispatch, exactly + as the Chain debt owner leg discharges its debt. Without that clause + such a plan would sit on the Unfinished-work list for good, reporting + finished work as unfinished. `ruling: 2026-09-11`. A sixth question from the chain-debt design — whether a future undischarged instance gets the same retroactive decline as the live @@ -115,20 +136,35 @@ sentence, the Confirming round entry) and one owed (W5). paragraph generalizes from "never terminates on a diff-scoped LGTM" to "closes only when the latest verdict round was full-document", and the recovery sentence reads "latest round heading is diff-scoped, - whatever its verdict". `spec-plan-lifecycle.md`, Unfinished-work - list, Unresolved verdict owner leg: the plan exception widens the - same way — the confirming round closes the verdict and the annotation - may not, on any plan whose latest heading is diff-scoped. The same - rule's *The disposition ledger* section gains one sentence defining - the latest round heading by ordinal (decision 4); the glossary's - Round heading entry already carries it, applied at grilling. The - rule's resolution-annotation bullet — "Concerns resolved without a - fresh review round keep the verdict and gain a resolution date" — - gains the plan clause of decision 5: on a plan the annotation and - the adjudication are written only once the latest round heading is - full-document. The glossary gains **Confirming round**, minted at - grilling: both rules use the phrase throughout for the object W1 keys - on, and no entry defined it. + whatever its verdict". The same paragraph gains the gating sentence: + the confirming round inherits the gating of whatever ended the + rounds, autonomous only where autonomy still stands, since + `blocking`, the round cap and the all-Minor signal each suspend it by + their own terminator. Three sentences in that section rename the + spec's pair-offer arm from "confirming round" to "a full-document + round", so the term names one object. + + `spec-plan-lifecycle.md`, Unfinished-work list, Unresolved verdict + owner leg: the plan exception widens to any plan whose latest heading + is diff-scoped — the confirming round closes the verdict and the + annotation may not. Its co-firing sentence narrows in the same edit, + to "where that heading is an `LGTM`, such a plan matches Chain debt as + well": that class's command keys on `LGTM`, so a diff-scoped + `concerns` heading produces no second hit and the unnarrowed sentence + would be false for the case W1 adds. The leg also gains the + implemented-document decline of decision 5, in the shape the Chain + debt leg already uses. + + The same rule's *The disposition ledger* section gains two sentences: + the latest round heading is the one carrying the highest ordinal + wherever it sits, and ordinals run per document and per field and + continue across loops (decision 4). The rule's resolution-annotation + bullet — "Concerns resolved without a fresh review round keep the + verdict and gain a resolution date" — gains the plan clause of + decision 5: on a plan the annotation and the adjudication are written + only once the latest round heading is full-document. The glossary + gains **Confirming round**, minted at grilling and amended in this + wave to carry the gating clause. 2. **W2 — what the cap does not promise.** `workflow.md`, Terminators, Round cap bullet: one sentence saying a run of `blocking` verdicts is unbounded by the cap, because the cap counts autonomous rounds and @@ -143,21 +179,31 @@ sentence, the Confirming round entry) and one owed (W5). the clause "recorded decline — of the gate's pair offer, or, where …, of the question …" loses its comma pile; the term's meaning does not move. -6. **W6 — topic-branch naming.** `ticket-frontmatter.md` gains a short - *Branch naming* section: `feature/-`, +6. **W6 — topic-branch naming.** `workflow.md` gains a short *Branch + naming* paragraph near step 1: `feature/-`, `feature/` where the ticket is `none`, auto-generated worktree names renamed to it before the first commit, and the - `branch:` field recording the result. The rule's sourcing paragraph - already assumes this shape; the section makes the assumption a - convention. The repo-level half landed in this repo's `CLAUDE.md` - on 2026-07-22; this is the cross-project half. + `branch:` field recording the result. It goes to the workflow rule + rather than to `ticket-frontmatter.md`, whose `paths: docs/**` + frontmatter cannot fire at the moment the convention binds — a branch + is cut before any `docs/` file of that work exists, and `workflow.md` + is the payload's only unscoped rule and the owner of step 1. The + ticket rule's sourcing paragraph, which already reads a ticket out of + `feature/ABC-123-...`, gains a pointer rather than the convention. + The repo-level half landed in this repo's `CLAUDE.md` on 2026-07-22; + this is the cross-project half. 7. **W7 — the propagation duties as an author-facing rule.** A new rule file, `rules/propagation-duties.md`, scoped by `paths:` to `docs/specs/**`, `docs/plans/**` and `docs/domain/**` — the first two as the process-artifacts rule scopes them, the third because duty 1 fires when a glossary `_Avoid_` ban is minted, and a rule - silent at the glossary would miss the one edit that duty names. It - costs nothing outside authoring. It keys the eight duties by + silent at the glossary would miss the one edit that duty names. + Outside authoring it costs nothing; inside it, it loads for every + reader of a matching document, dispatched agents included — the + propagation-auditor among them, which already carries these duties in + its card. That duplication is accepted, and bounded by what the rule + holds: the trigger list and the enumeration each demands, keyed to + the card's duty numbers, and none of the card's measurements. It keys the eight duties by the edit an author just made — changed an interface, prescribed a block, added a field or label, asserted a count, used a name the source does not define, reported another document's state, wrote a @@ -170,22 +216,24 @@ sentence, the Confirming round entry) and one owed (W5). ## What this wave does not do -- **No decline path for a plan's confirming round** (decision 5). A - plan whose latest heading is diff-scoped stays unannotated and - visible to the Unresolved verdict command until the round runs; the - developer's only lever is to order the round or leave the plan open. - An annotation written before the round would be a rule violation, - and a violated plan is invisible to the list the way any violation - is — the recovery clause in `workflow.md`, re-offer at the document's - next touch, is what catches it. A state-aware lint reading headings by - ordinal is where a mechanical check belongs. +- **No decline path for an in-flight plan's confirming round** + (decision 5). Such a plan stays unannotated and visible to the + Unresolved verdict command until the round runs; the developer's only + lever is to order the round or leave the plan open. An annotation + written before the round would be a rule violation, and a violated + plan is invisible to the list the way any violation is — the recovery + clause in `workflow.md`, re-offer at the document's next touch, is + what catches it. A state-aware lint reading headings by ordinal is + where a mechanical check belongs. A plan already `implemented` is not + this case: W1 gives it the recorded decline. - **The Chain debt term keeps its meaning.** It names what a diff-scoped `LGTM` leaves. W1 gives a plan a broader obligation, owned by the workflow rule, and does not fold it into the term. - **The sync-rules install is the developer's.** Adding a rule file - changes the payload's ruleset hash; the project-level install under - `.claude/rules/working-process/` is Ignored mode and updates by - running `sync-rules`, which is not a commit on this branch. + changes the payload's ruleset hash, so every rules target carrying + this payload drifts until `sync-rules` updates it — this machine's is + user-level, at `~/.claude/rules/working-process/`. Running it is not a + commit on this branch and not part of this wave. - **Nothing here reaches `superpowers:writing-plans`.** The mutation proof, the spec-coverage duty, the seam checklist, the authoring bans and the platform-API class stay in the valuable package; each needs @@ -197,12 +245,14 @@ Each change is checked by grep against the shipped file, whitespace normalized so a wrapped phrase still matches; a check states both its before and after value. -- W1: `tr -s '[:space:]' ' ' < plugins/working-process/rules/workflow.md | grep -o 'never terminates on a diff-scoped LGTM' | wc -l` — 1 before, 0 after; `grep -o 'latest verdict round was full-document' | wc -l` over the same stream — 0 before, 1 after. `grep -c 'whose latest round heading is a$' plugins/working-process/rules/spec-plan-lifecycle.md` — the owner leg's line — 1 before, 0 after. Over the whitespace-normalized lifecycle rule: `grep -o 'highest ordinal' | wc -l` — 0 before, 1 after (decision 4); `grep -o 'wait for the confirming round' | wc -l` — 0 before, 1 after (decision 5). `grep -c '^\*\*Confirming round\*\*:' docs/domain/glossary.md` and `grep -c 'highest ordinal' docs/domain/glossary.md` — both 1 before and after: applied at grilling, declared invariants. +- W1, `workflow.md`, whitespace-normalized: `grep -o 'never terminates on a diff-scoped LGTM' | wc -l` — 1 before, 0 after; `grep -o 'latest verdict round was full-document' | wc -l` — 0 before, 1 after; `grep -o 'inherits the gating of whatever ended' | wc -l` — 0 before, 1 after (round one's F1); `grep -o 'a confirming round on a spec' | wc -l` — 1 before, 0 after (round one's M1, the spec arm renamed). +- W1, `spec-plan-lifecycle.md`: `grep -c 'whose latest round heading is a$' ` — the owner leg's line — 1 before, 0 after. Whitespace-normalized: `grep -o 'highest ordinal' | wc -l` — 0 before, 1 after (decision 4); `grep -o 'continue across loops' | wc -l` — 0 before, 1 after (round one's F2); `grep -o 'where that heading is an .LGTM.' | wc -l` — 0 before, 1 after (round one's F3); `grep -o 'wait for the confirming round' | wc -l` — 0 before, 1 after (decision 5). +- W1, glossary: `grep -c '^\*\*Confirming round\*\*:' docs/domain/glossary.md` — 1 before and after, a declared invariant, the entry having landed at grilling; `tr -s '[:space:]' ' ' < docs/domain/glossary.md | grep -o 'inherits the gating' | wc -l` — 0 before, 1 after (round one's F1 reaches the entry too). - W2: `tr -s '[:space:]' ' ' < plugins/working-process/rules/workflow.md | grep -o 'unbounded by the cap' | wc -l` — 0 before, 1 after. - W3: `tr -s '[:space:]' ' ' < plugins/working-process/rules/spec-plan-lifecycle.md | grep -o 'never stands in for the' | wc -l` — 0 before, 1 after. - W4: `grep -c 'Unresolved verdict and Chain debt' plugins/working-process/skills/process-status/SKILL.md` — 0 before, 1 after. - W5: `grep -c 'decline — of the gate' docs/domain/glossary.md` — 1 before, 0 after. -- W6: `grep -c '^## Branch naming' plugins/working-process/rules/ticket-frontmatter.md` — 0 before, 1 after. +- W6: `grep -c '^## Branch naming' plugins/working-process/rules/workflow.md` — 0 before, 1 after; the same command against `ticket-frontmatter.md` — 0 before and after, a declared invariant, since round one moved the section out of that rule; `tr -s '[:space:]' ' ' < plugins/working-process/rules/ticket-frontmatter.md | grep -o 'branch naming convention' | wc -l` — 0 before, 1 after (the pointer that replaces it). - W7: `ls plugins/working-process/rules/*.md | wc -l` — 5 before, 6 after; `grep -c 'docs/domain/\*\*' plugins/working-process/rules/propagation-duties.md` — no file before, 1 after; `grep -c 'six rule files' plugins/working-process/README.md` — 0 before, 1 after; `claude plugin validate plugins/working-process` passes after. ## Review rounds @@ -215,47 +265,15 @@ across four rule files, the glossary, a skill, an agent card, the repo `CLAUDE.md` and two other specs — was checked against what it names before anything was written here, and all twenty hold. -- open — [Important] the confirming round is asserted to run "under the - same standing consent" on every path that can end a plan's rounds, - while three of the four newly covered paths are terminators that - suspend autonomy: `blocking` (`workflow.md:366`), the round cap - (`:371`) and the all-Minor signal (`:389`). The shipped sentence at - `:437` was written for the diff-scoped `LGTM` path alone, where - autonomy still stands -- open — [Important] "latest round heading means the highest ordinal" is - well-defined only if ordinals never restart, which nothing writes - down: the heading grammar is silent and `workflow.md:449` says a later - round on the same document opens a new loop. Practice continues the - count (`2026-08-24-process-status-and-anchor-hardening-design.md:388` - is `LGTM (round 1)`, `:420` is `concerns (round 2)`), but W1 keys on it -- open — [Important] widening the Unresolved verdict owner leg to any - diff-scoped latest heading leaves its own co-firing sentence false: a - `concerns` or `blocking` diff-scoped heading produces no Chain debt - hit, because that class's command keys on `LGTM` - (`spec-plan-lifecycle.md:378-383`). Decision 3 and W4 both rest on - that sentence -- open — [Important] W6 puts the branch-naming convention in - `ticket-frontmatter.md`, whose `paths: docs/**` frontmatter cannot fire - at branch creation — step 1, typically before any `docs/` file exists. - The only unscoped rule in the payload is `workflow.md`, which owns - that step -- open — [Minor] the minted **Confirming round** entry says a spec owes - none, while `workflow.md:416`, `:421` and `:424` use the same words for - the spec's pair-offer arm -- open — [Minor] the sync-rules exclusion describes a project-level - install that no longer exists and calls it "Ignored mode", a term the - glossary defines for Process directories only — which `.claude/rules/` - explicitly is not -- open — [Minor] decision 5's stated cost covers in-flight plans only: a - plan reaching `implemented` with a diff-scoped latest heading becomes a - permanent Unresolved verdict resident with no disposition, where the - sibling Chain debt class already has an implemented-document decline -- open — [Minor] W7's "costs nothing outside authoring" overstates: the - rule loads for every agent that reads a matching document, including - the propagation-auditor, which already carries the same eight duties -- open — [Minor] "None needs a design of its own" is contradicted by the - document's own content, since W1 reshapes what closes a plan and mints - a glossary term +- fixed 2026-09-11 — [Important] the confirming round was asserted to run "under the same standing consent" on every path that can end a plan's rounds, while `blocking`, the round cap and the all-Minor signal each suspend autonomy by their own terminator; license: those three terminators' own text (`workflow.md:366`, `:371`, `:389`), which the shipped sentence at `:437` was never written against; decision 5, the W1 scope item and the glossary entry now say the round inherits the gating of whatever ended the rounds, autonomous only where autonomy still stands +- fixed 2026-09-11 — [Important] "highest ordinal" was well-defined only if ordinals never restart, which nothing wrote down while `workflow.md:449` has a later round opening a new loop; license: the round cap's own derivation, which counts by folding round headings and is unsound under restarting ordinals; decision 4 and the W1 scope item now add the clause — ordinals run per document and per field and continue across loops +- fixed 2026-09-11 — [Important] widening the Unresolved verdict owner leg left its own co-firing sentence false for the case W1 adds, since the Chain debt command keys on `LGTM` and a diff-scoped `concerns` heading produces no second hit; license: that command, published in the same rule; the W1 scope item now narrows the sentence to "where that heading is an `LGTM`" in the same edit that widens the leg +- fixed 2026-09-11 — [Important] W6 placed the branch-naming convention in `ticket-frontmatter.md`, whose `paths: docs/**` frontmatter cannot fire when a branch is cut before any `docs/` file of that work exists; license: that rule's own frontmatter, and `workflow.md` being the payload's only unscoped rule and the owner of step 1; W6 now lands the paragraph in `workflow.md` and leaves the ticket rule a pointer +- fixed 2026-09-11 — [Minor] the minted **Confirming round** entry said a spec owes none while `workflow.md` used the same words for the spec's pair-offer arm; ruling: 2026-09-11; the term stays plan-only and W1 renames the spec arm to "a full-document round" in the three sentences that carry it +- fixed 2026-09-11 — [Minor] the sync-rules exclusion described a project-level install removed during this very round and called it "Ignored mode", which the glossary defines for Process directories only; license: the glossary's Process directory entry, which names `.claude/rules/` as not one; the bullet now states the mechanism — the ruleset hash changes and every target carrying the payload drifts until `sync-rules` runs — and names this machine's user-level target +- fixed 2026-09-11 — [Minor] decision 5's stated cost covered in-flight plans only, leaving a plan that reaches `implemented` with a diff-scoped latest heading a permanent Unresolved verdict resident; ruling: 2026-09-11; the implemented-document decline of the sibling Chain debt leg now extends to the confirming round, on the ground that leg already ships — completed work is not re-reviewed +- fixed 2026-09-11 — [Minor] W7's "costs nothing outside authoring" ignored that the rule loads for every reader of a matching document, the propagation-auditor included, which already carries these duties; license: that agent's card; the item now accepts the duplication and bounds the rule to the trigger list plus duty numbers, with none of the card's measurements +- fixed 2026-09-11 — [Minor] "None needs a design of its own" was contradicted by the document's own content; ruling: 2026-09-11; the Problem section now says six errata and one design item, and why W1 rides here rather than in a spec of its own - signal 2026-09-11 — another round earns its cost: F1 to F4 change rule text the whole wave hangs on and F1 touches an entry minted at grilling, so the fix wave deserves a diff-scoped read; the leftovers From 800422d648b73f0b6f2ac933af2c1c90c6c0ae75 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 20:50:21 +0200 Subject: [PATCH 046/120] docs: repair the glossary check the fix wave invalidated --- docs/specs/2026-09-11-review-loop-errata-wave-four.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/specs/2026-09-11-review-loop-errata-wave-four.md b/docs/specs/2026-09-11-review-loop-errata-wave-four.md index 19bcc65..7181faf 100644 --- a/docs/specs/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/specs/2026-09-11-review-loop-errata-wave-four.md @@ -247,7 +247,7 @@ before and after value. - W1, `workflow.md`, whitespace-normalized: `grep -o 'never terminates on a diff-scoped LGTM' | wc -l` — 1 before, 0 after; `grep -o 'latest verdict round was full-document' | wc -l` — 0 before, 1 after; `grep -o 'inherits the gating of whatever ended' | wc -l` — 0 before, 1 after (round one's F1); `grep -o 'a confirming round on a spec' | wc -l` — 1 before, 0 after (round one's M1, the spec arm renamed). - W1, `spec-plan-lifecycle.md`: `grep -c 'whose latest round heading is a$' ` — the owner leg's line — 1 before, 0 after. Whitespace-normalized: `grep -o 'highest ordinal' | wc -l` — 0 before, 1 after (decision 4); `grep -o 'continue across loops' | wc -l` — 0 before, 1 after (round one's F2); `grep -o 'where that heading is an .LGTM.' | wc -l` — 0 before, 1 after (round one's F3); `grep -o 'wait for the confirming round' | wc -l` — 0 before, 1 after (decision 5). -- W1, glossary: `grep -c '^\*\*Confirming round\*\*:' docs/domain/glossary.md` — 1 before and after, a declared invariant, the entry having landed at grilling; `tr -s '[:space:]' ' ' < docs/domain/glossary.md | grep -o 'inherits the gating' | wc -l` — 0 before, 1 after (round one's F1 reaches the entry too). +- W1, glossary: `grep -c '^\*\*Confirming round\*\*:' docs/domain/glossary.md` — 1 before and after, a declared invariant, the entry having landed at grilling; `tr -s '[:space:]' ' ' < docs/domain/glossary.md | grep -o 'inherits the gating' | wc -l` — 1 before and after, a declared invariant: round one's F1 reached the entry, so this text landed with that fix wave rather than with the prescribed rule edits. - W2: `tr -s '[:space:]' ' ' < plugins/working-process/rules/workflow.md | grep -o 'unbounded by the cap' | wc -l` — 0 before, 1 after. - W3: `tr -s '[:space:]' ' ' < plugins/working-process/rules/spec-plan-lifecycle.md | grep -o 'never stands in for the' | wc -l` — 0 before, 1 after. - W4: `grep -c 'Unresolved verdict and Chain debt' plugins/working-process/skills/process-status/SKILL.md` — 0 before, 1 after. @@ -274,6 +274,7 @@ before anything was written here, and all twenty hold. - fixed 2026-09-11 — [Minor] decision 5's stated cost covered in-flight plans only, leaving a plan that reaches `implemented` with a diff-scoped latest heading a permanent Unresolved verdict resident; ruling: 2026-09-11; the implemented-document decline of the sibling Chain debt leg now extends to the confirming round, on the ground that leg already ships — completed work is not re-reviewed - fixed 2026-09-11 — [Minor] W7's "costs nothing outside authoring" ignored that the rule loads for every reader of a matching document, the propagation-auditor included, which already carries these duties; license: that agent's card; the item now accepts the duplication and bounds the rule to the trigger list plus duty numbers, with none of the card's measurements - fixed 2026-09-11 — [Minor] "None needs a design of its own" was contradicted by the document's own content; ruling: 2026-09-11; the Problem section now says six errata and one design item, and why W1 rides here rather than in a spec of its own +- hit fixed 2026-09-11 — the W1 glossary check stated 0 before for "inherits the gating" while the fix wave had just written that clause into the entry, so the check would have failed against a correct tree; the check is now a declared invariant at 1, like the Confirming round entry check beside it. Re-simulating all twenty before-values found no second instance - signal 2026-09-11 — another round earns its cost: F1 to F4 change rule text the whole wave hangs on and F1 touches an entry minted at grilling, so the fix wave deserves a diff-scoped read; the leftovers From 3cdc849615d4db8cda952ff6e942a215a7817e44 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 21:01:18 +0200 Subject: [PATCH 047/120] docs: apply architect round two to the errata wave-four spec --- docs/domain/glossary.md | 4 +- ...2026-09-11-review-loop-errata-wave-four.md | 59 +++++++++++++++---- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/docs/domain/glossary.md b/docs/domain/glossary.md index 7afacb6..9395214 100644 --- a/docs/domain/glossary.md +++ b/docs/domain/glossary.md @@ -377,7 +377,9 @@ _Avoid_: chain accepted, chain closed, unclosed chain **Confirming round**: The full-document round a plan's loop owes before it may close, whenever the latest round heading is diff-scoped — whatever path would otherwise -end the rounds. It runs as the same loop's last round, counts against +end the rounds. A plan already `implemented` owes none: there the round +is discharged by recorded decline without any dispatch, as the sibling +chain debt is, since completed work is not re-reviewed. It runs as the same loop's last round, counts against the round cap like any other, and its verdict is the one stamped; on a plan the resolution annotation and the adjudication wait for it. It inherits the gating of whatever ended the rounds: autonomous where diff --git a/docs/specs/2026-09-11-review-loop-errata-wave-four.md b/docs/specs/2026-09-11-review-loop-errata-wave-four.md index 7181faf..8182ff7 100644 --- a/docs/specs/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/specs/2026-09-11-review-loop-errata-wave-four.md @@ -3,7 +3,7 @@ ticket: none date: 2026-09-11 status: draft grilled: 2026-09-11 -architect: blocking +architect: concerns revises: [./2026-08-17-autonomous-review-loop-design.md, ./2026-09-07-diff-scoped-chain-debt-design.md, ./2026-08-27-audit-agents-design.md] branch: feature/process-wave-four base: develop @@ -114,8 +114,13 @@ where a ledger later cites them. measured plan whose confirming round earned nothing. A plan already at `status: implemented` is the exception, on the ground its sibling class already ships: completed work is not re-reviewed, so there the - round is discharged by recorded decline without any dispatch, exactly - as the Chain debt owner leg discharges its debt. Without that clause + round is discharged by recorded decline without any dispatch, as the + Chain debt owner leg discharges its debt. The plan clause below + carries the same exception, or it would forbid the very annotation + this one requires. One thing the analogy does not carry: a `blocking` + verdict closes by adjudication, which the glossary defines as the + developer's own act, so on an implemented plan a session derives the + `concerns` decline and never the adjudication. Without that clause such a plan would sit on the Unfinished-work list for good, reporting finished work as unfinished. `ruling: 2026-09-11`. @@ -140,9 +145,10 @@ sentence, the Confirming round entry) and one owed (W5). the confirming round inherits the gating of whatever ended the rounds, autonomous only where autonomy still stands, since `blocking`, the round cap and the all-Minor signal each suspend it by - their own terminator. Three sentences in that section rename the + their own terminator. Four sentences in that section rename the spec's pair-offer arm from "confirming round" to "a full-document - round", so the term names one object. + round" — the enumeration is `workflow.md:416`, `:419`, `:421` and + `:424` — so the term names one object. `spec-plan-lifecycle.md`, Unfinished-work list, Unresolved verdict owner leg: the plan exception widens to any plan whose latest heading @@ -162,15 +168,25 @@ sentence, the Confirming round entry) and one owed (W5). bullet — "Concerns resolved without a fresh review round keep the verdict and gain a resolution date" — gains the plan clause of decision 5: on a plan the annotation and the adjudication are written - only once the latest round heading is full-document. The glossary + only once the latest round heading is full-document, unless the plan + is already `implemented`, where the annotation is written citing that + standing decision — the exception the owner leg carries, stated where + the prohibition is, since an `implemented` document is amended in + frontmatter and the annotation is the only lever left. The glossary gains **Confirming round**, minted at grilling and amended in this wave to carry the gating clause. -2. **W2 — what the cap does not promise.** `workflow.md`, Terminators, +2. **W2 — what the terminators do not promise.** `workflow.md`, Terminators, Round cap bullet: one sentence saying a run of `blocking` verdicts is unbounded by the cap, because the cap counts autonomous rounds and `blocking` suspends autonomy — each continuation is the developer's own decision, so consenting to three rounds consents to three - autonomous ones. + autonomous ones. The all-Minor bullet gains a plan clause in the same + edit: it says today that the resolution annotation records the + developer's close, which W1 makes untrue on a plan until the + confirming round runs. A session executing a terminator reads that + terminator's bullet, not the section three pages down, and the cap + bullet already carries plan-specific text, so the pointer belongs + there. 3. **W3 — sha beside hash** (decision 2). `spec-plan-lifecycle.md`, the per-round-commits paragraph: one sentence. 4. **W4 — co-firing line** (decision 3). `process-status/SKILL.md`, @@ -179,8 +195,11 @@ sentence, the Confirming round entry) and one owed (W5). the clause "recorded decline — of the gate's pair offer, or, where …, of the question …" loses its comma pile; the term's meaning does not move. -6. **W6 — topic-branch naming.** `workflow.md` gains a short *Branch - naming* paragraph near step 1: `feature/-`, +6. **W6 — topic-branch naming.** `workflow.md` gains a short + `## Branch naming` section, after the numbered flow and before + `## Dispatching a verdict agent` — the file's only other H2, and the + flow's steps name no branch-creation moment to sit beside: + `feature/-`, `feature/` where the ticket is `none`, auto-generated worktree names renamed to it before the first commit, and the `branch:` field recording the result. It goes to the workflow rule @@ -245,10 +264,10 @@ Each change is checked by grep against the shipped file, whitespace normalized so a wrapped phrase still matches; a check states both its before and after value. -- W1, `workflow.md`, whitespace-normalized: `grep -o 'never terminates on a diff-scoped LGTM' | wc -l` — 1 before, 0 after; `grep -o 'latest verdict round was full-document' | wc -l` — 0 before, 1 after; `grep -o 'inherits the gating of whatever ended' | wc -l` — 0 before, 1 after (round one's F1); `grep -o 'a confirming round on a spec' | wc -l` — 1 before, 0 after (round one's M1, the spec arm renamed). +- W1, `workflow.md`, whitespace-normalized: `grep -o 'never terminates on a diff-scoped LGTM' | wc -l` — 1 before, 0 after; `grep -o 'latest verdict round was full-document' | wc -l` — 0 before, 1 after; `grep -o 'inherits the gating of whatever ended' | wc -l` — 0 before, 1 after (round one's F1); `grep -o 'a confirming round on a spec' | wc -l` — 1 before, 0 after, and `grep -o 'confirming round alone' | wc -l` — 1 before, 0 after: the two sentences of round one's M1 that a single anchor missed (round two's F3, which enumerated all four). - W1, `spec-plan-lifecycle.md`: `grep -c 'whose latest round heading is a$' ` — the owner leg's line — 1 before, 0 after. Whitespace-normalized: `grep -o 'highest ordinal' | wc -l` — 0 before, 1 after (decision 4); `grep -o 'continue across loops' | wc -l` — 0 before, 1 after (round one's F2); `grep -o 'where that heading is an .LGTM.' | wc -l` — 0 before, 1 after (round one's F3); `grep -o 'wait for the confirming round' | wc -l` — 0 before, 1 after (decision 5). -- W1, glossary: `grep -c '^\*\*Confirming round\*\*:' docs/domain/glossary.md` — 1 before and after, a declared invariant, the entry having landed at grilling; `tr -s '[:space:]' ' ' < docs/domain/glossary.md | grep -o 'inherits the gating' | wc -l` — 1 before and after, a declared invariant: round one's F1 reached the entry, so this text landed with that fix wave rather than with the prescribed rule edits. -- W2: `tr -s '[:space:]' ' ' < plugins/working-process/rules/workflow.md | grep -o 'unbounded by the cap' | wc -l` — 0 before, 1 after. +- W1, glossary: `grep -c '^\*\*Confirming round\*\*:' docs/domain/glossary.md` — 1 before and after, a declared invariant, the entry having landed at grilling; `tr -s '[:space:]' ' ' < docs/domain/glossary.md | grep -o 'inherits the gating' | wc -l` — 1 before and after, and `grep -o 'A plan already .implemented. owes none' | wc -l` over the same stream — 1 before and after: both declared invariants, since rounds one and two wrote those clauses into the entry rather than leaving them to the prescribed rule edits. +- W2: over the whitespace-normalized `workflow.md`, `grep -o 'unbounded by the cap' | wc -l` — 0 before, 1 after; `grep -o 'records their close, and no session' | wc -l` — 1 before, 0 after (round two's F5: the all-Minor bullet's close sentence gains its plan clause). - W3: `tr -s '[:space:]' ' ' < plugins/working-process/rules/spec-plan-lifecycle.md | grep -o 'never stands in for the' | wc -l` — 0 before, 1 after. - W4: `grep -c 'Unresolved verdict and Chain debt' plugins/working-process/skills/process-status/SKILL.md` — 0 before, 1 after. - W5: `grep -c 'decline — of the gate' docs/domain/glossary.md` — 1 before, 0 after. @@ -274,6 +293,20 @@ before anything was written here, and all twenty hold. - fixed 2026-09-11 — [Minor] decision 5's stated cost covered in-flight plans only, leaving a plan that reaches `implemented` with a diff-scoped latest heading a permanent Unresolved verdict resident; ruling: 2026-09-11; the implemented-document decline of the sibling Chain debt leg now extends to the confirming round, on the ground that leg already ships — completed work is not re-reviewed - fixed 2026-09-11 — [Minor] W7's "costs nothing outside authoring" ignored that the rule loads for every reader of a matching document, the propagation-auditor included, which already carries these duties; license: that agent's card; the item now accepts the duplication and bounds the rule to the trigger list plus duty numbers, with none of the card's measurements - fixed 2026-09-11 — [Minor] "None needs a design of its own" was contradicted by the document's own content; ruling: 2026-09-11; the Problem section now says six errata and one design item, and why W1 rides here rather than in a spec of its own +### 2026-09-11 — architect, fable 5.1, concerns (round 2, diff-scoped) + +Scoped to round one's fix wave. Every citation checked before anything +was written here: the four spec-arm sentences, the one H2 in +`workflow.md`, and each rule and glossary line the report names. + +- fixed 2026-09-11 — [Important] the implemented-document decline of round one's M3 landed in decision 5 and the owner leg but not in the annotation-bullet clause the same wave prescribes, which forbids the annotation absolutely until the latest heading is full-document — so the plan it was meant to free would sit on the Unfinished-work list for good; license: decision 5's own ruling, which grants that decline, and the lifecycle rule's clause amending an `implemented` document in frontmatter only, which leaves the annotation the sole lever; the plan clause now carries the exception where the prohibition is +- fixed 2026-09-11 — [Minor] the Chain debt analogy carried derivability onto a `blocking` close, which the glossary defines as Adjudication — the developer's own act; license: that entry; decision 5 now says a session derives the `concerns` decline and never the adjudication +- fixed 2026-09-11 — [Minor] W1 said three sentences rename the spec's pair-offer arm where `workflow.md` carries four, and the check anchored only one of them, so `:419` would have survived and the term would name two objects again; license: the enumeration itself, `:416`, `:419`, `:421`, `:424`; the item now says four and names them, and the check gains the second anchor +- fixed 2026-09-11 — [Minor] the **Confirming round** entry, amended in this wave, states the obligation unconditionally while decision 5 exempts an implemented plan, where the sibling Chain debt entry carries its own exception in its definition; license: decision 5's ruling and that sibling's shape; the entry now carries the clause +- fixed 2026-09-11 — [Minor] the gating sentence of round one's F1 was placed in the diff-scoped-LGTM section while the terminators it names point nowhere at it, and the all-Minor bullet still says the resolution annotation records the developer's close — untrue on a plan once W1 lands; license: the cap bullet's own precedent of carrying plan-specific text, and that bullet's sentence being falsified by this wave; W2 now widens to the all-Minor bullet and says why the terminator's own bullet is where a session reads +- fixed 2026-09-11 — [Minor] W6 prescribed a paragraph "near step 1" while its check anchored an H2, and the flow's numbered steps name no branch-creation moment; license: the file's own structure — one H2, `## Dispatching a verdict agent`; the item now prescribes a `## Branch naming` section placed after the flow and before that H2 +- signal 2026-09-11 — a third round has moderate value: the F1 repair is one clause in two places and the rest are one-line fixes, so if it runs it should be a short diff-scoped read of the F1 and F5 repairs; the leftovers after it are wording, one number and one heading + - hit fixed 2026-09-11 — the W1 glossary check stated 0 before for "inherits the gating" while the fix wave had just written that clause into the entry, so the check would have failed against a correct tree; the check is now a declared invariant at 1, like the Confirming round entry check beside it. Re-simulating all twenty before-values found no second instance - signal 2026-09-11 — another round earns its cost: F1 to F4 change rule text the whole wave hangs on and F1 touches an entry minted at From 8b0ea66bcf1bae1f9810b837f730d8c9f74052dc Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 21:01:42 +0200 Subject: [PATCH 048/120] docs: restore the round-one gate line and signal to their own heading --- docs/specs/2026-09-11-review-loop-errata-wave-four.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/specs/2026-09-11-review-loop-errata-wave-four.md b/docs/specs/2026-09-11-review-loop-errata-wave-four.md index 8182ff7..51c5e49 100644 --- a/docs/specs/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/specs/2026-09-11-review-loop-errata-wave-four.md @@ -293,6 +293,9 @@ before anything was written here, and all twenty hold. - fixed 2026-09-11 — [Minor] decision 5's stated cost covered in-flight plans only, leaving a plan that reaches `implemented` with a diff-scoped latest heading a permanent Unresolved verdict resident; ruling: 2026-09-11; the implemented-document decline of the sibling Chain debt leg now extends to the confirming round, on the ground that leg already ships — completed work is not re-reviewed - fixed 2026-09-11 — [Minor] W7's "costs nothing outside authoring" ignored that the rule loads for every reader of a matching document, the propagation-auditor included, which already carries these duties; license: that agent's card; the item now accepts the duplication and bounds the rule to the trigger list plus duty numbers, with none of the card's measurements - fixed 2026-09-11 — [Minor] "None needs a design of its own" was contradicted by the document's own content; ruling: 2026-09-11; the Problem section now says six errata and one design item, and why W1 rides here rather than in a spec of its own +- hit fixed 2026-09-11 — the W1 glossary check stated 0 before for "inherits the gating" while the fix wave had just written that clause into the entry, so the check would have failed against a correct tree; the check is now a declared invariant at 1, like the Confirming round entry check beside it. Re-simulating all twenty before-values found no second instance +- signal 2026-09-11 — another round earns its cost: F1 to F4 change rule + ### 2026-09-11 — architect, fable 5.1, concerns (round 2, diff-scoped) Scoped to round one's fix wave. Every citation checked before anything @@ -306,9 +309,6 @@ was written here: the four spec-arm sentences, the one H2 in - fixed 2026-09-11 — [Minor] the gating sentence of round one's F1 was placed in the diff-scoped-LGTM section while the terminators it names point nowhere at it, and the all-Minor bullet still says the resolution annotation records the developer's close — untrue on a plan once W1 lands; license: the cap bullet's own precedent of carrying plan-specific text, and that bullet's sentence being falsified by this wave; W2 now widens to the all-Minor bullet and says why the terminator's own bullet is where a session reads - fixed 2026-09-11 — [Minor] W6 prescribed a paragraph "near step 1" while its check anchored an H2, and the flow's numbered steps name no branch-creation moment; license: the file's own structure — one H2, `## Dispatching a verdict agent`; the item now prescribes a `## Branch naming` section placed after the flow and before that H2 - signal 2026-09-11 — a third round has moderate value: the F1 repair is one clause in two places and the rest are one-line fixes, so if it runs it should be a short diff-scoped read of the F1 and F5 repairs; the leftovers after it are wording, one number and one heading - -- hit fixed 2026-09-11 — the W1 glossary check stated 0 before for "inherits the gating" while the fix wave had just written that clause into the entry, so the check would have failed against a correct tree; the check is now a declared invariant at 1, like the Confirming round entry check beside it. Re-simulating all twenty before-values found no second instance -- signal 2026-09-11 — another round earns its cost: F1 to F4 change rule text the whole wave hangs on and F1 touches an entry minted at grilling, so the fix wave deserves a diff-scoped read; the leftovers after it are worth little, being wording and one honest cost statement From dcb0a69e9fe578ef41fbebbd1b1dd8737e5d0800 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 21:12:18 +0200 Subject: [PATCH 049/120] docs: apply architect round three to the errata wave-four spec --- ...2026-09-11-review-loop-errata-wave-four.md | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/docs/specs/2026-09-11-review-loop-errata-wave-four.md b/docs/specs/2026-09-11-review-loop-errata-wave-four.md index 51c5e49..69fdbff 100644 --- a/docs/specs/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/specs/2026-09-11-review-loop-errata-wave-four.md @@ -159,7 +159,11 @@ sentence, the Confirming round entry) and one owed (W5). `concerns` heading produces no second hit and the unnarrowed sentence would be false for the case W1 adds. The leg also gains the implemented-document decline of decision 5, in the shape the Chain - debt leg already uses. + debt leg uses — with the distinction that leg never needed, since it + knows only `LGTM` headings: on an implemented plan a session derives + the `concerns` decline, while a `blocking` heading waits for the + developer's adjudication, which the glossary defines as their own + act. The same rule's *The disposition ledger* section gains two sentences: the latest round heading is the one carrying the highest ordinal @@ -183,7 +187,8 @@ sentence, the Confirming round entry) and one owed (W5). autonomous ones. The all-Minor bullet gains a plan clause in the same edit: it says today that the resolution annotation records the developer's close, which W1 makes untrue on a plan until the - confirming round runs. A session executing a terminator reads that + confirming round runs, so the sentence gains the qualifier "on a + plan, once the confirming round has run". A session executing a terminator reads that terminator's bullet, not the section three pages down, and the cap bullet already carries plan-specific text, so the pointer belongs there. @@ -196,9 +201,11 @@ sentence, the Confirming round entry) and one owed (W5). …, of the question …" loses its comma pile; the term's meaning does not move. 6. **W6 — topic-branch naming.** `workflow.md` gains a short - `## Branch naming` section, after the numbered flow and before - `## Dispatching a verdict agent` — the file's only other H2, and the - flow's steps name no branch-creation moment to sit beside: + `## Branch naming` section, immediately before + `## Dispatching a verdict agent` — the file's only other H2, so the + new one closes the material that follows the numbered flow rather + than swallowing it, and the flow's steps name no branch-creation + moment to sit beside: `feature/-`, `feature/` where the ticket is `none`, auto-generated worktree names renamed to it before the first commit, and the @@ -264,10 +271,10 @@ Each change is checked by grep against the shipped file, whitespace normalized so a wrapped phrase still matches; a check states both its before and after value. -- W1, `workflow.md`, whitespace-normalized: `grep -o 'never terminates on a diff-scoped LGTM' | wc -l` — 1 before, 0 after; `grep -o 'latest verdict round was full-document' | wc -l` — 0 before, 1 after; `grep -o 'inherits the gating of whatever ended' | wc -l` — 0 before, 1 after (round one's F1); `grep -o 'a confirming round on a spec' | wc -l` — 1 before, 0 after, and `grep -o 'confirming round alone' | wc -l` — 1 before, 0 after: the two sentences of round one's M1 that a single anchor missed (round two's F3, which enumerated all four). +- W1, `workflow.md`, whitespace-normalized: `grep -o 'never terminates on a diff-scoped LGTM' | wc -l` — 1 before, 0 after; `grep -o 'latest verdict round was full-document' | wc -l` — 0 before, 1 after; `grep -o 'inherits the gating of whatever ended' | wc -l` — 0 before, 1 after (round one's F1); one anchor per renamed sentence, each 1 before and 0 after — `grep -o 'or a confirming full-document round'` (`:416`), `grep -o 'confirming round alone'` (`:419`), `grep -o 'a confirming round on a spec'` (`:421`), `grep -o 'The confirming-round arm'` (`:424`). Round two anchored two of the four and round three the rest, which is why each now stands on its own sentence rather than on the count. - W1, `spec-plan-lifecycle.md`: `grep -c 'whose latest round heading is a$' ` — the owner leg's line — 1 before, 0 after. Whitespace-normalized: `grep -o 'highest ordinal' | wc -l` — 0 before, 1 after (decision 4); `grep -o 'continue across loops' | wc -l` — 0 before, 1 after (round one's F2); `grep -o 'where that heading is an .LGTM.' | wc -l` — 0 before, 1 after (round one's F3); `grep -o 'wait for the confirming round' | wc -l` — 0 before, 1 after (decision 5). - W1, glossary: `grep -c '^\*\*Confirming round\*\*:' docs/domain/glossary.md` — 1 before and after, a declared invariant, the entry having landed at grilling; `tr -s '[:space:]' ' ' < docs/domain/glossary.md | grep -o 'inherits the gating' | wc -l` — 1 before and after, and `grep -o 'A plan already .implemented. owes none' | wc -l` over the same stream — 1 before and after: both declared invariants, since rounds one and two wrote those clauses into the entry rather than leaving them to the prescribed rule edits. -- W2: over the whitespace-normalized `workflow.md`, `grep -o 'unbounded by the cap' | wc -l` — 0 before, 1 after; `grep -o 'records their close, and no session' | wc -l` — 1 before, 0 after (round two's F5: the all-Minor bullet's close sentence gains its plan clause). +- W2: over the whitespace-normalized `workflow.md`, `grep -o 'unbounded by the cap' | wc -l` — 0 before, 1 after; `grep -o 'records their close, and no session' | wc -l` — 1 before, 0 after, paired with `grep -o 'on a plan, once the confirming round has run' | wc -l` — 0 before, 1 after: the removal and the arrival, since a removal alone proves only that the sentence moved (round three's F4). - W3: `tr -s '[:space:]' ' ' < plugins/working-process/rules/spec-plan-lifecycle.md | grep -o 'never stands in for the' | wc -l` — 0 before, 1 after. - W4: `grep -c 'Unresolved verdict and Chain debt' plugins/working-process/skills/process-status/SKILL.md` — 0 before, 1 after. - W5: `grep -c 'decline — of the gate' docs/domain/glossary.md` — 1 before, 0 after. @@ -295,6 +302,9 @@ before anything was written here, and all twenty hold. - fixed 2026-09-11 — [Minor] "None needs a design of its own" was contradicted by the document's own content; ruling: 2026-09-11; the Problem section now says six errata and one design item, and why W1 rides here rather than in a spec of its own - hit fixed 2026-09-11 — the W1 glossary check stated 0 before for "inherits the gating" while the fix wave had just written that clause into the entry, so the check would have failed against a correct tree; the check is now a declared invariant at 1, like the Confirming round entry check beside it. Re-simulating all twenty before-values found no second instance - signal 2026-09-11 — another round earns its cost: F1 to F4 change rule + text the whole wave hangs on and F1 touches an entry minted at + grilling, so the fix wave deserves a diff-scoped read; the leftovers + after it are worth little, being wording and one honest cost statement ### 2026-09-11 — architect, fable 5.1, concerns (round 2, diff-scoped) @@ -309,6 +319,16 @@ was written here: the four spec-arm sentences, the one H2 in - fixed 2026-09-11 — [Minor] the gating sentence of round one's F1 was placed in the diff-scoped-LGTM section while the terminators it names point nowhere at it, and the all-Minor bullet still says the resolution annotation records the developer's close — untrue on a plan once W1 lands; license: the cap bullet's own precedent of carrying plan-specific text, and that bullet's sentence being falsified by this wave; W2 now widens to the all-Minor bullet and says why the terminator's own bullet is where a session reads - fixed 2026-09-11 — [Minor] W6 prescribed a paragraph "near step 1" while its check anchored an H2, and the flow's numbered steps name no branch-creation moment; license: the file's own structure — one H2, `## Dispatching a verdict agent`; the item now prescribes a `## Branch naming` section placed after the flow and before that H2 - signal 2026-09-11 — a third round has moderate value: the F1 repair is one clause in two places and the rest are one-line fixes, so if it runs it should be a short diff-scoped read of the F1 and F5 repairs; the leftovers after it are wording, one number and one heading - text the whole wave hangs on and F1 touches an entry minted at - grilling, so the fix wave deserves a diff-scoped read; the leftovers - after it are worth little, being wording and one honest cost statement + +### 2026-09-11 — architect, fable 5.1, concerns (round 3, diff-scoped) + +Scoped to round two's fix wave, as that round's stop signal asked. Every +citation checked first, the reviewer's note on this ledger's own seam +included. + +- fixed 2026-09-11 — [Important] round two's F1 repair carried the implemented-plan exception into four places but the verdict distinction into only one: the owner-leg prescription said "in the shape the Chain debt leg already uses", a shape neutral about the verdict because that leg knows only `LGTM`, so read literally it licensed a session to write `blocking (adjudicated )` — the developer's own act; license: decision 5's ruling and the glossary's **Adjudication** entry; the W1 prescription now carries the distinction into the rule text a session reads instead of leaving it in the spec +- fixed 2026-09-11 — [Minor] W6's placement phrase "after the numbered flow and before `## Dispatching a verdict agent`" spans five paragraphs, and the literal reading would pull them all under the new heading; license: the file's own structure, those paragraphs belonging to the flow rather than to branch naming; the item now says "immediately before" and says what that buys +- fixed 2026-09-11 — [Minor] the spec-arm rename enumerated four sentences and anchored two, leaving `:416` and `:424` to survive the edit — the same partial-anchor class round two had just repaired one layer down; license: the enumeration itself; the check now carries one anchor per renamed sentence +- fixed 2026-09-11 — [Minor] W2's check proved only the removal of the all-Minor bullet's close sentence and never named the clause replacing it, where every W1 check pairs a removal with an arrival; license: those checks' own shape; W2 now names the qualifier and the check anchors it 0 to 1 +- hit fixed 2026-09-11 — reordering the ledger at the previous commit moved only the first line of round one's multi-line `signal`, leaving its three continuation lines under round two's signal; the reviewer reported it as an integrity note rather than a finding, and the lines are back under their own +- signal 2026-09-11 — a fourth round would not repay: what remains after this wave is a clause, a placement phrase and two anchors, none of which needs design judgment. The residue class — partial anchors and drifted ledger lines — is what an integrity audit proves with two quotes, so the chain debt is better discharged at the consumption gate by the audit arm than by another round From 67fdf209e468c8966d9b2486309e9e046f53e06c Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 21:29:19 +0200 Subject: [PATCH 050/120] docs: apply architect round four to the errata wave-four spec --- ...2026-09-11-review-loop-errata-wave-four.md | 60 +++++++++++++++---- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/docs/specs/2026-09-11-review-loop-errata-wave-four.md b/docs/specs/2026-09-11-review-loop-errata-wave-four.md index 69fdbff..5fdb54c 100644 --- a/docs/specs/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/specs/2026-09-11-review-loop-errata-wave-four.md @@ -3,7 +3,7 @@ ticket: none date: 2026-09-11 status: draft grilled: 2026-09-11 -architect: concerns +architect: blocking revises: [./2026-08-17-autonomous-review-loop-design.md, ./2026-09-07-diff-scoped-chain-debt-design.md, ./2026-08-27-audit-agents-design.md] branch: feature/process-wave-four base: develop @@ -27,10 +27,15 @@ document's alternatives section would have carried, and because round one found its rough edges to be one-clause fixes rather than a reshape (developer ruling, 2026-09-11). -The findings live in this repo's Private memory as lifted copies +Three of the seven come from those cycles and live in this repo's +Private memory as lifted copies (`idea-working-process-second-cycle`, `idea-working-process-sweep-verifier`), -each pointing at its authoritative source. This document records the -decisions and the changes; it does not repeat the evidence. +each pointing at its authoritative source: W1's trigger, W2's cap +sentence and W7's author-facing rule. The other four are this repo's +own — W3 and W4 answer questions its own designs left open, W5 is a seam +in text it shipped, and W6 is the cross-project half of a convention it +adopted in 2026-07. This document records the decisions and the changes; +it does not repeat the evidence. ## Decisions taken 2026-09-11 @@ -140,15 +145,27 @@ sentence, the Confirming round entry) and one owed (W5). `workflow.md`, *What a diff-scoped LGTM certifies*: the plan paragraph generalizes from "never terminates on a diff-scoped LGTM" to "closes only when the latest verdict round was full-document", - and the recovery sentence reads "latest round heading is diff-scoped, - whatever its verdict". The same paragraph gains the gating sentence: + and the recovery sentence carries decision 1's trigger rather than + the heading alone: a diff-scoped `LGTM` keeps the shipped "at the + document's next touch", since that heading is itself an end, while a + diff-scoped heading of any other verdict re-offers the round when + something tries to end the plan's rounds — an annotation, an + adjudication, a `status` move — or when a terminator that suspends + autonomy has fired. Without that trigger the sentence would fire on a + plan merely mid-loop between two diff-scoped rounds, where the loop + rule prescribes a different next round: "Held set empty — dispatch + the next round without asking, within the cap", and every later round + diff-scoped. The same paragraph gains the gating sentence: the confirming round inherits the gating of whatever ended the rounds, autonomous only where autonomy still stands, since `blocking`, the round cap and the all-Minor signal each suspend it by their own terminator. Four sentences in that section rename the spec's pair-offer arm from "confirming round" to "a full-document round" — the enumeration is `workflow.md:416`, `:419`, `:421` and - `:424` — so the term names one object. + `:424`, a snapshot of the file as it stands — so the term names one + object. The numbers move once W6 inserts its heading above them; the + checks anchor text rather than lines, so neither edit depends on the + other's order. `spec-plan-lifecycle.md`, Unfinished-work list, Unresolved verdict owner leg: the plan exception widens to any plan whose latest heading @@ -195,7 +212,16 @@ sentence, the Confirming round entry) and one owed (W5). 3. **W3 — sha beside hash** (decision 2). `spec-plan-lifecycle.md`, the per-round-commits paragraph: one sentence. 4. **W4 — co-firing line** (decision 3). `process-status/SKILL.md`, - Step 4, after the Misplaced stamp paragraph. + Step 4, after the Misplaced stamp paragraph — and written as a shape + rather than as a pair of class names: where two confirmed hits share + one document, the report says so on one line and points at the + classes' own owner legs. Naming the pair, or restating that they are + one debt seen from two sides, would put the relationship in a second + place and make the skill carry what it declares it does not — process + knowledge of its own, where it takes the owner from the entry and + never from its own knowledge. The shape also survives the next pair + of classes without an edit, which is the same property the Misplaced + stamp paragraph buys by keying on match semantics. 5. **W5 — glossary wrap seam.** `docs/domain/glossary.md`, Chain debt: the clause "recorded decline — of the gate's pair offer, or, where …, of the question …" loses its comma pile; the term's meaning does @@ -271,12 +297,12 @@ Each change is checked by grep against the shipped file, whitespace normalized so a wrapped phrase still matches; a check states both its before and after value. -- W1, `workflow.md`, whitespace-normalized: `grep -o 'never terminates on a diff-scoped LGTM' | wc -l` — 1 before, 0 after; `grep -o 'latest verdict round was full-document' | wc -l` — 0 before, 1 after; `grep -o 'inherits the gating of whatever ended' | wc -l` — 0 before, 1 after (round one's F1); one anchor per renamed sentence, each 1 before and 0 after — `grep -o 'or a confirming full-document round'` (`:416`), `grep -o 'confirming round alone'` (`:419`), `grep -o 'a confirming round on a spec'` (`:421`), `grep -o 'The confirming-round arm'` (`:424`). Round two anchored two of the four and round three the rest, which is why each now stands on its own sentence rather than on the count. +- W1, `workflow.md`, whitespace-normalized: `grep -o 'never terminates on a diff-scoped LGTM' | wc -l` — 1 before, 0 after; `grep -o 'latest verdict round was full-document' | wc -l` — 0 before, 1 after; `grep -o 'inherits the gating of whatever ended' | wc -l` — 0 before, 1 after (round one's F1); `grep -o 'at the document.s next touch' | wc -l` — 1 before and after, a declared invariant: round four's F1 keeps that phrase for the `LGTM` heading and adds the other verdicts' trigger beside it rather than replacing it; one anchor per renamed sentence, each 1 before and 0 after — `grep -o 'or a confirming full-document round'` (`:416`), `grep -o 'confirming round alone'` (`:419`), `grep -o 'a confirming round on a spec'` (`:421`), `grep -o 'The confirming-round arm'` (`:424`). Round two anchored two of the four and round three the rest, which is why each now stands on its own sentence rather than on the count. - W1, `spec-plan-lifecycle.md`: `grep -c 'whose latest round heading is a$' ` — the owner leg's line — 1 before, 0 after. Whitespace-normalized: `grep -o 'highest ordinal' | wc -l` — 0 before, 1 after (decision 4); `grep -o 'continue across loops' | wc -l` — 0 before, 1 after (round one's F2); `grep -o 'where that heading is an .LGTM.' | wc -l` — 0 before, 1 after (round one's F3); `grep -o 'wait for the confirming round' | wc -l` — 0 before, 1 after (decision 5). - W1, glossary: `grep -c '^\*\*Confirming round\*\*:' docs/domain/glossary.md` — 1 before and after, a declared invariant, the entry having landed at grilling; `tr -s '[:space:]' ' ' < docs/domain/glossary.md | grep -o 'inherits the gating' | wc -l` — 1 before and after, and `grep -o 'A plan already .implemented. owes none' | wc -l` over the same stream — 1 before and after: both declared invariants, since rounds one and two wrote those clauses into the entry rather than leaving them to the prescribed rule edits. - W2: over the whitespace-normalized `workflow.md`, `grep -o 'unbounded by the cap' | wc -l` — 0 before, 1 after; `grep -o 'records their close, and no session' | wc -l` — 1 before, 0 after, paired with `grep -o 'on a plan, once the confirming round has run' | wc -l` — 0 before, 1 after: the removal and the arrival, since a removal alone proves only that the sentence moved (round three's F4). - W3: `tr -s '[:space:]' ' ' < plugins/working-process/rules/spec-plan-lifecycle.md | grep -o 'never stands in for the' | wc -l` — 0 before, 1 after. -- W4: `grep -c 'Unresolved verdict and Chain debt' plugins/working-process/skills/process-status/SKILL.md` — 0 before, 1 after. +- W4: `tr -s '[:space:]' ' ' < plugins/working-process/skills/process-status/SKILL.md | grep -o 'two confirmed hits share one document' | wc -l` — 0 before, 1 after; `grep -c 'Unresolved verdict and Chain debt' plugins/working-process/skills/process-status/SKILL.md` — 0 before and after, a declared invariant: round four's F2 replaced the class pair with the shape, so the skill must never name them. - W5: `grep -c 'decline — of the gate' docs/domain/glossary.md` — 1 before, 0 after. - W6: `grep -c '^## Branch naming' plugins/working-process/rules/workflow.md` — 0 before, 1 after; the same command against `ticket-frontmatter.md` — 0 before and after, a declared invariant, since round one moved the section out of that rule; `tr -s '[:space:]' ' ' < plugins/working-process/rules/ticket-frontmatter.md | grep -o 'branch naming convention' | wc -l` — 0 before, 1 after (the pointer that replaces it). - W7: `ls plugins/working-process/rules/*.md | wc -l` — 5 before, 6 after; `grep -c 'docs/domain/\*\*' plugins/working-process/rules/propagation-duties.md` — no file before, 1 after; `grep -c 'six rule files' plugins/working-process/README.md` — 0 before, 1 after; `claude plugin validate plugins/working-process` passes after. @@ -332,3 +358,17 @@ included. - fixed 2026-09-11 — [Minor] W2's check proved only the removal of the all-Minor bullet's close sentence and never named the clause replacing it, where every W1 check pairs a removal with an arrival; license: those checks' own shape; W2 now names the qualifier and the check anchors it 0 to 1 - hit fixed 2026-09-11 — reordering the ledger at the previous commit moved only the first line of round one's multi-line `signal`, leaving its three continuation lines under round two's signal; the reviewer reported it as an integrity note rather than a finding, and the lines are back under their own - signal 2026-09-11 — a fourth round would not repay: what remains after this wave is a clause, a placement phrase and two anchors, none of which needs design judgment. The residue class — partial anchors and drifted ledger lines — is what an integrity audit proves with two quotes, so the chain debt is better discharged at the consumption gate by the audit arm than by another round + +### 2026-09-11 — architect, fable 5.1, blocking (round 4, full-document) + +Dispatched full-document against the reviewer's round-three stop signal, +on the developer's order, and the brief said so rather than letting the +override read as an oversight. The gate returned `CLEAN` beforehand. +Every citation checked, including the two that prove F1 — the loop +rule's own prescription for a plan mid-loop. + +- fixed 2026-09-11 — [Important] W1's recovery sentence generalized to "latest round heading is diff-scoped, whatever its verdict", which makes a plan merely mid-loop between two diff-scoped rounds indistinguishable from one owing a confirming round — and for that state the loop rule prescribes something else, "Held set empty — dispatch the next round without asking, within the cap", every later round diff-scoped; the `LGTM` case was sound because that heading is itself an end; license: decision 1's own ruling, which keys on what ends a plan's rounds rather than on the heading alone; the sentence now carries that trigger and keeps "next touch" for `LGTM` +- fixed 2026-09-11 — [Important] W4 wrote the co-firing line as a pair of class names restating that they are one debt seen from two sides, putting the relationship in a second place and making the skill carry process knowledge it declares it has none of — it takes the owner from the entry, never from its own knowledge, and a later erratum changing the owner leg would have to edit the skill too; license: decision 3's own ruling, that the line is derived from the mapping without judging it, which "one debt" does not satisfy; W4 now publishes the shape — two confirmed hits sharing one document — and the check anchors that, with the class pair a declared invariant at zero +- fixed 2026-09-11 — [Minor] the Problem section attributed all seven items to the two outside cycles and pointed at two memory entries as the record, while those entries carry only W1, W2 and W7; W3, W4, W5 and W6 are this repo's own; license: those entries' contents; the section now splits three from four and names which is which +- fixed 2026-09-11 — the line enumeration W1 publishes is a snapshot that W6's heading will shift, and nothing said whether the two edits are ordered; ruling: 2026-09-11; the item now says the numbers are a snapshot and that the checks anchor text, so neither edit depends on the other's order +- signal 2026-09-11 — a fifth full round would not repay, and this is the second round to say so: both remaining repairs were clauses needing no whole-document context, and the residue class — provenance, a snapshot of line numbers — is what an integrity audit proves with two quotes more cheaply than a round. The reviewer noted that `blocking` suspends autonomy, so whether a short diff-scoped round or the audit follows is the developer's call From 2bb8fc75ad014ceca3de07b944c16a99043afe2f Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 21:46:45 +0200 Subject: [PATCH 051/120] docs: dispose the integrity audit of the errata wave-four spec --- ...2026-09-11-review-loop-errata-wave-four.md | 91 ++++++++++++++----- 1 file changed, 67 insertions(+), 24 deletions(-) diff --git a/docs/specs/2026-09-11-review-loop-errata-wave-four.md b/docs/specs/2026-09-11-review-loop-errata-wave-four.md index 5fdb54c..310866e 100644 --- a/docs/specs/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/specs/2026-09-11-review-loop-errata-wave-four.md @@ -14,8 +14,9 @@ base: develop ## Problem Two outside cycles ran working-process 0.13.0 through 0.16.0 on real -client work and groomed their findings against each release. Seven -items survived 0.16.0. Three of them were decisions the developer had +client work and groomed their findings against each release. Three +items survived 0.16.0 from them, and this wave carries four more of this +repo's own, seven in all. Three of the seven were decisions the developer had not yet taken; four were sentences nobody had written; grilling found two more decisions the rules had left implicit. Six are errata: they remove a contradiction or write down what the rules already implied. @@ -64,16 +65,24 @@ where a ledger later cites them. committed, which is the default path. Something present only sometimes cannot replace something that must be present always. One sentence records it so a later session does not "tidy" the hash - away. `ruling: 2026-09-11`. + away. Alternative declined: leaving it an open question until + per-round commits stop being opt-in, which costs nothing now and + re-derives the same argument later. The third option — a sha in + place of the hash — was refused on construction rather than on + preference: the hash must work on the default path, where no sha + exists. `ruling: 2026-09-11`. 3. **`process-status` reports the plan co-firing in one line derived from its mapping, without judging it.** The rule already calls the Unresolved verdict / Chain debt duplicate on a plan deliberate (`spec-plan-lifecycle.md`, the Unresolved verdict owner leg). The - report adds one line where both classes hit the same plan — one debt - seen from two sides, both settled through the confirming round, see - that owner leg — derived from two hits sharing a file, in the shape - the skill already uses when Misplaced stamp silences a neighbour. It does not say "deliberate": that is process - knowledge, and the skill's contract is to carry none. Alternative + report says so on one line where two confirmed hits share one + document, and points at the classes' own owner legs — derived from + the mapping, two hits in one file, in the shape the skill already + uses when Misplaced stamp silences a neighbour. It neither calls the + duplicate deliberate nor restates that it is one debt seen from two + sides: both are process knowledge, and the skill's contract is to + carry none. Round four sharpened this from a named pair of classes + to the shape, for the reason the ruling already gave. Alternative declined: silence, which leaves the explanation where the confused reader never looks. `ruling: 2026-09-11`. @@ -138,8 +147,10 @@ one — turned out to be answered by the text that shipped: ## Scope Seven changes in `plugins/working-process/`, and three in -`docs/domain/glossary.md` — two applied at grilling (the Round heading -sentence, the Confirming round entry) and one owed (W5). +`docs/domain/glossary.md` — two already applied and one owed (W5). The +Round heading sentence landed at grilling; the **Confirming round** +entry was minted there and amended by rounds one and two, so what stands +in the file is the wave's own work, not a prescription waiting on it. 1. **W1 — plan close needs a full-document round** (decision 1). `workflow.md`, *What a diff-scoped LGTM certifies*: the plan @@ -155,11 +166,15 @@ sentence, the Confirming round entry) and one owed (W5). plan merely mid-loop between two diff-scoped rounds, where the loop rule prescribes a different next round: "Held set empty — dispatch the next round without asking, within the cap", and every later round - diff-scoped. The same paragraph gains the gating sentence: - the confirming round inherits the gating of whatever ended the - rounds, autonomous only where autonomy still stands, since - `blocking`, the round cap and the all-Minor signal each suspend it by - their own terminator. Four sentences in that section rename the + diff-scoped. The same paragraph's sentence "That round runs + under the loop's standing consent like any other" is replaced rather + than joined, since the two would contradict: the confirming round + inherits the gating of whatever ended the rounds, autonomous only + where autonomy still stands, because `blocking`, the round cap and + the all-Minor signal each suspend it by their own terminator. What + the replaced sentence also carried — that the round spends none of + the round's one interruption — survives for the autonomous case and + is stated of it. Four sentences in that section rename the spec's pair-offer arm from "confirming round" to "a full-document round" — the enumeration is `workflow.md:416`, `:419`, `:421` and `:424`, a snapshot of the file as it stands — so the term names one @@ -273,9 +288,11 @@ sentence, the Confirming round entry) and one owed (W5). Unresolved verdict command until the round runs; the developer's only lever is to order the round or leave the plan open. An annotation written before the round would be a rule violation, and a violated - plan is invisible to the list the way any violation is — the recovery - clause in `workflow.md`, re-offer at the document's next touch, is - what catches it. A state-aware lint reading headings by ordinal is + plan is invisible to the list the way any violation is. What catches + it is the recovery clause as W1 rewrites it: an annotation is one of + the events that try to end a plan's rounds, so writing one re-offers + the confirming round rather than escaping it. The "next touch" reading + belongs to the `LGTM` heading alone. A state-aware lint reading headings by ordinal is where a mechanical check belongs. A plan already `implemented` is not this case: W1 gives it the recorded decline. - **The Chain debt term keeps its meaning.** It names what a diff-scoped @@ -293,19 +310,22 @@ sentence, the Confirming round entry) and one owed (W5). ## Verification -Each change is checked by grep against the shipped file, whitespace -normalized so a wrapped phrase still matches; a check states both its -before and after value. +Each change is checked by grep against the shipped file. Every check +over prose normalizes whitespace first, so a phrase still matches +wherever the line wraps; only a check anchoring something that cannot +wrap — a heading, a filename, a file count — is written plain, and none +anchors a line ending. A check states both its before and after value, +except where it declares itself an invariant and says why. -- W1, `workflow.md`, whitespace-normalized: `grep -o 'never terminates on a diff-scoped LGTM' | wc -l` — 1 before, 0 after; `grep -o 'latest verdict round was full-document' | wc -l` — 0 before, 1 after; `grep -o 'inherits the gating of whatever ended' | wc -l` — 0 before, 1 after (round one's F1); `grep -o 'at the document.s next touch' | wc -l` — 1 before and after, a declared invariant: round four's F1 keeps that phrase for the `LGTM` heading and adds the other verdicts' trigger beside it rather than replacing it; one anchor per renamed sentence, each 1 before and 0 after — `grep -o 'or a confirming full-document round'` (`:416`), `grep -o 'confirming round alone'` (`:419`), `grep -o 'a confirming round on a spec'` (`:421`), `grep -o 'The confirming-round arm'` (`:424`). Round two anchored two of the four and round three the rest, which is why each now stands on its own sentence rather than on the count. -- W1, `spec-plan-lifecycle.md`: `grep -c 'whose latest round heading is a$' ` — the owner leg's line — 1 before, 0 after. Whitespace-normalized: `grep -o 'highest ordinal' | wc -l` — 0 before, 1 after (decision 4); `grep -o 'continue across loops' | wc -l` — 0 before, 1 after (round one's F2); `grep -o 'where that heading is an .LGTM.' | wc -l` — 0 before, 1 after (round one's F3); `grep -o 'wait for the confirming round' | wc -l` — 0 before, 1 after (decision 5). +- W1, `workflow.md`, whitespace-normalized: `grep -o 'never terminates on a diff-scoped LGTM' | wc -l` — 1 before, 0 after; `grep -o 'latest verdict round was full-document' | wc -l` — 0 before, 1 after; `grep -o 'inherits the gating of whatever ended' | wc -l` — 0 before, 1 after, paired with `grep -o 'runs under the loop.s standing consent like any other' | wc -l` — 1 before, 0 after, the sentence it replaces (round one's F1, the pairing added by the integrity audit); `grep -o 'at the document.s next touch' | wc -l` — 1 before and after, a declared invariant: round four's F1 keeps that phrase for the `LGTM` heading and adds the other verdicts' trigger beside it rather than replacing it; one anchor per renamed sentence, each 1 before and 0 after — `grep -o 'or a confirming full-document round'` (`:416`), `grep -o 'confirming round alone'` (`:419`), `grep -o 'a confirming round on a spec'` (`:421`), `grep -o 'The confirming-round arm'` (`:424`). Round two anchored two of the four and round three the rest, which is why each now stands on its own sentence rather than on the count. +- W1, `spec-plan-lifecycle.md`: `grep -o 'except on a plan whose latest round heading is a diff-scoped' | wc -l` over the whitespace-normalized rule — the owner leg's antecedent — 1 before, 0 after. Whitespace-normalized: `grep -o 'highest ordinal' | wc -l` — 0 before, 1 after (decision 4); `grep -o 'continue across loops' | wc -l` — 0 before, 1 after (round one's F2); `grep -o 'where that heading is an .LGTM.' | wc -l` — 0 before, 1 after (round one's F3); `grep -o 'wait for the confirming round' | wc -l` — 0 before, 1 after (decision 5). - W1, glossary: `grep -c '^\*\*Confirming round\*\*:' docs/domain/glossary.md` — 1 before and after, a declared invariant, the entry having landed at grilling; `tr -s '[:space:]' ' ' < docs/domain/glossary.md | grep -o 'inherits the gating' | wc -l` — 1 before and after, and `grep -o 'A plan already .implemented. owes none' | wc -l` over the same stream — 1 before and after: both declared invariants, since rounds one and two wrote those clauses into the entry rather than leaving them to the prescribed rule edits. - W2: over the whitespace-normalized `workflow.md`, `grep -o 'unbounded by the cap' | wc -l` — 0 before, 1 after; `grep -o 'records their close, and no session' | wc -l` — 1 before, 0 after, paired with `grep -o 'on a plan, once the confirming round has run' | wc -l` — 0 before, 1 after: the removal and the arrival, since a removal alone proves only that the sentence moved (round three's F4). - W3: `tr -s '[:space:]' ' ' < plugins/working-process/rules/spec-plan-lifecycle.md | grep -o 'never stands in for the' | wc -l` — 0 before, 1 after. - W4: `tr -s '[:space:]' ' ' < plugins/working-process/skills/process-status/SKILL.md | grep -o 'two confirmed hits share one document' | wc -l` — 0 before, 1 after; `grep -c 'Unresolved verdict and Chain debt' plugins/working-process/skills/process-status/SKILL.md` — 0 before and after, a declared invariant: round four's F2 replaced the class pair with the shape, so the skill must never name them. - W5: `grep -c 'decline — of the gate' docs/domain/glossary.md` — 1 before, 0 after. - W6: `grep -c '^## Branch naming' plugins/working-process/rules/workflow.md` — 0 before, 1 after; the same command against `ticket-frontmatter.md` — 0 before and after, a declared invariant, since round one moved the section out of that rule; `tr -s '[:space:]' ' ' < plugins/working-process/rules/ticket-frontmatter.md | grep -o 'branch naming convention' | wc -l` — 0 before, 1 after (the pointer that replaces it). -- W7: `ls plugins/working-process/rules/*.md | wc -l` — 5 before, 6 after; `grep -c 'docs/domain/\*\*' plugins/working-process/rules/propagation-duties.md` — no file before, 1 after; `grep -c 'six rule files' plugins/working-process/README.md` — 0 before, 1 after; `claude plugin validate plugins/working-process` passes after. +- W7: `ls plugins/working-process/rules/*.md | wc -l` — 5 before, 6 after; `grep -c 'docs/domain/\*\*' plugins/working-process/rules/propagation-duties.md` — no file before, 1 after; `grep -c 'six rule files' plugins/working-process/README.md` — 0 before, 1 after; `claude plugin validate plugins/working-process` passes before and after — a declared invariant, since a wave that only adds a rule file must not be the thing that breaks the manifest. ## Review rounds @@ -372,3 +392,26 @@ rule's own prescription for a plan mid-loop. - fixed 2026-09-11 — [Minor] the Problem section attributed all seven items to the two outside cycles and pointed at two memory entries as the record, while those entries carry only W1, W2 and W7; W3, W4, W5 and W6 are this repo's own; license: those entries' contents; the section now splits three from four and names which is which - fixed 2026-09-11 — the line enumeration W1 publishes is a snapshot that W6's heading will shift, and nothing said whether the two edits are ordered; ruling: 2026-09-11; the item now says the numbers are a snapshot and that the checks anchor text, so neither edit depends on the other's order - signal 2026-09-11 — a fifth full round would not repay, and this is the second round to say so: both remaining repairs were clauses needing no whole-document context, and the residue class — provenance, a snapshot of line numbers — is what an integrity audit proves with two quotes more cheaply than a round. The reviewer noted that `blocking` suspends autonomy, so whether a short diff-scoped round or the audit follows is the developer's call + +### 2026-09-11 — integrity audit, fable, at the consumption gate + +Dispatched on a fresh context before plan-writing, on the developer's +call and on the reviewer's twice-given signal that the residue belonged +here rather than to a fifth round. Coverage 374 of 374 lines. It returned +eight defects, each proved by two quotes, and twelve ranked implementer +questions; the defects are disposed below and the questions are the +plan's input. Not a verdict: the audit grades nothing and stamps +nothing, so `architect: blocking` stands until it is closed. + +Seven of the eight were one shape — a claim repaired in one place and +left stale in its neighbour — which is the shape four review rounds +passed over and this read caught in one pass. + +- fixed 2026-09-11 — W1 added the gating sentence beside `workflow.md`'s "That round runs under the loop's standing consent like any other" without saying the shipped sentence goes, so the rule would carry both, and the check anchored only the arrival — against this document's own rule that a W1 check pairs a removal with an arrival; license: that rule, stated in the Verification section; W1 now says the sentence is replaced and keeps what it also carried, and the check carries the removal anchor +- fixed 2026-09-11 — the exclusion on the missing decline path said "next touch" catches an annotation written early, while round four had just reserved "next touch" for the `LGTM` heading; license: W1's own rewritten trigger, in which an annotation is one of the events that end a plan's rounds; the bullet now says the annotation is itself the trigger +- fixed 2026-09-11 — decision 3 still described the report line as a named pair of classes that are "one debt seen from two sides", the exact words round four's F2 removed from W4; license: decision 3's own ruling, that the line is derived from the mapping without judging it; the decision now states the shape and records that round four sharpened it +- fixed 2026-09-11 — the Problem section's first paragraph said seven items survived 0.16.0 from the two outside cycles while its second paragraph, rewritten in round four, said three; license: the two memory entries, which carry three; the first paragraph now splits the same way +- fixed 2026-09-11 — the decisions preamble said each was put to the developer with its options and cost, and decision 2 alone recorded no declined alternative; license: that preamble; decision 2 now records the option left on the table and why the third was refused on construction rather than preference +- fixed 2026-09-11 — the Scope preamble called both landed glossary changes "applied at grilling" while the **Confirming round** entry was minted there and amended by rounds one and two; license: those rounds' own `fixed` lines; the preamble now says what landed when +- fixed 2026-09-11 — W7's `claude plugin validate` check stated only an after value where the section's preamble requires both; license: that preamble; the check is a declared invariant, passing before and after, on the ground that adding a rule file must not be what breaks the manifest +- fixed 2026-09-11 — the Verification preamble claimed every check normalizes whitespace while four were plain `grep -c`, one of them anchoring a line ending with `$` — which would pass or fail on where an editor wrapped the new sentence, the precise dependency the claim denied; license: the preamble's own claim; the preamble now says which checks are plain and why, and the line-ending anchor is replaced by a normalized phrase From 4b6c650e39ee41c5f38cb72a7eb0de055c401c93 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 21:47:55 +0200 Subject: [PATCH 052/120] docs: answer four implementer questions and adjudicate the errata wave-four spec --- ...2026-09-11-review-loop-errata-wave-four.md | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/specs/2026-09-11-review-loop-errata-wave-four.md b/docs/specs/2026-09-11-review-loop-errata-wave-four.md index 310866e..e37bf6a 100644 --- a/docs/specs/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/specs/2026-09-11-review-loop-errata-wave-four.md @@ -1,10 +1,10 @@ --- ticket: none date: 2026-09-11 -status: draft +status: approved grilled: 2026-09-11 -architect: blocking -revises: [./2026-08-17-autonomous-review-loop-design.md, ./2026-09-07-diff-scoped-chain-debt-design.md, ./2026-08-27-audit-agents-design.md] +architect: blocking (adjudicated 2026-09-11) +revises: [./2026-08-17-autonomous-review-loop-design.md, ./2026-09-07-diff-scoped-chain-debt-design.md] branch: feature/process-wave-four base: develop --- @@ -166,7 +166,15 @@ in the file is the wave's own work, not a prescription waiting on it. plan merely mid-loop between two diff-scoped rounds, where the loop rule prescribes a different next round: "Held set empty — dispatch the next round without asking, within the cap", and every later round - diff-scoped. The same paragraph's sentence "That round runs + diff-scoped. The paragraph also answers what a dispatching session + needs and the recovery sentence alone does not: a session never + judges that the rounds are ending. The terminators do — an `LGTM`, a + `blocking`, the cap, the all-Minor signal — as does the developer + closing the loop. While none has fired and the held set is empty, the + next round is diff-scoped and the loop continues; the confirming + round is owed the moment one fires, which is the moment a session + would otherwise write the annotation. Leaving that to inference is + what let the recovery sentence drift. The same paragraph's sentence "That round runs under the loop's standing consent like any other" is replaced rather than joined, since the two would contradict: the confirming round inherits the gating of whatever ended the rounds, autonomous only @@ -200,7 +208,9 @@ in the file is the wave's own work, not a prescription waiting on it. The same rule's *The disposition ledger* section gains two sentences: the latest round heading is the one carrying the highest ordinal wherever it sits, and ordinals run per document and per field and - continue across loops (decision 4). The rule's resolution-annotation + continue across loops (decision 4). The glossary's **Round heading** + entry gains the continuity half too, so the term reads without the + rule beside it. The rule's resolution-annotation bullet — "Concerns resolved without a fresh review round keep the verdict and gain a resolution date" — gains the plan clause of decision 5: on a plan the annotation and the adjudication are written @@ -390,7 +400,7 @@ rule's own prescription for a plan mid-loop. - fixed 2026-09-11 — [Important] W1's recovery sentence generalized to "latest round heading is diff-scoped, whatever its verdict", which makes a plan merely mid-loop between two diff-scoped rounds indistinguishable from one owing a confirming round — and for that state the loop rule prescribes something else, "Held set empty — dispatch the next round without asking, within the cap", every later round diff-scoped; the `LGTM` case was sound because that heading is itself an end; license: decision 1's own ruling, which keys on what ends a plan's rounds rather than on the heading alone; the sentence now carries that trigger and keeps "next touch" for `LGTM` - fixed 2026-09-11 — [Important] W4 wrote the co-firing line as a pair of class names restating that they are one debt seen from two sides, putting the relationship in a second place and making the skill carry process knowledge it declares it has none of — it takes the owner from the entry, never from its own knowledge, and a later erratum changing the owner leg would have to edit the skill too; license: decision 3's own ruling, that the line is derived from the mapping without judging it, which "one debt" does not satisfy; W4 now publishes the shape — two confirmed hits sharing one document — and the check anchors that, with the class pair a declared invariant at zero - fixed 2026-09-11 — [Minor] the Problem section attributed all seven items to the two outside cycles and pointed at two memory entries as the record, while those entries carry only W1, W2 and W7; W3, W4, W5 and W6 are this repo's own; license: those entries' contents; the section now splits three from four and names which is which -- fixed 2026-09-11 — the line enumeration W1 publishes is a snapshot that W6's heading will shift, and nothing said whether the two edits are ordered; ruling: 2026-09-11; the item now says the numbers are a snapshot and that the checks anchor text, so neither edit depends on the other's order +- fixed 2026-09-11 — the line enumeration W1 publishes is a snapshot that W6's heading will shift, and nothing said whether the two edits are ordered — the reviewer raised it as a line for the integrity class rather than as a graded finding, which is why this line carries no severity; ruling: 2026-09-11; the item now says the numbers are a snapshot and that the checks anchor text, so neither edit depends on the other's order - signal 2026-09-11 — a fifth full round would not repay, and this is the second round to say so: both remaining repairs were clauses needing no whole-document context, and the residue class — provenance, a snapshot of line numbers — is what an integrity audit proves with two quotes more cheaply than a round. The reviewer noted that `blocking` suspends autonomy, so whether a short diff-scoped round or the audit follows is the developer's call ### 2026-09-11 — integrity audit, fable, at the consumption gate @@ -414,4 +424,5 @@ passed over and this read caught in one pass. - fixed 2026-09-11 — the decisions preamble said each was put to the developer with its options and cost, and decision 2 alone recorded no declined alternative; license: that preamble; decision 2 now records the option left on the table and why the third was refused on construction rather than preference - fixed 2026-09-11 — the Scope preamble called both landed glossary changes "applied at grilling" while the **Confirming round** entry was minted there and amended by rounds one and two; license: those rounds' own `fixed` lines; the preamble now says what landed when - fixed 2026-09-11 — W7's `claude plugin validate` check stated only an after value where the section's preamble requires both; license: that preamble; the check is a declared invariant, passing before and after, on the ground that adding a rule file must not be what breaks the manifest +- fixed 2026-09-11 — four of the twelve implementer questions were answerable from written decisions and are answered in the document rather than left to the plan: what event ends a plan's rounds for a dispatching session and not only for the re-offer (Q1, licensed by the terminators, which are what end them); the **Round heading** entry carrying decision 4's continuity clause so the term reads alone (Q9); this ledger's one severity-free line saying why it has none (Q10); and `revises:` dropping `2026-08-27-audit-agents-design.md`, since that design anticipated an author-facing mirror of the duties and W7 is that mirror rather than a departure from it (Q11, licensed by the lifecycle rule's definition of `revises:` as a claim that the named design no longer matches what shipped). The remaining eight are the plan's agenda: full replacement text for three rule passages, W7's rule body, W6's placeholder and renaming duty, W3's paragraph and sentence, W4's literal line shape, and W2's sentence placement - fixed 2026-09-11 — the Verification preamble claimed every check normalizes whitespace while four were plain `grep -c`, one of them anchoring a line ending with `$` — which would pass or fail on where an editor wrapped the new sentence, the precise dependency the claim denied; license: the preamble's own claim; the preamble now says which checks are plain and why, and the line-ending anchor is replaced by a normalized phrase From 4ac5151019a7801a523e0a7233dbc3ab3570a626 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 22:14:45 +0200 Subject: [PATCH 053/120] docs: implementation plan for errata wave four --- ...2026-09-11-review-loop-errata-wave-four.md | 1008 +++++++++++++++++ 1 file changed, 1008 insertions(+) create mode 100644 docs/plans/2026-09-11-review-loop-errata-wave-four.md diff --git a/docs/plans/2026-09-11-review-loop-errata-wave-four.md b/docs/plans/2026-09-11-review-loop-errata-wave-four.md new file mode 100644 index 0000000..70a5740 --- /dev/null +++ b/docs/plans/2026-09-11-review-loop-errata-wave-four.md @@ -0,0 +1,1008 @@ +--- +ticket: none +date: 2026-09-11 +status: draft +spec: ../specs/2026-09-11-review-loop-errata-wave-four.md +branch: feature/process-wave-four +base: develop +--- + +# Review loop — errata wave four Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> superpowers:subagent-driven-development (recommended) or +> superpowers:executing-plans to implement this plan task-by-task. Steps +> use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Land the seven changes of +`docs/specs/2026-09-11-review-loop-errata-wave-four.md` in the +`working-process` plugin and this repo's glossary. + +**Architecture:** Every deliverable is prose in a shipped rule, skill, +README or glossary entry. There is no code and no test suite; a task's +test is the grep pair the spec publishes, run before and after the edit. +Tasks are drawn one per file so no two touch the same text, except +`workflow.md`, whose two tasks edit sections that do not overlap. + +**Tech Stack:** Markdown rule files distributed as a Claude Code Rules +payload; `grep`, `tr` and `claude plugin validate` for verification. + +## Global Constraints + +- **The spec is the source.** Where this plan and the spec disagree, the + spec wins and the plan is wrong — except where *Deviations from the + spec* below records a departure and its reason. +- **Prose wraps at 72 characters** in every rule, skill and glossary + file. Match the surrounding paragraph; never reflow a paragraph this + plan does not change. +- **A check over prose normalizes whitespace first** + (`tr -s '[:space:]' ' '`), so a phrase matches wherever the line wraps. + Only a check anchoring something that cannot wrap — a heading, a + filename, a file count — is written plain, and none anchors a line + ending. +- **Every step states its before value and its after value**, and an + invariant says so and says why. +- **The glossary binds.** `docs/domain/glossary.md` terms and `_Avoid_` + bans govern this text. `Confirming round`, `Round heading`, `Chain + debt`, `Adjudication`, `Unfinished-work list` and `Disposition ledger` + are canonical here. +- **Public repo hygiene**: no machine-specific paths, no company or + client names, all committed text in English. +- **Commit messages are one line** — a conventional-commit subject, no + body, no trailers. +- **Do not run `sync-rules` and do not move the spec's `status`.** Both + are the developer's, and the spec's *What this wave does not do* says + so. + +## Deviations from the spec + +Recorded here and beside the text they concern, so a reviewer trips over +the reason where the disagreement lives. + +1. **W3's sentence lands in the section's last paragraph**, the one + opening "What per-round commits never do is replace the ledger". The + spec says only "the per-round-commits paragraph" and the integrity + audit reported the ambiguity (its question 6). That paragraph is the + section's statement of what the mechanism does not do, which is what + this sentence says. Task 3 carries the reason. +2. **W7's rule body writes the trigger list as a table**, which the spec + does not prescribe — it prescribes the keying ("keys the eight duties + by the edit an author just made") and the citation by duty number. A + table is the shortest form that keeps trigger, enumeration and duty + number on one row. Task 6 carries the reason. +3. **W7's prose never writes the literal `docs/domain/**`.** The spec's + own check expects exactly one line matching it, and the frontmatter + entry is that line; a second mention in prose would make the check + read 2 where the spec says 1. Task 6 says "the domain directory" + instead. + +## File structure + +Modified: + +- `plugins/working-process/rules/workflow.md` — Task 1 (the plan + paragraph and the spec's pair-offer arm, inside *What a diff-scoped + LGTM certifies*) and Task 2 (two Terminators bullets, plus a new + `## Branch naming` section). The two tasks share the file and touch no + common line. +- `plugins/working-process/rules/spec-plan-lifecycle.md` — Task 3: the + Unresolved verdict owner leg, two sentences in *The disposition + ledger*, the resolution-annotation bullet, and one sentence in the + per-round-commits section. +- `plugins/working-process/rules/ticket-frontmatter.md` — Task 2: one + pointer sentence in *Sourcing and backfill*. +- `docs/domain/glossary.md` — Task 4: the **Round heading** entry gains + a clause; the **Chain debt** entry loses a comma pile. +- `plugins/working-process/skills/process-status/SKILL.md` — Task 5: one + paragraph in Step 4. +- `plugins/working-process/README.md` — Task 6: the rule count and the + new rule's name. + +Created: + +- `plugins/working-process/rules/propagation-duties.md` — Task 6. + +## Order and independence + +The integrity audit confirmed the seven items may be applied in any +order, with three caveats this plan fixes by its task order: + +- Tasks 1 and 2 both edit `workflow.md`; Task 1 runs first so the line + numbers the spec quotes (`:416`, `:419`, `:421`, `:424`) still hold + when it reads them. Task 2 inserts a heading above those lines, which + is why it runs second. Every check anchors text rather than a line, so + the order is a convenience, not a dependency. +- Task 2's W2 sentence assumes decision 1's semantics, which Task 1 + lands. Running Task 1 first keeps the rule self-consistent at every + commit. +- `sync-rules` and `claude plugin validate` run once, at Task 7, after + every file has changed. Validation before then proves nothing about + the end state. + +--- + +### Task 1: `workflow.md` — a plan's loop closes on a full-document round + +**Files:** +- Modify: `plugins/working-process/rules/workflow.md` — the section + `### What a diff-scoped LGTM certifies`, its spec paragraph and its + plan paragraph. + +**Interfaces:** +- Produces: the phrases later tasks and checks depend on — `latest + verdict round was full-document`, `inherits the gating of whatever + ended`, and the preserved `at the document's next touch`. +- Consumes: nothing from earlier tasks. + +- [ ] **Step 1: Record the before values** + +Run from the repository root: + +```bash +W=plugins/working-process/rules/workflow.md +n() { tr -s '[:space:]' ' ' < "$1"; } +echo "A $(n $W | grep -o 'never terminates on a diff-scoped LGTM' | wc -l)" +echo "B $(n $W | grep -o 'latest verdict round was full-document' | wc -l)" +echo "C $(n $W | grep -o 'inherits the gating of whatever ended' | wc -l)" +echo "D $(n $W | grep -o 'runs under the loop.s standing consent like any other' | wc -l)" +echo "E $(n $W | grep -o 'at the document.s next touch' | wc -l)" +echo "F $(n $W | grep -o 'or a confirming full-document round' | wc -l)" +echo "G $(n $W | grep -o 'confirming round alone' | wc -l)" +echo "H $(n $W | grep -o 'a confirming round on a spec' | wc -l)" +echo "I $(n $W | grep -o 'The confirming-round arm' | wc -l)" +``` + +Expected: `A 1`, `B 0`, `C 0`, `D 1`, `E 1`, `F 1`, `G 1`, `H 1`, `I 1`. + +If any differs, stop and report: the file is not the one this task was +written against. + +- [ ] **Step 2: Replace the spec's pair-offer paragraph** + +Find this paragraph and replace it whole. It renames the spec's arm from +"confirming round" to "a full-document round" in all four of its +sentences, so the term **Confirming round** names one object — the +plan's obligation — and nothing else. + +Find: + +``` +For a spec, the consumption gate before plan-writing offers the pair as +one question — an integrity audit or a confirming full-document round — +and never an offer followed by a re-offer of the option just declined. +When the `integrity-auditor` agent is absent the offer carries the +confirming round alone. The two arms cost differently and the offer says +so: an audit returns material for the dispatcher to dispose of and +leaves the verdict alone, while a confirming round on a spec is a new +loop's first round, since the spec's LGTM already closed its loop — it +mints its own verdict and stamps it, so a `concerns` there flips the +field back while plan-writing waits. The confirming-round arm therefore +blocks plan-writing; the audit arm does not, and plan-writing follows +its dispositions. +``` + +Replace with: + +``` +For a spec, the consumption gate before plan-writing offers the pair as +one question — an integrity audit or a full-document round — and never +an offer followed by a re-offer of the option just declined. When the +`integrity-auditor` agent is absent the offer carries the full-document +round alone. The two arms cost differently and the offer says so: an +audit returns material for the dispatcher to dispose of and leaves the +verdict alone, while a full-document round on a spec is a new loop's +first round, since the spec's LGTM already closed its loop — it mints +its own verdict and stamps it, so a `concerns` there flips the field +back while plan-writing waits. The full-document-round arm therefore +blocks plan-writing; the audit arm does not, and plan-writing follows +its dispositions. +``` + +- [ ] **Step 3: Replace the plan paragraph** + +Find this paragraph and replace it with three. The first generalizes the +trigger and states the gating; the second says who decides that the +rounds are ending; the third keeps the relay-then-stamp exception and +splits recovery by verdict. + +Find: + +``` +For a plan the loop never terminates on a diff-scoped LGTM: one +full-document confirming round follows, and the confirming round's +verdict is the one stamped. That round runs under the loop's standing +consent like any other — the rules mandate it, so it is no decision of +the developer's and spends none of the round's one interruption. It +carries the single named exception to the relay-then-stamp order above — +a plan's diff-scoped LGTM is relayed and its round record written, and +only the frontmatter stamp waits for the confirming round. Recovery +therefore reads the ledger rather than the stamp: a plan whose latest +round heading is a diff-scoped LGTM that no later full-document round +follows is re-offered its confirming round at the document's next touch, +whatever the frontmatter says. +``` + +Replace with: + +``` +For a plan the loop closes only when the latest verdict round was +full-document: whatever ends its rounds, one confirming full-document +round follows, and that round's verdict is the one stamped. It inherits +the gating of whatever ended the rounds. Where autonomy still stands — +a diff-scoped `LGTM`, or a `concerns` inside the cap — the rules mandate +the round, so it is no decision of the developer's and spends none of +the round's one interruption. After `blocking`, the round cap or the +all-Minor signal it is the developer's to order, because each of those +suspends autonomy by its own terminator, and a round the rules mandate +cannot outrank a terminator that stopped the loop. + +A session never judges that the rounds are ending. The terminators do, +as does the developer closing the loop. While none has fired and the +held set is empty, the next round is diff-scoped and the loop simply +continues; the confirming round is owed the moment one fires, which is +the moment a session would otherwise write the resolution annotation. + +The plan case carries the single named exception to the relay-then-stamp +order above — a plan's diff-scoped LGTM is relayed and its round record +written, and only the frontmatter stamp waits for the confirming round. +Recovery therefore reads the ledger rather than the stamp. A plan whose +latest round heading is a diff-scoped `LGTM` that no later full-document +round follows is re-offered its confirming round at the document's next +touch, whatever the frontmatter says, since that heading is itself an +end. A latest heading of any other verdict is not an end, so there the +round is re-offered when something tries to end the rounds — an +annotation, an adjudication, a `status` move — or when a terminator that +suspends autonomy has fired. +``` + +- [ ] **Step 4: Verify** + +Run the Step 1 command again. + +Expected: `A 0`, `B 1`, `C 1`, `D 0`, `E 1`, `F 0`, `G 0`, `H 0`, `I 0`. + +`E` is the declared invariant: the phrase survives because the `LGTM` +heading keeps it. + +- [ ] **Step 5: Check the neighbours the rename must not touch** + +The words "confirming round" stay wherever they name the plan's +obligation. Run: + +```bash +grep -n 'confirming' plugins/working-process/rules/workflow.md +``` + +Expected: matches remain at the verdict-agent dispatch bullet ("waits +for the confirming round"), in the Round cap bullet (two), and in the +paragraphs Step 3 wrote. No match remains in the spec's pair-offer +paragraph. + +- [ ] **Step 6: Commit** + +```bash +git add plugins/working-process/rules/workflow.md +git commit -m "feat(working-process): close a plan's loop only on a full-document round" +``` + +--- + +### Task 2: `workflow.md` terminators and branch naming + +**Files:** +- Modify: `plugins/working-process/rules/workflow.md` — the `Round cap` + and `All-Minor signal` bullets of `### Terminators`, and a new + `## Branch naming` section. +- Modify: `plugins/working-process/rules/ticket-frontmatter.md` — one + sentence in `## Sourcing and backfill`. + +**Interfaces:** +- Consumes: Task 1's plan paragraph, whose semantics the all-Minor + qualifier assumes. +- Produces: the `## Branch naming` heading that the ticket rule's + pointer names. + +- [ ] **Step 1: Record the before values** + +```bash +W=plugins/working-process/rules/workflow.md +T=plugins/working-process/rules/ticket-frontmatter.md +n() { tr -s '[:space:]' ' ' < "$1"; } +echo "A $(n $W | grep -o 'unbounded by the cap' | wc -l)" +echo "B $(n $W | grep -o 'records their close, and no session' | wc -l)" +echo "C $(n $W | grep -o 'on a plan, once the confirming round has run' | wc -l)" +echo "D $(grep -c '^## Branch naming' $W)" +echo "E $(grep -c '^## Branch naming' $T)" +echo "F $(n $T | grep -o 'branch naming convention' | wc -l)" +``` + +Expected: `A 0`, `B 1`, `C 0`, `D 0`, `E 0`, `F 0`. + +- [ ] **Step 2: Extend the Round cap bullet** + +Find: + +``` + therefore escalates once before its confirming round, which is the + most expensive shape a round takes and the one a cap guarding spend + should guard first. +``` + +Replace with: + +``` + therefore escalates once before its confirming round, which is the + most expensive shape a round takes and the one a cap guarding spend + should guard first. A run of `blocking` verdicts is unbounded by the + cap, for the reason the cap is stated in: it counts autonomous rounds, + and `blocking` suspends autonomy, so every continuation after one is + the developer's own decision rather than a round the cap governs. + Consenting to three rounds consents to three autonomous ones. +``` + +- [ ] **Step 3: Qualify the all-Minor bullet** + +In the `All-Minor signal` bullet, find: + +``` + until the developer answers it: the resolution annotation the + spec-plan-lifecycle rule defines records their close, and no session + writes it without their answer. +``` + +Replace with: + +``` + until the developer answers it: the resolution annotation the + spec-plan-lifecycle rule defines records their close — on a plan, once + the confirming round has run — and no session writes it without their + answer. +``` + +- [ ] **Step 4: Add the Branch naming section** + +The words `## Dispatching a verdict agent` occur twice in this file — +once as the heading, and once inside backticks in the closing paragraph +of the section it names. Anchor on the pair below, which is unique. + +Find: + +``` +The pass binds wording, never decisions. Without the skill there is no +substitute pass and no install nagging — the work proceeds normally. + +## Dispatching a verdict agent +``` + +Replace with: + +``` +The pass binds wording, never decisions. Without the skill there is no +substitute pass and no install nagging — the work proceeds normally. + +## Branch naming + +Feature work happens on a topic branch named +`feature/-`, and `feature/` where the +ticket is `none`. The ticket is the reference the ticket-frontmatter +rule defines, written without the punctuation a ref cannot carry: a +Jira key as it stands (`feature/ABC-123-short-name`), a GitHub or GitLab +issue as its number alone (`feature/123-short-name`), since a leading +`#` and the `/` of an `org/repo#123` form would both change what the ref +means. + +A worktree created with a generated name is renamed to this shape before +its first commit, so the branch a reader sees is the branch the +convention names. The work's spec and plan record the result in their +`branch:` field, and the ticket rule reads the ticket back out of it. + +## Dispatching a verdict agent +``` + +- [ ] **Step 5: Point the ticket rule at it** + +In `ticket-frontmatter.md`, `## Sourcing and backfill`, find: + +``` +For a NEW document: branch name (`feature/ABC-123-...`) → conversation +context → ask the developer once; no answer means `none`. Never ask twice +for one unit of work — a plan inherits its spec's ticket, and artifacts +of the same session reuse the established value. +``` + +Replace with: + +``` +For a NEW document: branch name (`feature/ABC-123-...`) → conversation +context → ask the developer once; no answer means `none`. Never ask twice +for one unit of work — a plan inherits its spec's ticket, and artifacts +of the same session reuse the established value. The branch naming +convention that produces the names this order reads is the workflow +rule's, which is where a branch is cut — before any `docs/` file of that +work exists, and so before this rule loads. +``` + +- [ ] **Step 6: Verify** + +Run the Step 1 command again. + +Expected: `A 1`, `B 0`, `C 1`, `D 1`, `E 0`, `F 1`. + +`E` is the declared invariant: the section belongs to the workflow rule +and must never appear in the ticket rule. + +- [ ] **Step 7: Commit** + +```bash +git add plugins/working-process/rules/workflow.md plugins/working-process/rules/ticket-frontmatter.md +git commit -m "feat(working-process): bound the cap's promise and publish the branch-naming convention" +``` + +--- + +### Task 3: `spec-plan-lifecycle.md` — owner leg, ordinals, annotation, sha + +**Files:** +- Modify: `plugins/working-process/rules/spec-plan-lifecycle.md` — the + Unresolved verdict owner leg in `## Unfinished-work list`; two + sentences in `## The disposition ledger`; the resolution-annotation + bullet near the top; one sentence in the per-round-commits section. + +**Interfaces:** +- Consumes: Task 1's semantics — the owner leg and the annotation clause + both state what Task 1's paragraph decided. +- Produces: nothing later tasks read. + +- [ ] **Step 1: Record the before values** + +```bash +L=plugins/working-process/rules/spec-plan-lifecycle.md +n() { tr -s '[:space:]' ' ' < "$1"; } +echo "A $(n $L | grep -o 'except on a plan whose latest round heading is a diff-scoped' | wc -l)" +echo "B $(n $L | grep -o 'highest ordinal' | wc -l)" +echo "C $(n $L | grep -o 'continue across loops' | wc -l)" +echo "D $(n $L | grep -o 'where that heading is an .LGTM.' | wc -l)" +echo "E $(n $L | grep -o 'wait for the confirming round' | wc -l)" +echo "F $(n $L | grep -o 'never stands in for the' | wc -l)" +``` + +Expected: `A 1`, `B 0`, `C 0`, `D 0`, `E 0`, `F 0`. + +- [ ] **Step 2: Widen the Unresolved verdict owner leg** + +Find: + +``` + Owner: a fresh round at the prescribed tier, or the resolution + annotation above — except on a plan whose latest round heading is a + diff-scoped `LGTM`, where the confirming full-document round closes + the verdict and the annotation may not, since a plan's loop never + terminates on that heading. Such a plan matches Chain debt as well: + one debt seen from two sides, both extinguished by that round, and the + duplicate is deliberate. +``` + +Replace with: + +``` + Owner: a fresh round at the prescribed tier, or the resolution + annotation above — except on a plan whose latest round heading is + diff-scoped, whatever its verdict, where the confirming full-document + round closes the verdict and the annotation may not, since a plan's + loop closes only on a full-document round. Such a plan matches Chain + debt as well where that heading is an `LGTM`: one debt seen from two + sides, both extinguished by that round, and the duplicate is + deliberate. On a plan already at `status: implemented` the round is + discharged by recorded decline without any dispatch, as the Chain debt + leg discharges its own debt — a session derives that decline under a + `concerns` heading, while a `blocking` one waits for the developer's + adjudication, which is theirs to make. +``` + +The narrowing to `LGTM` rides with the widening because the Chain debt +command keys on `LGTM`: a diff-scoped `concerns` heading produces no +second hit, so the unnarrowed sentence would be false for exactly the +case this edit adds. + +- [ ] **Step 3: Define the latest heading and the ordinals** + +In `## The disposition ledger`, find this paragraph — the insertion goes +after it, so the replacement repeats it and adds the new one: + +``` +The scope token admits two values, `diff-scoped` and `full-document`, +and is omitted only on a round predating the distinction. A recovery +reading keys on the absence of `full-document`, so a full-document round +always says so. +``` + +Replace with: + +``` +The scope token admits two values, `diff-scoped` and `full-document`, +and is omitted only on a round predating the distinction. A recovery +reading keys on the absence of `full-document`, so a full-document round +always says so. + +The latest round heading is the one carrying the highest ordinal, +wherever it sits in the section: no order is prescribed for the blocks +and live ledgers run both ways. Ordinals run per document and per field, +and continue across loops — a later loop opens at the next number rather +than at one, which is what the round cap's derivation from the headings +already assumed. +``` + +- [ ] **Step 4: Give the annotation bullet its plan clause** + +Find: + +``` + its own token — `architect: blocking (adjudicated 2026-08-17)` — and + the round's ledger record as that body note. +``` + +Replace with: + +``` + its own token — `architect: blocking (adjudicated 2026-08-17)` — and + the round's ledger record as that body note. On a plan both wait for + the confirming round: neither is written while the latest round + heading is diff-scoped, unless the plan is already `implemented`, + where the annotation is written citing that standing decision — such a + document is amended in frontmatter, so the annotation is the only + lever left. +``` + +- [ ] **Step 5: Part the round sha from the integrity hash** + +In the per-round-commits section, find the final paragraph: + +``` +What per-round commits never do is replace the ledger. Git says which +lines changed; the ledger says with what intent and on whose license, +and a diff carries neither "narrowed the claim" nor a cited ADR. A +`` clause may thin to a sentence where the commit carries +the detail; it does not go. +``` + +Replace with: + +``` +What per-round commits never do is replace the ledger. Git says which +lines changed; the ledger says with what intent and on whose license, +and a diff carries neither "narrowed the claim" nor a cited ADR. A +`` clause may thin to a sentence where the commit carries +the detail; it does not go. Nor does a sha stand in for a hash: a round +heading that one day carried its commit's sha would name that round, +and it never stands in for the `integrity:` hash, which is computed from +the body and must work on a document nobody committed — the default +path, where no sha exists. +``` + +This paragraph is the section's statement of what the mechanism does not +do, which is what the sentence says; the spec named only "the +per-round-commits paragraph", and the integrity audit reported that +ambiguity (deviation 1). + +- [ ] **Step 6: Verify** + +Run the Step 1 command again. + +Expected: `A 0`, `B 1`, `C 1`, `D 1`, `E 1`, `F 1`. + +- [ ] **Step 7: Commit** + +```bash +git add plugins/working-process/rules/spec-plan-lifecycle.md +git commit -m "feat(working-process): widen the plan exception, define the latest heading, part sha from hash" +``` + +--- + +### Task 4: the glossary — ordinal continuity and the Chain debt seam + +**Files:** +- Modify: `docs/domain/glossary.md` — the **Round heading** entry and + the **Chain debt** entry. + +**Interfaces:** +- Consumes: Task 3's ordinal sentences, which this entry restates in the + term's own voice. +- Produces: nothing later tasks read. + +- [ ] **Step 1: Record the before values** + +```bash +G=docs/domain/glossary.md +n() { tr -s '[:space:]' ' ' < "$1"; } +echo "A $(grep -c '^\*\*Confirming round\*\*:' $G)" +echo "B $(n $G | grep -o 'inherits the gating' | wc -l)" +echo "C $(n $G | grep -o 'A plan already .implemented. owes none' | wc -l)" +echo "D $(grep -c 'decline — of the gate' $G)" +echo "E $(n $G | grep -o 'continue across loops' | wc -l)" +``` + +Expected: `A 1`, `B 1`, `C 1`, `D 1`, `E 0`. + +`A`, `B` and `C` are declared invariants: the **Confirming round** entry +was minted at the grilling and amended by review rounds one and two, so +it already stands in the file and this task must not disturb it. + +- [ ] **Step 2: Give Round heading the continuity clause** + +Find: + +``` +diff-scoped chain — is read by folding these. The latest round heading +is the one carrying the highest ordinal, wherever it sits in the +section: the rules prescribe no order for the blocks, and live ledgers +run both ways. +``` + +Replace with: + +``` +diff-scoped chain — is read by folding these. The latest round heading +is the one carrying the highest ordinal, wherever it sits in the +section: the rules prescribe no order for the blocks, and live ledgers +run both ways. Ordinals run per document and per field, and continue +across loops rather than restarting. +``` + +- [ ] **Step 3: Undo the Chain debt comma pile** + +Find: + +``` +once it terminates. Discharged three ways: an integrity audit, any later +full-document round whatever its verdict, or the developer's recorded +decline — of the gate's pair offer, or, where the document is already +implemented and the gate never fired, of the question that offer would +have put. +``` + +Replace with: + +``` +once it terminates. Discharged three ways: an integrity audit, any later +full-document round whatever its verdict, or the developer's recorded +decline. What they decline is the gate's pair offer — or, where the +document is already implemented and the gate never fired, the question +that offer would have put. +``` + +The term's meaning does not move; only the punctuation does. + +- [ ] **Step 4: Verify** + +Run the Step 1 command again. + +Expected: `A 1`, `B 1`, `C 1`, `D 0`, `E 1`. + +- [ ] **Step 5: Commit** + +```bash +git add docs/domain/glossary.md +git commit -m "docs(glossary): carry ordinal continuity and clear the Chain debt comma pile" +``` + +--- + +### Task 5: `process-status` reports two hits on one document + +**Files:** +- Modify: + `plugins/working-process/skills/process-status/SKILL.md` — one + paragraph in `## Step 4 — report`, after the Misplaced stamp + paragraph. + +**Interfaces:** +- Consumes: nothing from earlier tasks. +- Produces: nothing later tasks read. + +- [ ] **Step 1: Record the before values** + +```bash +S=plugins/working-process/skills/process-status/SKILL.md +echo "A $(tr -s '[:space:]' ' ' < $S | grep -o 'two confirmed hits share one document' | wc -l)" +echo "B $(grep -c 'Unresolved verdict and Chain debt' $S)" +``` + +Expected: `A 0`, `B 0`. + +`B` is a declared invariant at zero: the line is written as a shape, and +naming the pair of classes would give the relationship a second home and +make this skill carry process knowledge it declares it has none of. + +- [ ] **Step 2: Add the paragraph** + +Find the end of the Misplaced stamp paragraph — the two lines below are +unique in the file: + +``` +published later works the same way. Name the field on the line, so the +developer can see which class went quiet. +``` + +Replace with: + +``` +published later works the same way. Name the field on the line, so the +developer can see which class went quiet. + +Where two confirmed hits share one document, say so on one line and +point at the owner legs of the classes that produced them. The line is +the mapping read back — two hits, one file — and nothing more: it does +not say the duplicate is deliberate, and it does not explain how the two +relate, because both are process knowledge and this skill carries none. +A reader who wants the relationship finds it in the legs the line points +at. +``` + +- [ ] **Step 3: Verify** + +Run the Step 1 command again. + +Expected: `A 1`, `B 0`. + +- [ ] **Step 4: Commit** + +```bash +git add plugins/working-process/skills/process-status/SKILL.md +git commit -m "feat(working-process): report two hits on one document as a shape" +``` + +--- + +### Task 6: the propagation duties as an author-facing rule + +**Files:** +- Create: `plugins/working-process/rules/propagation-duties.md` +- Modify: `plugins/working-process/README.md` — the `## Process rules` + paragraph. + +**Interfaces:** +- Consumes: nothing from earlier tasks. +- Produces: a sixth file in `rules/`, which Task 7's file count reads. + +- [ ] **Step 1: Record the before values** + +```bash +echo "A $(ls plugins/working-process/rules/*.md | wc -l)" +echo "B $(grep -c 'six rule files' plugins/working-process/README.md)" +echo "C $(grep -c 'five rule files' plugins/working-process/README.md)" +``` + +Expected: `A 5`, `B 0`, `C 1`. + +- [ ] **Step 2: Create the rule file** + +Write `plugins/working-process/rules/propagation-duties.md` with exactly +this content: + +````markdown +--- +paths: + - "docs/specs/**" + - "docs/plans/**" + - "docs/domain/**" +--- + +# Propagation duties — the author's checklist + +The `propagation-auditor` agent walks eight duties over a spec or plan +before an expensive reader sees it. This rule is that list turned +around: keyed by the edit an author has just made, so the enumeration a +duty demands can be done at the desk instead of paid for at the gate. +The agent's card is the definition and keeps the measurement behind each +duty; this rule cites them by number and repeats none of that evidence. + +| You have just… | Enumerate | Duty | +|---|---|---| +| changed an interface — a signature, a name, a heading, an anchor, a field | every consumer, by parsing the structure that defines them, never by text match | 1 | +| prescribed a verbatim block | the anchor it targets — the text it replaces must exist in that file byte-exactly, and once | 2 | +| reported a change already made | that block against the shipped file, both ways: one that never landed, and shipped text a block no longer matches | 2 | +| added a field, label or state | both ends of its chain — what writes it, and what reads it | 3 | +| asserted a count | the count itself, re-derived from what the tool prints or what the list holds | 4 | +| used a name your source does not define | the source's own names; an invented name is a gap in the source rather than an error in yours | 5 | +| reported another document's state | that sentence, against that document | 6 | +| written a verification command | the command, run against your own replacement text | 7 | +| copied a citation out of a review report | the file and line it names, read at the source | 8 | + +Two of these fire where an author does not expect them. A newly minted +glossary `_Avoid_` ban is a changed interface, so duty 1 reaches every +shipped occurrence of the banned term — which is why this rule loads at +the domain directory as well as at specs and plans. And a count asserted +about another document is duties 4 and 6 at once: re-derive it, then +read it back at its source. + +Doing this before a dispatch never replaces the gate. The audit runs on +a fresh context and finds what an author's eye has stopped seeing; what +self-checking buys is a shorter list for it to find, which is what makes +the round after it cheaper. +```` + +- [ ] **Step 3: Update the README** + +Find: + +``` +The plugin ships five rule files in `rules/` — the preferred workflow +(always loaded once installed), spec/plan frontmatter and lifecycle, +Process directory conventions, ticket frontmatter, and the review-report +contract (`review-reports.md`: where a code-review run writes its +Review report and what shape it takes; domain review skills locate the +installed contract via its contract probe — the project-level then +user-level install path, in that order). Claude Code +does not load plugin rules by itself: install them with the +`working-process:sync-rules` skill. +``` + +Replace with: + +``` +The plugin ships six rule files in `rules/` — the preferred workflow +(always loaded once installed), spec/plan frontmatter and lifecycle, +Process directory conventions, ticket frontmatter, the propagation +duties keyed by the edit that triggers them (`propagation-duties.md`, +loaded while a spec, plan or domain document is open), and the +review-report contract (`review-reports.md`: where a code-review run +writes its Review report and what shape it takes; domain review skills +locate the installed contract via its contract probe — the +project-level then user-level install path, in that order). Claude Code +does not load plugin rules by itself: install them with the +`working-process:sync-rules` skill. +``` + +- [ ] **Step 4: Verify** + +Run the Step 1 command again, plus the rule's own scope check: + +```bash +echo "D $(grep -c 'docs/domain/\*\*' plugins/working-process/rules/propagation-duties.md)" +``` + +Expected: `A 6`, `B 1`, `C 0`, `D 1`. + +`D` is 1 because the frontmatter entry is the only line carrying that +literal; the prose says "the domain directory" for exactly that reason +(deviation 3). + +- [ ] **Step 5: Commit** + +```bash +git add plugins/working-process/rules/propagation-duties.md plugins/working-process/README.md +git commit -m "feat(working-process): ship the propagation duties as an author-facing rule" +``` + +--- + +### Task 7: end-state sweep + +**Files:** +- Modify: none. This task only verifies and reports. + +**Interfaces:** +- Consumes: every preceding task's deliverable. +- Produces: the report the developer reads before deciding what follows. + +- [ ] **Step 1: Run every check the spec publishes** + +```bash +cd "$(git rev-parse --show-toplevel)" +W=plugins/working-process/rules/workflow.md +L=plugins/working-process/rules/spec-plan-lifecycle.md +T=plugins/working-process/rules/ticket-frontmatter.md +S=plugins/working-process/skills/process-status/SKILL.md +G=docs/domain/glossary.md +n() { tr -s '[:space:]' ' ' < "$1"; } +echo "W1a $(n $W | grep -o 'never terminates on a diff-scoped LGTM' | wc -l) want 0" +echo "W1b $(n $W | grep -o 'latest verdict round was full-document' | wc -l) want 1" +echo "W1c $(n $W | grep -o 'inherits the gating of whatever ended' | wc -l) want 1" +echo "W1d $(n $W | grep -o 'runs under the loop.s standing consent like any other' | wc -l) want 0" +echo "W1e $(n $W | grep -o 'at the document.s next touch' | wc -l) want 1 (invariant)" +echo "W1f $(n $W | grep -o 'or a confirming full-document round' | wc -l) want 0" +echo "W1g $(n $W | grep -o 'confirming round alone' | wc -l) want 0" +echo "W1h $(n $W | grep -o 'a confirming round on a spec' | wc -l) want 0" +echo "W1i $(n $W | grep -o 'The confirming-round arm' | wc -l) want 0" +echo "W1j $(n $L | grep -o 'except on a plan whose latest round heading is a diff-scoped' | wc -l) want 0" +echo "W1k $(n $L | grep -o 'highest ordinal' | wc -l) want 1" +echo "W1l $(n $L | grep -o 'continue across loops' | wc -l) want 1" +echo "W1m $(n $L | grep -o 'where that heading is an .LGTM.' | wc -l) want 1" +echo "W1n $(n $L | grep -o 'wait for the confirming round' | wc -l) want 1" +echo "W1o $(grep -c '^\*\*Confirming round\*\*:' $G) want 1 (invariant)" +echo "W1p $(n $G | grep -o 'inherits the gating' | wc -l) want 1 (invariant)" +echo "W1q $(n $G | grep -o 'A plan already .implemented. owes none' | wc -l) want 1 (invariant)" +echo "W2a $(n $W | grep -o 'unbounded by the cap' | wc -l) want 1" +echo "W2b $(n $W | grep -o 'records their close, and no session' | wc -l) want 0" +echo "W2c $(n $W | grep -o 'on a plan, once the confirming round has run' | wc -l) want 1" +echo "W3 $(n $L | grep -o 'never stands in for the' | wc -l) want 1" +echo "W4a $(n $S | grep -o 'two confirmed hits share one document' | wc -l) want 1" +echo "W4b $(grep -c 'Unresolved verdict and Chain debt' $S) want 0 (invariant)" +echo "W5 $(grep -c 'decline — of the gate' $G) want 0" +echo "W6a $(grep -c '^## Branch naming' $W) want 1" +echo "W6b $(grep -c '^## Branch naming' $T) want 0 (invariant)" +echo "W6c $(n $T | grep -o 'branch naming convention' | wc -l) want 1" +echo "W7a $(ls plugins/working-process/rules/*.md | wc -l) want 6" +echo "W7b $(grep -c 'docs/domain/\*\*' plugins/working-process/rules/propagation-duties.md) want 1" +echo "W7c $(grep -c 'six rule files' plugins/working-process/README.md) want 1" +``` + +Expected: every line's value equals its `want`. Report any that does +not, with the file and the phrase. + +- [ ] **Step 2: Validate the plugin and the marketplace** + +```bash +claude plugin validate plugins/working-process && claude plugin validate . +``` + +Expected: both pass. This is the declared invariant `W7d` — they passed +before the wave, and a wave that only adds a rule file must not be what +breaks them. + +- [ ] **Step 3: Confirm the ruleset hash moved** + +```bash +plugins/working-process/scripts/ruleset-hash.sh plugins/working-process/rules +grep -o '"rulesetHash": "[^"]*"' ~/.claude/rules/working-process/.manifest.json +``` + +Expected: the two differ. That difference is the drift the developer +clears by running `sync-rules`; this plan does not run it, and the +difference is the evidence that it is owed. + +- [ ] **Step 4: Confirm the wrap width held** + +```bash +for f in plugins/working-process/rules/workflow.md \ + plugins/working-process/rules/spec-plan-lifecycle.md \ + plugins/working-process/rules/ticket-frontmatter.md \ + plugins/working-process/rules/propagation-duties.md \ + plugins/working-process/skills/process-status/SKILL.md \ + docs/domain/glossary.md; do + echo "$f: $(awk 'length > 78 && $0 !~ /^\|/ && $0 !~ /^ / {c++} END {print c+0}' "$f") long lines" +done +``` + +Expected: `0` for every file except `propagation-duties.md`, whose table +rows are exempt — the `awk` skips lines starting with `|`, so a non-zero +count there names a prose line to rewrap, not a table row. + +- [ ] **Step 5: Report the end state** + +State to the developer: seven changes landed across six files, one file +created, every published check at its expected value, both validations +passing, and the ruleset hash now differing from the installed +manifest's. Name the two things this plan deliberately left undone — +running `sync-rules`, and moving the spec's `status` to `implemented` — +and that both are theirs. + +Do not commit in this task; there is nothing to commit. + +## Self-review + +**Spec coverage.** W1 → Tasks 1, 3 and 4 (its `workflow.md`, +`spec-plan-lifecycle.md` and glossary halves). W2 → Task 2. W3 → Task 3 +step 5. W4 → Task 5. W5 → Task 4 step 3. W6 → Task 2 steps 4 and 5. +W7 → Task 6. Every check in the spec's Verification section appears in +Task 7 step 1, and each also appears in the before/after pair of the +task that produces it. The spec's three exclusions produce no task by +design, and Task 7 step 3 turns one of them — the owed `sync-rules` +run — into evidence rather than silence. + +**Placeholder scan.** No step says TBD, "handle appropriately", or +"similar to Task N"; every edit carries its find text and its +replacement text in full. + +**Consistency.** The phrases the checks anchor are the phrases the +replacement texts contain, verified by reading each replacement against +its check: `latest verdict round was full-document` (Task 1 step 3), +`inherits the gating of whatever ended` (same), `unbounded by the cap` +(Task 2 step 2), `on a plan, once the confirming round has run` (Task 2 +step 3), `highest ordinal` and `continue across loops` (Task 3 step 3 +and Task 4 step 2), `where that heading is an LGTM` (Task 3 step 2), +`wait for the confirming round` (Task 3 step 4), `never stands in for +the` (Task 3 step 5), `two confirmed hits share one document` (Task 5 +step 2), `branch naming convention` (Task 2 step 5), `six rule files` +(Task 6 step 3). From b0ab00b8ade9fe1d14c36dfa323e51fd2ab4cb27 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 22:35:38 +0200 Subject: [PATCH 054/120] docs: apply plan-adversary round one to the errata wave-four plan --- ...2026-09-11-review-loop-errata-wave-four.md | 254 ++++++++++++------ 1 file changed, 177 insertions(+), 77 deletions(-) diff --git a/docs/plans/2026-09-11-review-loop-errata-wave-four.md b/docs/plans/2026-09-11-review-loop-errata-wave-four.md index 70a5740..ae35e29 100644 --- a/docs/plans/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/plans/2026-09-11-review-loop-errata-wave-four.md @@ -2,6 +2,7 @@ ticket: none date: 2026-09-11 status: draft +adversary: blocking spec: ../specs/2026-09-11-review-loop-errata-wave-four.md branch: feature/process-wave-four base: develop @@ -48,6 +49,12 @@ payload; `grep`, `tr` and `claude plugin validate` for verification. are canonical here. - **Public repo hygiene**: no machine-specific paths, no company or client names, all committed text in English. +- **The replacement texts already carry the elements-of-style pass.** + The repo requires it over changed prose in rules, skills, READMEs and + the glossary; it was applied to every Replace block at authoring, so + an implementer copying a block verbatim is complying rather than + skipping it. A block an implementer rewords leaves that guarantee and + owes the pass again. - **Commit messages are one line** — a conventional-commit subject, no body, no trailers. - **Do not run `sync-rules` and do not move the spec's `status`.** Both @@ -83,7 +90,8 @@ Modified: - `plugins/working-process/rules/workflow.md` — Task 1 (the plan paragraph and the spec's pair-offer arm, inside *What a diff-scoped LGTM certifies*) and Task 2 (two Terminators bullets, plus a new - `## Branch naming` section). The two tasks share the file and touch no + `## Branch naming` section, and the all-Minor qualifier moved into + Task 1 by review round one). The two tasks share the file and touch no common line. - `plugins/working-process/rules/spec-plan-lifecycle.md` — Task 3: the Unresolved verdict owner leg, two sentences in *The disposition @@ -112,9 +120,11 @@ order, with three caveats this plan fixes by its task order: when it reads them. Task 2 inserts a heading above those lines, which is why it runs second. Every check anchors text rather than a line, so the order is a convenience, not a dependency. -- Task 2's W2 sentence assumes decision 1's semantics, which Task 1 - lands. Running Task 1 first keeps the rule self-consistent at every - commit. +- Task 2's cap sentence assumes decision 1's semantics, which Task 1 + lands. Task 1 also carries the all-Minor qualifier, which review round + one moved there from Task 2: without it, Task 1's own commit would + leave `workflow.md` saying both that the annotation waits for the + confirming round and that it records the developer's close. - `sync-rules` and `claude plugin validate` run once, at Task 7, after every file has changed. Validation before then proves nothing about the end state. @@ -226,8 +236,10 @@ Replace with: ``` For a plan the loop closes only when the latest verdict round was -full-document: whatever ends its rounds, one confirming full-document -round follows, and that round's verdict is the one stamped. It inherits +full-document: whatever ends its rounds while the latest round heading +is diff-scoped, one confirming full-document round follows, and that +round's verdict is the one stamped. A round already full-document owes +no successor — it is the close. It inherits the gating of whatever ended the rounds. Where autonomy still stands — a diff-scoped `LGTM`, or a `concerns` inside the cap — the rules mandate the round, so it is no decision of the developer's and spends none of @@ -255,16 +267,46 @@ annotation, an adjudication, a `status` move — or when a terminator that suspends autonomy has fired. ``` -- [ ] **Step 4: Verify** +- [ ] **Step 4: Qualify the all-Minor bullet** -Run the Step 1 command again. +This lands here rather than in Task 2 so that no commit leaves the rule +saying two things: Step 3 writes that the annotation waits for the +confirming round, and until this edit the all-Minor bullet still says the +annotation records the developer's close. + +Find: + +``` + until the developer answers it: the resolution annotation the + spec-plan-lifecycle rule defines records their close, and no session + writes it without their answer. +``` + +Replace with: + +``` + until the developer answers it: the resolution annotation the + spec-plan-lifecycle rule defines records their close — on a plan, once + the confirming round has run — and no session writes it without their + answer. +``` -Expected: `A 0`, `B 1`, `C 1`, `D 0`, `E 1`, `F 0`, `G 0`, `H 0`, `I 0`. +- [ ] **Step 5: Verify** + +Run the Step 1 command again, with the two the qualifier moves: + +```bash +echo "J $(n $W | grep -o 'records their close, and no session' | wc -l)" +echo "K $(n $W | grep -o 'on a plan, once the confirming round has run' | wc -l)" +``` + +Expected: `A 0`, `B 1`, `C 1`, `D 0`, `E 1`, `F 0`, `G 0`, `H 0`, `I 0`, +`J 0`, `K 1`. `E` is the declared invariant: the phrase survives because the `LGTM` heading keeps it. -- [ ] **Step 5: Check the neighbours the rename must not touch** +- [ ] **Step 6: Check the neighbours the rename must not touch** The words "confirming round" stay wherever they name the plan's obligation. Run: @@ -278,7 +320,7 @@ for the confirming round"), in the Round cap bullet (two), and in the paragraphs Step 3 wrote. No match remains in the spec's pair-offer paragraph. -- [ ] **Step 6: Commit** +- [ ] **Step 7: Commit** ```bash git add plugins/working-process/rules/workflow.md @@ -309,14 +351,14 @@ W=plugins/working-process/rules/workflow.md T=plugins/working-process/rules/ticket-frontmatter.md n() { tr -s '[:space:]' ' ' < "$1"; } echo "A $(n $W | grep -o 'unbounded by the cap' | wc -l)" -echo "B $(n $W | grep -o 'records their close, and no session' | wc -l)" -echo "C $(n $W | grep -o 'on a plan, once the confirming round has run' | wc -l)" echo "D $(grep -c '^## Branch naming' $W)" echo "E $(grep -c '^## Branch naming' $T)" echo "F $(n $T | grep -o 'branch naming convention' | wc -l)" ``` -Expected: `A 0`, `B 1`, `C 0`, `D 0`, `E 0`, `F 0`. +Expected: `A 0`, `D 0`, `E 0`, `F 0`. The all-Minor qualifier that used +to sit in this task moved to Task 1 step 4, so that no commit leaves the +rule self-contradicting; its two checks moved with it. - [ ] **Step 2: Extend the Round cap bullet** @@ -340,26 +382,7 @@ Replace with: Consenting to three rounds consents to three autonomous ones. ``` -- [ ] **Step 3: Qualify the all-Minor bullet** - -In the `All-Minor signal` bullet, find: - -``` - until the developer answers it: the resolution annotation the - spec-plan-lifecycle rule defines records their close, and no session - writes it without their answer. -``` - -Replace with: - -``` - until the developer answers it: the resolution annotation the - spec-plan-lifecycle rule defines records their close — on a plan, once - the confirming round has run — and no session writes it without their - answer. -``` - -- [ ] **Step 4: Add the Branch naming section** +- [ ] **Step 3: Add the Branch naming section** The words `## Dispatching a verdict agent` occur twice in this file — once as the heading, and once inside backticks in the closing paragraph @@ -391,15 +414,16 @@ issue as its number alone (`feature/123-short-name`), since a leading `#` and the `/` of an `org/repo#123` form would both change what the ref means. -A worktree created with a generated name is renamed to this shape before -its first commit, so the branch a reader sees is the branch the -convention names. The work's spec and plan record the result in their +The branch of a worktree created with a generated name is renamed to +this shape before its first commit, so the branch a reader sees is the +branch the convention names; the worktree's own directory is a separate +name and this convention does not govern it. The work's spec and plan record the result in their `branch:` field, and the ticket rule reads the ticket back out of it. ## Dispatching a verdict agent ``` -- [ ] **Step 5: Point the ticket rule at it** +- [ ] **Step 4: Point the ticket rule at it** In `ticket-frontmatter.md`, `## Sourcing and backfill`, find: @@ -419,19 +443,23 @@ for one unit of work — a plan inherits its spec's ticket, and artifacts of the same session reuse the established value. The branch naming convention that produces the names this order reads is the workflow rule's, which is where a branch is cut — before any `docs/` file of that -work exists, and so before this rule loads. +work exists, and so before this rule loads. Reading a name back: a +Jira-shaped segment is the key itself; a bare leading number is a +GitHub or GitLab issue and becomes `"#"`, quoted as the Values +section requires; an `org/repo#123` reference cannot survive a branch +name and comes from the next source in the order above. ``` -- [ ] **Step 6: Verify** +- [ ] **Step 5: Verify** Run the Step 1 command again. -Expected: `A 1`, `B 0`, `C 1`, `D 1`, `E 0`, `F 1`. +Expected: `A 1`, `D 1`, `E 0`, `F 1`. `E` is the declared invariant: the section belongs to the workflow rule and must never appear in the ticket rule. -- [ ] **Step 7: Commit** +- [ ] **Step 6: Commit** ```bash git add plugins/working-process/rules/workflow.md plugins/working-process/rules/ticket-frontmatter.md @@ -548,9 +576,9 @@ Replace with: the round's ledger record as that body note. On a plan both wait for the confirming round: neither is written while the latest round heading is diff-scoped, unless the plan is already `implemented`, - where the annotation is written citing that standing decision — such a - document is amended in frontmatter, so the annotation is the only - lever left. + where the annotation — or the developer's adjudication — is written + citing that standing decision, such a document being amended in + frontmatter, where that entry is the only lever left. ``` - [ ] **Step 5: Part the round sha from the integrity hash** @@ -790,12 +818,18 @@ paths: # Propagation duties — the author's checklist -The `propagation-auditor` agent walks eight duties over a spec or plan -before an expensive reader sees it. This rule is that list turned -around: keyed by the edit an author has just made, so the enumeration a -duty demands can be done at the desk instead of paid for at the gate. -The agent's card is the definition and keeps the measurement behind each -duty; this rule cites them by number and repeats none of that evidence. +Before a document goes to an expensive reader, eight classes of change +owe an enumeration. This rule lists them keyed by the edit an author has +just made, so the enumeration can be done at the desk instead of paid +for at the gate. The list below is complete as it stands and needs +nothing else to be usable. + +When the `propagation-auditor` agent is available it walks the same +eight as a gate, and its card is their definition and keeps the +measurement behind each; the numbers in the last column are that card's, +so a hit it reports can be read back to the row that would have caught +it. Without the agent the rows still hold — what is lost is the second +pair of eyes, not the duties. | You have just… | Enumerate | Duty | |---|---|---| @@ -816,10 +850,10 @@ the domain directory as well as at specs and plans. And a count asserted about another document is duties 4 and 6 at once: re-derive it, then read it back at its source. -Doing this before a dispatch never replaces the gate. The audit runs on -a fresh context and finds what an author's eye has stopped seeing; what -self-checking buys is a shorter list for it to find, which is what makes -the round after it cheaper. +Self-checking never replaces a gate where one runs. A fresh context +finds what an author's eye has stopped seeing; what this buys is a +shorter list for it to find, which is what makes the round after it +cheaper. ```` - [ ] **Step 3: Update the README** @@ -868,7 +902,22 @@ Expected: `A 6`, `B 1`, `C 0`, `D 1`. literal; the prose says "the domain directory" for exactly that reason (deviation 3). -- [ ] **Step 5: Commit** +- [ ] **Step 5: Read the new frontmatter back** + +`claude plugin validate` does not check `rules/` — a rule file is not a +plugin component — so an unquoted `: ` in a scalar fails silently at +load time and nothing in Task 7 would catch it. Read the block back and +parse it: + +```bash +sed -n '1,6p' plugins/working-process/rules/propagation-duties.md +python3 -c "import yaml,sys; d=yaml.safe_load(open('plugins/working-process/rules/propagation-duties.md').read().split('---')[1]); print(d); assert d['paths']==['docs/specs/**','docs/plans/**','docs/domain/**']" +``` + +Expected: the six lines are the `---`, three `paths:` entries and the +closing `---`; the parse prints the dict and the assertion passes. + +- [ ] **Step 6: Commit** ```bash git add plugins/working-process/rules/propagation-duties.md plugins/working-process/README.md @@ -939,44 +988,74 @@ claude plugin validate plugins/working-process && claude plugin validate . Expected: both pass. This is the declared invariant `W7d` — they passed before the wave, and a wave that only adds a rule file must not be what -breaks them. +breaks them. What it does not cover is the rule file itself: `validate` +reads plugin components, and `rules/` is not one, so the new file's +frontmatter is checked by Task 6 step 5 and by nothing here. -- [ ] **Step 3: Confirm the ruleset hash moved** +- [ ] **Step 3: Confirm the payload changed, and say what that does not mean** ```bash -plugins/working-process/scripts/ruleset-hash.sh plugins/working-process/rules -grep -o '"rulesetHash": "[^"]*"' ~/.claude/rules/working-process/.manifest.json -``` - -Expected: the two differ. That difference is the drift the developer -clears by running `sync-rules`; this plan does not run it, and the -difference is the evidence that it is owed. - -- [ ] **Step 4: Confirm the wrap width held** +echo "repo $(plugins/working-process/scripts/ruleset-hash.sh plugins/working-process/rules)" +CACHE=$(claude plugin list --json | python3 -c 'import json,sys; print(next(p["installPath"] for p in json.load(sys.stdin) if p["id"]=="working-process@missing-bits"))') +echo "cache $(plugins/working-process/scripts/ruleset-hash.sh "$CACHE/rules")" +echo "manifest $(grep -o '"rulesetHash": "[^"]*"' ~/.claude/rules/working-process/.manifest.json)" +``` + +Expected: `repo` differs from `cache`, and `cache` still equals +`manifest`. + +Read that result carefully, because the obvious reading is wrong. The +difference proves the payload changed **in this checkout**. It is not +drift any installed copy can see: the drift hook hashes +`$CLAUDE_PLUGIN_ROOT/rules`, and `sync-rules` discovers its source from +the `installPath` that `claude plugin list` reports — both of which are +the installed plugin cache, not this working tree. The cache stays at +its released version until a release or a `-dev` dogfood install puts +this branch there. So `sync-rules` run today would report no drift and +would re-copy the old rules, and the developer would keep running the +process under them while believing this wave was installed. + +- [ ] **Step 4: Confirm the wrap width did not worsen** + +The constraint is "match the surrounding paragraph", not a hard column: +`workflow.md` already carries three lines over 72 characters that this +wave does not touch. So the check is a comparison, not a threshold — +count the long lines in each file at `HEAD` before the wave and now, and +require the second number not to exceed the first. ```bash +BASE=$(git merge-base HEAD develop) for f in plugins/working-process/rules/workflow.md \ plugins/working-process/rules/spec-plan-lifecycle.md \ plugins/working-process/rules/ticket-frontmatter.md \ - plugins/working-process/rules/propagation-duties.md \ plugins/working-process/skills/process-status/SKILL.md \ docs/domain/glossary.md; do - echo "$f: $(awk 'length > 78 && $0 !~ /^\|/ && $0 !~ /^ / {c++} END {print c+0}' "$f") long lines" + before=$(git show "$BASE:$f" | awk 'length > 72 && $0 !~ /^\|/' | wc -l) + after=$(awk 'length > 72 && $0 !~ /^\|/' "$f" | wc -l) + printf '%-62s before %s after %s\n' "$f" "$before" "$after" done +awk 'length > 72 && $0 !~ /^\|/' plugins/working-process/rules/propagation-duties.md | wc -l ``` -Expected: `0` for every file except `propagation-duties.md`, whose table -rows are exempt — the `awk` skips lines starting with `|`, so a non-zero -count there names a prose line to rewrap, not a table row. +Expected: `after` is not greater than `before` for any of the five, and +`0` for the new rule file, whose table rows the `awk` skips. + +On a count that grew, rewrap the offending paragraph in the file that +owns it and commit it as `style(working-process): rewrap `. Do not +reflow a paragraph this wave did not change. - [ ] **Step 5: Report the end state** State to the developer: seven changes landed across six files, one file -created, every published check at its expected value, both validations -passing, and the ruleset hash now differing from the installed -manifest's. Name the two things this plan deliberately left undone — -running `sync-rules`, and moving the spec's `status` to `implemented` — -and that both are theirs. +created, every published check at its expected value, and +`claude plugin validate` passing for the plugin and the marketplace. +Then state the delivery gap in its own sentence: this branch's rules are +not the rules any session is running, and will not be until the plugin +cache carries this branch — through a release, or a `-dev` dogfood +install. Neither the drift hook nor `sync-rules` will announce that, +because both read the cache. Name the two things this plan deliberately +left undone — the install, and moving the spec's `status` to +`implemented` — and that both are theirs. Do not commit in this task; there is nothing to commit. @@ -1006,3 +1085,24 @@ and Task 4 step 2), `where that heading is an LGTM` (Task 3 step 2), the` (Task 3 step 5), `two confirmed hits share one document` (Task 5 step 2), `branch naming convention` (Task 2 step 5), `six rule files` (Task 6 step 3). + +## Review rounds + +### 2026-09-11 — plan-adversary, fable 5.1, blocking (round 1, full-document) + +The propagation gate returned `CLEAN` beforehand, so this round wrote no +gate lines. Every citation was checked before anything was written here, +the drift-hook and sync-rules source paths included — the finding that +cost this plan its most confident sentence. + +- fixed 2026-09-11 — [Important] the new plan paragraph dropped decision 1's "unless the latest verdict round already was full-document" clause, reading as an unconditional obligation — a plan closed by a full-document round would owe another one; license: decision 1's own text and the glossary's **Confirming round** entry, which conditions the obligation on a diff-scoped latest heading; the sentence now carries "while the latest round heading is diff-scoped" and says outright that a full-document round is the close +- fixed 2026-09-11 — [Important] Task 7 told the developer the repo-versus-manifest hash difference was "the drift the developer clears by running `sync-rules`", while both the drift hook and that skill read their source from the installed plugin cache rather than this checkout — so the command would report no drift, re-copy the old rules, and leave the developer running the process under them while believing the wave was installed; license: `check-rules-drift.sh`, which hashes `$CLAUDE_PLUGIN_ROOT/rules`, and the sync-rules skill's step 0, which discovers its source from `installPath`; the step now compares three hashes, states that the difference proves only that the payload changed here, and names what actually delivers it — a release or a `-dev` dogfood install +- fixed 2026-09-11 — [Important] the new rule named the `propagation-auditor` agent unconditionally and pointed at "the agent's card" as its definition, where committed project-level rules load for readers without the plugin; license: `.claude/rules/plugin-authoring.md`, which requires agent mentions in rule text to be conditional; the rule now states the eight classes as its own complete list and makes the agent, its card and the duty numbers a conditional second paragraph +- fixed 2026-09-11 — [Important] the branch convention minted a name shape the ticket rule must decode — a bare leading number — while only one end of that chain was written: nothing said how the number reads back, nor that an `org/repo#123` reference cannot survive a branch name; license: the ticket rule's own sourcing order and its requirement that a `#`-leading value be quoted, plus the new rule's duty-3 row demanding both ends of a chain; the pointer now states all three read-backs +- fixed 2026-09-11 — [Minor] the plan claimed Task 1 "keeps the rule self-consistent at every commit" while Task 1's own commit would leave `workflow.md` saying both that the annotation waits for the confirming round and that it records the developer's close, the repair landing only in Task 2; license: the spec's sentence that W1 makes that bullet untrue on a plan; the qualifier and its two checks moved into Task 1 as step 4, and the order section says why +- fixed 2026-09-11 — [Minor] the annotation bullet's implemented-plan exception freed only "the annotation" while the owner leg written in the same task has a `blocking` heading waiting for the developer's adjudication — read literally the bullet forbade that adjudication; license: the spec's clause that a session derives the `concerns` decline and never the adjudication, which leaves the adjudication the developer's to write; the bullet now names both +- fixed 2026-09-11 — [Minor] "A worktree created with a generated name is renamed to this shape" named the wrong object: the shape is a branch name, and this repo keeps the worktree directory at its own path regardless; license: this repo's `CLAUDE.md`, which fixes that directory independently; the sentence now renames the branch and says the worktree's directory is a separate name +- fixed 2026-09-11 — [Minor] the wrap check flagged lines over 78 while the constraint says 72, and skipped every indented line — which is where three replacements land — with no repair path for a non-zero count; license: the plan's own Global Constraint; the check is now a before/after comparison against the merge base, since `workflow.md` already carries three long lines this wave does not touch, and it names the rewrap commit +- fixed 2026-09-11 — [Minor] `claude plugin validate` was presented as the invariant guarding the new rule file, which it cannot be: it does not read `rules/` at all, so an unquoted `: ` would fail silently at load time; license: `.claude/rules/plugin-authoring.md`, which says so and asks for a hand review; Task 6 gained a frontmatter read-back with a YAML parse, and Task 7 says what validate does not cover +- fixed 2026-09-11 — [Minor] no step invoked the elements-of-style pass the repo mandates over changed prose, and an implementer copying verbatim text could not run it without deviating; license: `.claude/rules/elements-of-style.md`; a Global Constraint now records that every Replace block carries the pass from authoring, and that rewording a block forfeits the guarantee +- signal 2026-09-11 — another round earns its cost: the four Important repairs change rule text the wave hangs on and the report the developer acts on, so a diff-scoped read aimed at those four first is worth it; what remains after that is wording, one threshold and one added step From c9690eebbd88f406b20307697d259affddde72d1 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 22:41:57 +0200 Subject: [PATCH 055/120] docs: correct three step references the renumbering left stale --- .../2026-09-11-review-loop-errata-wave-four.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/plans/2026-09-11-review-loop-errata-wave-four.md b/docs/plans/2026-09-11-review-loop-errata-wave-four.md index ae35e29..d7c287b 100644 --- a/docs/plans/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/plans/2026-09-11-review-loop-errata-wave-four.md @@ -1063,12 +1063,12 @@ Do not commit in this task; there is nothing to commit. **Spec coverage.** W1 → Tasks 1, 3 and 4 (its `workflow.md`, `spec-plan-lifecycle.md` and glossary halves). W2 → Task 2. W3 → Task 3 -step 5. W4 → Task 5. W5 → Task 4 step 3. W6 → Task 2 steps 4 and 5. +step 5. W4 → Task 5. W5 → Task 4 step 3. W6 → Task 2 steps 3 and 4. W7 → Task 6. Every check in the spec's Verification section appears in Task 7 step 1, and each also appears in the before/after pair of the task that produces it. The spec's three exclusions produce no task by -design, and Task 7 step 3 turns one of them — the owed `sync-rules` -run — into evidence rather than silence. +design, and Task 7 step 3 turns one of them — the owed install — into a +statement of what is and is not delivered rather than silence. **Placeholder scan.** No step says TBD, "handle appropriately", or "similar to Task N"; every edit carries its find text and its @@ -1078,14 +1078,18 @@ replacement text in full. replacement texts contain, verified by reading each replacement against its check: `latest verdict round was full-document` (Task 1 step 3), `inherits the gating of whatever ended` (same), `unbounded by the cap` -(Task 2 step 2), `on a plan, once the confirming round has run` (Task 2 -step 3), `highest ordinal` and `continue across loops` (Task 3 step 3 +(Task 2 step 2), `on a plan, once the confirming round has run` (Task 1 +step 4), `highest ordinal` and `continue across loops` (Task 3 step 3 and Task 4 step 2), `where that heading is an LGTM` (Task 3 step 2), `wait for the confirming round` (Task 3 step 4), `never stands in for the` (Task 3 step 5), `two confirmed hits share one document` (Task 5 -step 2), `branch naming convention` (Task 2 step 5), `six rule files` +step 2), `branch naming convention` (Task 2 step 4), `six rule files` (Task 6 step 3). +Every step reference above was re-derived after round one moved the +all-Minor qualifier into Task 1 and renumbered two tasks; three were +stale and are corrected here. + ## Review rounds ### 2026-09-11 — plan-adversary, fable 5.1, blocking (round 1, full-document) @@ -1105,4 +1109,5 @@ cost this plan its most confident sentence. - fixed 2026-09-11 — [Minor] the wrap check flagged lines over 78 while the constraint says 72, and skipped every indented line — which is where three replacements land — with no repair path for a non-zero count; license: the plan's own Global Constraint; the check is now a before/after comparison against the merge base, since `workflow.md` already carries three long lines this wave does not touch, and it names the rewrap commit - fixed 2026-09-11 — [Minor] `claude plugin validate` was presented as the invariant guarding the new rule file, which it cannot be: it does not read `rules/` at all, so an unquoted `: ` would fail silently at load time; license: `.claude/rules/plugin-authoring.md`, which says so and asks for a hand review; Task 6 gained a frontmatter read-back with a YAML parse, and Task 7 says what validate does not cover - fixed 2026-09-11 — [Minor] no step invoked the elements-of-style pass the repo mandates over changed prose, and an implementer copying verbatim text could not run it without deviating; license: `.claude/rules/elements-of-style.md`; a Global Constraint now records that every Replace block carries the pass from authoring, and that rewording a block forfeits the guarantee +- hit fixed 2026-09-11 — round one's wave moved the all-Minor qualifier into Task 1 and renumbered two tasks, leaving three step references in the Self-review section pointing at steps that had become something else — W6 at "Task 2 steps 4 and 5" where it is now 3 and 4, the qualifier's phrase at "Task 2 step 3", and the branch pointer at "Task 2 step 5"; the gate that preceded this round returned `CLEAN` and its body claimed to have checked that very mapping, so the dispatcher found these by re-deriving every step citation mechanically rather than by reading. All three corrected, and the section now records that the derivation was re-run - signal 2026-09-11 — another round earns its cost: the four Important repairs change rule text the wave hangs on and the report the developer acts on, so a diff-scoped read aimed at those four first is worth it; what remains after that is wording, one threshold and one added step From 3d6fa7bef8c84946ccbe333edeca9ad7e18ac9bd Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 22:50:23 +0200 Subject: [PATCH 056/120] docs: apply plan-adversary round two to the errata wave-four plan --- ...2026-09-11-review-loop-errata-wave-four.md | 139 ++++++++++++------ 1 file changed, 90 insertions(+), 49 deletions(-) diff --git a/docs/plans/2026-09-11-review-loop-errata-wave-four.md b/docs/plans/2026-09-11-review-loop-errata-wave-four.md index d7c287b..4375818 100644 --- a/docs/plans/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/plans/2026-09-11-review-loop-errata-wave-four.md @@ -90,9 +90,9 @@ Modified: - `plugins/working-process/rules/workflow.md` — Task 1 (the plan paragraph and the spec's pair-offer arm, inside *What a diff-scoped LGTM certifies*) and Task 2 (two Terminators bullets, plus a new - `## Branch naming` section, and the all-Minor qualifier moved into - Task 1 by review round one). The two tasks share the file and touch no - common line. + `## Branch naming` section; the all-Minor qualifier moved into Task 1 + at review round one). The two tasks share the file and touch no common + line. - `plugins/working-process/rules/spec-plan-lifecycle.md` — Task 3: the Unresolved verdict owner leg, two sentences in *The disposition ledger*, the resolution-annotation bullet, and one sentence in the @@ -125,9 +125,10 @@ order, with three caveats this plan fixes by its task order: one moved there from Task 2: without it, Task 1's own commit would leave `workflow.md` saying both that the annotation waits for the confirming round and that it records the developer's close. -- `sync-rules` and `claude plugin validate` run once, at Task 7, after - every file has changed. Validation before then proves nothing about - the end state. +- `claude plugin validate` runs once, at Task 7, after every file has + changed; validation before then proves nothing about the end state. + `sync-rules` never runs in this plan — the Global Constraints forbid + it and the spec puts it outside the wave. --- @@ -239,8 +240,8 @@ For a plan the loop closes only when the latest verdict round was full-document: whatever ends its rounds while the latest round heading is diff-scoped, one confirming full-document round follows, and that round's verdict is the one stamped. A round already full-document owes -no successor — it is the close. It inherits -the gating of whatever ended the rounds. Where autonomy still stands — +no successor — it is the close. The confirming round inherits the gating +of whatever ended the rounds. Where autonomy still stands — a diff-scoped `LGTM`, or a `concerns` inside the cap — the rules mandate the round, so it is no decision of the developer's and spends none of the round's one interruption. After `blocking`, the round cap or the @@ -316,9 +317,9 @@ grep -n 'confirming' plugins/working-process/rules/workflow.md ``` Expected: matches remain at the verdict-agent dispatch bullet ("waits -for the confirming round"), in the Round cap bullet (two), and in the -paragraphs Step 3 wrote. No match remains in the spec's pair-offer -paragraph. +for the confirming round"), in the Round cap bullet (two), in the +all-Minor bullet Step 4 qualified, and in the paragraphs Step 3 wrote. +No match remains in the spec's pair-offer paragraph. - [ ] **Step 7: Commit** @@ -333,14 +334,14 @@ git commit -m "feat(working-process): close a plan's loop only on a full-documen **Files:** - Modify: `plugins/working-process/rules/workflow.md` — the `Round cap` - and `All-Minor signal` bullets of `### Terminators`, and a new - `## Branch naming` section. + bullet of `### Terminators`, and a new `## Branch naming` section. The + `All-Minor signal` bullet belongs to Task 1 step 4. - Modify: `plugins/working-process/rules/ticket-frontmatter.md` — one sentence in `## Sourcing and backfill`. **Interfaces:** -- Consumes: Task 1's plan paragraph, whose semantics the all-Minor - qualifier assumes. +- Consumes: Task 1's plan paragraph, whose semantics the cap sentence + assumes. - Produces: the `## Branch naming` heading that the ticket rule's pointer names. @@ -408,17 +409,19 @@ substitute pass and no install nagging — the work proceeds normally. Feature work happens on a topic branch named `feature/-`, and `feature/` where the ticket is `none`. The ticket is the reference the ticket-frontmatter -rule defines, written without the punctuation a ref cannot carry: a -Jira key as it stands (`feature/ABC-123-short-name`), a GitHub or GitLab -issue as its number alone (`feature/123-short-name`), since a leading -`#` and the `/` of an `org/repo#123` form would both change what the ref -means. +rule defines, reduced to what a branch name should carry: a Jira key as +it stands (`feature/ABC-123-short-name`), a GitHub or GitLab issue as +its number alone (`feature/123-short-name`). Git would accept `#` and +`/` in a ref; the convention drops them anyway, because a `#` is awkward +to type unquoted in a shell and a `/` adds a hierarchy level that the +`feature/-` shape does not parse. The branch of a worktree created with a generated name is renamed to this shape before its first commit, so the branch a reader sees is the branch the convention names; the worktree's own directory is a separate -name and this convention does not govern it. The work's spec and plan record the result in their -`branch:` field, and the ticket rule reads the ticket back out of it. +name and this convention does not govern it. The work's spec and plan +record the result in their `branch:` field, and the ticket rule reads +the ticket back out of it. ## Dispatching a verdict agent ``` @@ -446,8 +449,8 @@ rule's, which is where a branch is cut — before any `docs/` file of that work exists, and so before this rule loads. Reading a name back: a Jira-shaped segment is the key itself; a bare leading number is a GitHub or GitLab issue and becomes `"#"`, quoted as the Values -section requires; an `org/repo#123` reference cannot survive a branch -name and comes from the next source in the order above. +section requires; an `org/repo#123` reference is not carried by the +convention, so it comes from the next source in the order above. ``` - [ ] **Step 5: Verify** @@ -818,9 +821,10 @@ paths: # Propagation duties — the author's checklist -Before a document goes to an expensive reader, eight classes of change -owe an enumeration. This rule lists them keyed by the edit an author has -just made, so the enumeration can be done at the desk instead of paid +Before a document goes to an expensive reader, eight duties fall due, +keyed by the nine edits that trigger them — one duty answers two +different edits. This rule lists them by the edit an author has just +made, so the enumeration can be done at the desk instead of paid for at the gate. The list below is complete as it stands and needs nothing else to be usable. @@ -909,13 +913,25 @@ plugin component — so an unquoted `: ` in a scalar fails silently at load time and nothing in Task 7 would catch it. Read the block back and parse it: +The check compares the written block against the prescribed one +literally. It uses no YAML parser: `python3 -c 'import yaml'` fails on +this host, and a plan must not make its only guard depend on a package +nobody declared. + ```bash -sed -n '1,6p' plugins/working-process/rules/propagation-duties.md -python3 -c "import yaml,sys; d=yaml.safe_load(open('plugins/working-process/rules/propagation-duties.md').read().split('---')[1]); print(d); assert d['paths']==['docs/specs/**','docs/plans/**','docs/domain/**']" +diff <(sed -n '1,6p' plugins/working-process/rules/propagation-duties.md) - <<'EOF' +--- +paths: + - "docs/specs/**" + - "docs/plans/**" + - "docs/domain/**" +--- +EOF ``` -Expected: the six lines are the `---`, three `paths:` entries and the -closing `---`; the parse prints the dict and the assertion passes. +Expected: no output and exit status 0. Any output is the diff between +what was written and what the plan prescribes; fix the file, not the +check. - [ ] **Step 6: Commit** @@ -1018,27 +1034,30 @@ process under them while believing this wave was installed. - [ ] **Step 4: Confirm the wrap width did not worsen** The constraint is "match the surrounding paragraph", not a hard column: -`workflow.md` already carries three lines over 72 characters that this -wave does not touch. So the check is a comparison, not a threshold — -count the long lines in each file at `HEAD` before the wave and now, and -require the second number not to exceed the first. +four of these files already carry long lines this wave does not touch, +and `docs/domain/glossary.md` gained one more from the wave's own +authoring before implementation began. So the check compares against +stated counts rather than against a git baseline, which would move under +it. ```bash -BASE=$(git merge-base HEAD develop) -for f in plugins/working-process/rules/workflow.md \ - plugins/working-process/rules/spec-plan-lifecycle.md \ - plugins/working-process/rules/ticket-frontmatter.md \ - plugins/working-process/skills/process-status/SKILL.md \ - docs/domain/glossary.md; do - before=$(git show "$BASE:$f" | awk 'length > 72 && $0 !~ /^\|/' | wc -l) - after=$(awk 'length > 72 && $0 !~ /^\|/' "$f" | wc -l) - printf '%-62s before %s after %s\n' "$f" "$before" "$after" +for pair in \ + "plugins/working-process/rules/workflow.md 3" \ + "plugins/working-process/rules/spec-plan-lifecycle.md 36" \ + "plugins/working-process/rules/ticket-frontmatter.md 1" \ + "plugins/working-process/skills/process-status/SKILL.md 3" \ + "docs/domain/glossary.md 50" \ + "plugins/working-process/README.md 7"; do + set -- $pair + now=$(awk 'length > 72 && $0 !~ /^\|/' "$1" | wc -l) + printf '%-62s was %-3s now %s\n' "$1" "$2" "$now" done awk 'length > 72 && $0 !~ /^\|/' plugins/working-process/rules/propagation-duties.md | wc -l ``` -Expected: `after` is not greater than `before` for any of the five, and -`0` for the new rule file, whose table rows the `awk` skips. +Expected: `now` equals `was` for all six — the replacement texts are +wrapped, so none of them adds a long line — and `0` for the new rule +file, whose table rows the `awk` skips. On a count that grew, rewrap the offending paragraph in the file that owns it and commit it as `style(working-process): rewrap `. Do not @@ -1051,9 +1070,11 @@ created, every published check at its expected value, and `claude plugin validate` passing for the plugin and the marketplace. Then state the delivery gap in its own sentence: this branch's rules are not the rules any session is running, and will not be until the plugin -cache carries this branch — through a release, or a `-dev` dogfood -install. Neither the drift hook nor `sync-rules` will announce that, -because both read the cache. Name the two things this plan deliberately +cache carries this branch: either through a release, or through a +prerelease install whose version convention the repo's plugin-versioning +rule defines and whose install procedure is the developer's own. Neither +the drift hook nor `sync-rules` will announce that, because both read +the cache. Name the two things this plan deliberately left undone — the install, and moving the spec's `status` to `implemented` — and that both are theirs. @@ -1111,3 +1132,23 @@ cost this plan its most confident sentence. - fixed 2026-09-11 — [Minor] no step invoked the elements-of-style pass the repo mandates over changed prose, and an implementer copying verbatim text could not run it without deviating; license: `.claude/rules/elements-of-style.md`; a Global Constraint now records that every Replace block carries the pass from authoring, and that rewording a block forfeits the guarantee - hit fixed 2026-09-11 — round one's wave moved the all-Minor qualifier into Task 1 and renumbered two tasks, leaving three step references in the Self-review section pointing at steps that had become something else — W6 at "Task 2 steps 4 and 5" where it is now 3 and 4, the qualifier's phrase at "Task 2 step 3", and the branch pointer at "Task 2 step 5"; the gate that preceded this round returned `CLEAN` and its body claimed to have checked that very mapping, so the dispatcher found these by re-deriving every step citation mechanically rather than by reading. All three corrected, and the section now records that the derivation was re-run - signal 2026-09-11 — another round earns its cost: the four Important repairs change rule text the wave hangs on and the report the developer acts on, so a diff-scoped read aimed at those four first is worth it; what remains after that is wording, one threshold and one added step + +### 2026-09-11 — plan-adversary, fable 5.1, blocking (round 2, diff-scoped) + +Scoped to round one's wave. The reviewer verified the delivery repair +against the hook script and the sync-rules skill and found it factually +sound — the one repair this round was told to attack first. Two others +did not survive execution on this branch. Every citation checked here, +including the two that needed running rather than reading. + +- fixed 2026-09-11 — [Important] round one's repair placed "A round already full-document owes no successor — it is the close." immediately before "It inherits the gating…", rebinding the pronoun to the closing round, so the shipped rule would say the closing round inherits gating — the opposite object from decision 5; license: the spec's own sentence, which makes the confirming round the subject; the sentence now names it +- fixed 2026-09-11 — [Important] the rebuilt wrap check baselined on the merge base with `develop`, but `docs/domain/glossary.md` was already edited on this branch before implementation begins — 49 long lines at the merge base against 50 now — so the step would report growth on a file the wave may not touch and order a rewrap the same step forbids; license: the measurement itself, run at this commit; the check now compares against six stated counts measured before implementation, which cannot move under it +- fixed 2026-09-11 — [Important] the frontmatter read-back, which Task 7 declares the only check of the new rule file, imported PyYAML — `python3 -c 'import yaml'` fails on this host, so the plan's sole guard would have died before asserting anything; license: that failure, reproduced at this commit; the check is now a literal `diff` of the first six lines against the prescribed block, with no dependency +- fixed 2026-09-11 — [Important] the branch convention justified itself with a false claim about git: `git check-ref-format` accepts both `feature/#123-short-name` and a `/`-bearing form, so "the punctuation a ref cannot carry" and "cannot survive a branch name" would have shipped an untruth in rule text; license: those two commands, run at this commit; the section now gives the true reasons — a `#` is awkward unquoted in a shell, a `/` adds a hierarchy level the shape does not parse — and the read-back says the `org/repo#123` form is not carried by the convention +- fixed 2026-09-11 — [Important] the delivery repair removed every `sync-rules` invocation from Task 7 while the Order section still said "`sync-rules` and `claude plugin validate` run once, at Task 7" — contradicting the Global Constraint, the spec's exclusion and the rewritten task; license: those three; the bullet now says validate runs there and `sync-rules` never runs in this plan +- fixed 2026-09-11 — [Minor] Task 1 step 6's expected `grep` output listed three sites for "confirming" and omitted the all-Minor bullet that step 4 had just written into the same file; license: step 4's own edit; the expectation now names four +- fixed 2026-09-11 — [Minor] three descriptors still credited Task 2 with the all-Minor bullet after the qualifier moved to Task 1 — the File structure entry, Task 2's Files block and its Interfaces block; license: the move itself, recorded in round one's ledger; all three now name the Round cap bullet alone +- fixed 2026-09-11 — [Minor] two replacement blocks written in round one's wave exceeded the 72-column constraint the plan commits to, so Task 7 step 4 would have reported `workflow.md` growing and ordered a `style:` commit to repair a defect the plan prescribed verbatim; license: the constraint; both paragraphs are rewrapped inside their Replace blocks, and every Replace block was re-measured — the one line still over 72 is an existing line carried through unchanged +- fixed 2026-09-11 — [Minor] the rewritten rule said "eight classes of change owe an enumeration. This rule lists them" and then listed nine rows, because one duty answers two different edits — a counter that does not re-derive, inside the rule whose duty 4 is counters; license: the card's eight duties against the table's nine triggers; the sentence now says eight duties keyed by nine edits +- fixed 2026-09-11 — [Minor] the delivery gap named "a release, or a `-dev` dogfood install" as its route while no repo text describes that install — the version convention is in the plugin-versioning rule and the procedure only in the developer's own notes; license: those two locations; the report now says where each half lives instead of naming a step nobody documented +- signal 2026-09-11 — a third round pays only as a short diff-scoped read: five Important repairs are one sentence or one command each, but two of them changed verification commands that must be run on this branch rather than read. The dispatcher ran both — the `diff` heredoc returns 0 against its own prescribed block, and the six stated wrap counts were measured at this commit — so what is left is Minor: descriptions, numbering and two wrapped lines From 269e9fbc74d0745987a09cd4c58735d0ab681b5c Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 23:05:19 +0200 Subject: [PATCH 057/120] docs: make the wrap check independent of shell word splitting --- ...2026-09-11-review-loop-errata-wave-four.md | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/plans/2026-09-11-review-loop-errata-wave-four.md b/docs/plans/2026-09-11-review-loop-errata-wave-four.md index 4375818..e384696 100644 --- a/docs/plans/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/plans/2026-09-11-review-loop-errata-wave-four.md @@ -1041,20 +1041,24 @@ stated counts rather than against a git baseline, which would move under it. ```bash -for pair in \ - "plugins/working-process/rules/workflow.md 3" \ - "plugins/working-process/rules/spec-plan-lifecycle.md 36" \ - "plugins/working-process/rules/ticket-frontmatter.md 1" \ - "plugins/working-process/skills/process-status/SKILL.md 3" \ - "docs/domain/glossary.md 50" \ - "plugins/working-process/README.md 7"; do - set -- $pair - now=$(awk 'length > 72 && $0 !~ /^\|/' "$1" | wc -l) - printf '%-62s was %-3s now %s\n' "$1" "$2" "$now" -done +while read -r f want; do + now=$(awk 'length > 72 && $0 !~ /^\|/' "$f" | wc -l) + printf '%-58s was %-3s now %s\n' "$f" "$want" "$now" +done <<'EOF' +plugins/working-process/rules/workflow.md 3 +plugins/working-process/rules/spec-plan-lifecycle.md 36 +plugins/working-process/rules/ticket-frontmatter.md 1 +plugins/working-process/skills/process-status/SKILL.md 3 +docs/domain/glossary.md 50 +plugins/working-process/README.md 7 +EOF awk 'length > 72 && $0 !~ /^\|/' plugins/working-process/rules/propagation-duties.md | wc -l ``` +The loop reads its pairs rather than splitting a string: `set -- $pair` +would work in bash and silently fail in zsh, which does not word-split +an unquoted parameter, and this repo's sessions run zsh. + Expected: `now` equals `was` for all six — the replacement texts are wrapped, so none of them adds a long line — and `0` for the new rule file, whose table rows the `awk` skips. @@ -1151,4 +1155,5 @@ including the two that needed running rather than reading. - fixed 2026-09-11 — [Minor] two replacement blocks written in round one's wave exceeded the 72-column constraint the plan commits to, so Task 7 step 4 would have reported `workflow.md` growing and ordered a `style:` commit to repair a defect the plan prescribed verbatim; license: the constraint; both paragraphs are rewrapped inside their Replace blocks, and every Replace block was re-measured — the one line still over 72 is an existing line carried through unchanged - fixed 2026-09-11 — [Minor] the rewritten rule said "eight classes of change owe an enumeration. This rule lists them" and then listed nine rows, because one duty answers two different edits — a counter that does not re-derive, inside the rule whose duty 4 is counters; license: the card's eight duties against the table's nine triggers; the sentence now says eight duties keyed by nine edits - fixed 2026-09-11 — [Minor] the delivery gap named "a release, or a `-dev` dogfood install" as its route while no repo text describes that install — the version convention is in the plugin-versioning rule and the procedure only in the developer's own notes; license: those two locations; the report now says where each half lives instead of naming a step nobody documented +- hit fixed 2026-09-11 — the wrap check round two rebuilt used `for pair in " "` with `set -- $pair`, which relies on word splitting that zsh does not do — run on this host every file name arrived with its count appended and `awk` failed on all six, so the step would have died at the implementer's shell; the gate before this round returned `CLEAN` without running it, and the dispatcher found it by running it; the loop now reads its pairs from a heredoc, verified on this host, and the step says why - signal 2026-09-11 — a third round pays only as a short diff-scoped read: five Important repairs are one sentence or one command each, but two of them changed verification commands that must be run on this branch rather than read. The dispatcher ran both — the `diff` heredoc returns 0 against its own prescribed block, and the six stated wrap counts were measured at this commit — so what is left is Minor: descriptions, numbering and two wrapped lines From 1d0a9538f46325f4af986a7222fb05bea136829d Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 23:13:01 +0200 Subject: [PATCH 058/120] docs: apply plan-adversary round three to the errata wave-four plan --- ...2026-09-11-review-loop-errata-wave-four.md | 65 +++++++++++++------ 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/docs/plans/2026-09-11-review-loop-errata-wave-four.md b/docs/plans/2026-09-11-review-loop-errata-wave-four.md index e384696..7218cfc 100644 --- a/docs/plans/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/plans/2026-09-11-review-loop-errata-wave-four.md @@ -161,9 +161,12 @@ echo "F $(n $W | grep -o 'or a confirming full-document round' | wc -l)" echo "G $(n $W | grep -o 'confirming round alone' | wc -l)" echo "H $(n $W | grep -o 'a confirming round on a spec' | wc -l)" echo "I $(n $W | grep -o 'The confirming-round arm' | wc -l)" +echo "J $(n $W | grep -o 'records their close, and no session' | wc -l)" +echo "K $(n $W | grep -o 'on a plan, once the confirming round has run' | wc -l)" ``` -Expected: `A 1`, `B 0`, `C 0`, `D 1`, `E 1`, `F 1`, `G 1`, `H 1`, `I 1`. +Expected: `A 1`, `B 0`, `C 0`, `D 1`, `E 1`, `F 1`, `G 1`, `H 1`, `I 1`, +`J 1`, `K 0`. If any differs, stop and report: the file is not the one this task was written against. @@ -294,12 +297,7 @@ Replace with: - [ ] **Step 5: Verify** -Run the Step 1 command again, with the two the qualifier moves: - -```bash -echo "J $(n $W | grep -o 'records their close, and no session' | wc -l)" -echo "K $(n $W | grep -o 'on a plan, once the confirming round has run' | wc -l)" -``` +Run the Step 1 command again. Expected: `A 0`, `B 1`, `C 1`, `D 0`, `E 1`, `F 0`, `G 0`, `H 0`, `I 0`, `J 0`, `K 1`. @@ -409,19 +407,22 @@ substitute pass and no install nagging — the work proceeds normally. Feature work happens on a topic branch named `feature/-`, and `feature/` where the ticket is `none`. The ticket is the reference the ticket-frontmatter -rule defines, reduced to what a branch name should carry: a Jira key as -it stands (`feature/ABC-123-short-name`), a GitHub or GitLab issue as -its number alone (`feature/123-short-name`). Git would accept `#` and -`/` in a ref; the convention drops them anyway, because a `#` is awkward -to type unquoted in a shell and a `/` adds a hierarchy level that the -`feature/-` shape does not parse. +rule defines, carried plainly: a Jira key as it stands +(`feature/ABC-123-short-name`), a same-repo GitHub or GitLab issue as +its number without the `#` (`feature/123-short-name`). The convention +states the shape and does not argue for it — git would accept a `#` or a +second `/` in a ref, and two attempts at justifying their absence +shipped claims that did not hold. The branch of a worktree created with a generated name is renamed to this shape before its first commit, so the branch a reader sees is the branch the convention names; the worktree's own directory is a separate name and this convention does not govern it. The work's spec and plan -record the result in their `branch:` field, and the ticket rule reads -the ticket back out of it. +record the result in their `branch:` field — the topic branch, not the +`.docs` branch a review loop's per-round commits use, which the +spec-plan-lifecycle rule names and which is a sibling of it rather than +a second topic branch. The ticket rule reads the ticket back out of the +recorded name. ## Dispatching a verdict agent ``` @@ -911,7 +912,7 @@ literal; the prose says "the domain directory" for exactly that reason `claude plugin validate` does not check `rules/` — a rule file is not a plugin component — so an unquoted `: ` in a scalar fails silently at load time and nothing in Task 7 would catch it. Read the block back and -parse it: +compare it: The check compares the written block against the prescribed one literally. It uses no YAML parser: `python3 -c 'import yaml'` fails on @@ -1034,15 +1035,20 @@ process under them while believing this wave was installed. - [ ] **Step 4: Confirm the wrap width did not worsen** The constraint is "match the surrounding paragraph", not a hard column: -four of these files already carry long lines this wave does not touch, -and `docs/domain/glossary.md` gained one more from the wave's own +every one of these files already carries long lines this wave does not +touch, and `docs/domain/glossary.md` gained one more from the wave's own authoring before implementation began. So the check compares against stated counts rather than against a git baseline, which would move under it. +Both `awk` calls are pinned to a UTF-8 locale. `length` counts bytes +under `LC_ALL=C`, and these files are full of em dashes, so the same +command reports 19 for `workflow.md` instead of 3 — the six stated +numbers are character counts and only hold as such. + ```bash while read -r f want; do - now=$(awk 'length > 72 && $0 !~ /^\|/' "$f" | wc -l) + now=$(LC_ALL=C.UTF-8 awk 'length > 72 && $0 !~ /^\|/' "$f" | wc -l) printf '%-58s was %-3s now %s\n' "$f" "$want" "$now" done <<'EOF' plugins/working-process/rules/workflow.md 3 @@ -1052,7 +1058,7 @@ plugins/working-process/skills/process-status/SKILL.md 3 docs/domain/glossary.md 50 plugins/working-process/README.md 7 EOF -awk 'length > 72 && $0 !~ /^\|/' plugins/working-process/rules/propagation-duties.md | wc -l +LC_ALL=C.UTF-8 awk 'length > 72 && $0 !~ /^\|/' plugins/working-process/rules/propagation-duties.md | wc -l ``` The loop reads its pairs rather than splitting a string: `set -- $pair` @@ -1157,3 +1163,22 @@ including the two that needed running rather than reading. - fixed 2026-09-11 — [Minor] the delivery gap named "a release, or a `-dev` dogfood install" as its route while no repo text describes that install — the version convention is in the plugin-versioning rule and the procedure only in the developer's own notes; license: those two locations; the report now says where each half lives instead of naming a step nobody documented - hit fixed 2026-09-11 — the wrap check round two rebuilt used `for pair in " "` with `set -- $pair`, which relies on word splitting that zsh does not do — run on this host every file name arrived with its count appended and `awk` failed on all six, so the step would have died at the implementer's shell; the gate before this round returned `CLEAN` without running it, and the dispatcher found it by running it; the loop now reads its pairs from a heredoc, verified on this host, and the step says why - signal 2026-09-11 — a third round pays only as a short diff-scoped read: five Important repairs are one sentence or one command each, but two of them changed verification commands that must be run on this branch rather than read. The dispatcher ran both — the `diff` heredoc returns 0 against its own prescribed block, and the six stated wrap counts were measured at this commit — so what is left is Minor: descriptions, numbering and two wrapped lines + +### 2026-09-11 — plan-adversary, fable 5.1, blocking (round 3, diff-scoped) + +Scoped to round two's wave, as that round's stop signal asked. The +reviewer ran both repaired commands on this host rather than reading +them, and checked its other claims by command too. One correction to the +brief it was given: the dispatcher told it the round cap was spent, which +was wrong — the developer answered before each of rounds 2 and 3, so each +reset the count. The cap has one round left; `blocking` is what stops the +loop here, not the cap. + +- fixed 2026-09-11 — [Important] the six stated wrap counts hold only under a UTF-8 locale: `awk 'length'` counts bytes under `LC_ALL=C`, and these files are full of em dashes, so the same command reports 19 for `workflow.md` against the stated 3, 57 against 36 for the lifecycle rule and 72 against 50 for the glossary — the step would then report growth on text the plan prescribes verbatim and order the rewrap it forbids, which is the failure round two repaired for a different cause; license: the measurement, run both ways at this commit; both `awk` calls are now pinned to `LC_ALL=C.UTF-8` and the step says the numbers are character counts +- fixed 2026-09-11 — [Important] the branch convention's second justification was also false: `bash -c 'echo feature/#123-short-name'` and `zsh -fc` both print the name, and only zsh with `extendedglob` set fails, so "awkward to type unquoted in a shell" does not hold either; license: those three runs; the convention now states the shape and argues for it not at all, and says why — two attempts at a reason shipped claims that did not hold, and a naming convention does not owe one +- held — [Important] the convention reduces "a GitHub or GitLab issue" to its number alone, while the ticket rule defines that phrase as `#123` **or** `org/repo#123`, so a cross-repo ticket lands as `feature/123-short-name` and the read-back turns it into `"#123"` — a same-repo reference to another repo's issue; question: what branch shape does a cross-repo issue take?; options: (a) it takes the `feature/` form that `ticket: none` uses, and the ticket comes from conversation, which makes the read-back sound — the session's recommendation; (b) the branch carries a marker the reader can key on, which costs a new convention; counter: the spec's W6 names only two cases and decides neither, so picking one here would settle a convention the spec left open +- fixed 2026-09-11 — [Minor] Task 6 step 5 still opened "Read the block back and parse it" one paragraph before "It uses no YAML parser" — a leftover of the version round two removed; license: that removal; the clause now says "compare it" +- fixed 2026-09-11 — [Minor] Task 7 step 4 said "four of these files already carry long lines" where all six do — a counter that does not re-derive, in the step the new rule's duty 4 is about; license: the six measured counts; it now says every one +- fixed 2026-09-11 — [Minor] checks `J` and `K`, moved into Task 1 by round one, had after values and no before values, against the Global Constraint that every step states both; license: that constraint; both `echo` lines moved into step 1 and their before values are measured and stated, `J 1` and `K 0` +- fixed 2026-09-11 — [Minor] the branch-naming section, read alone, left no room for the `.docs` branch this very work runs on, so the plan's own `branch:` field looked like a violation of the convention it ships; license: the spec-plan-lifecycle rule, which defines that branch as a sibling of the topic branch rather than a second one; the section now says the field records the topic branch and names the document branch as the sibling it is +- signal 2026-09-11 — a fourth round pays only as a few minutes on three sentences and one command line, and after this wave two of those three are settled; the third is the held question, which no round can answer. The residue belongs to the full-document confirming round the plan owes before its loop closes, which will read these repairs in place rather than in isolation From 4a3e6bd22f60c5155080a45e263712c7bde3825c Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Fri, 11 Sep 2026 23:19:03 +0200 Subject: [PATCH 059/120] docs: cut the branch-naming item back to what its spec item asks for --- ...2026-09-11-review-loop-errata-wave-four.md | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/docs/plans/2026-09-11-review-loop-errata-wave-four.md b/docs/plans/2026-09-11-review-loop-errata-wave-four.md index 7218cfc..208aec4 100644 --- a/docs/plans/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/plans/2026-09-11-review-loop-errata-wave-four.md @@ -405,14 +405,10 @@ substitute pass and no install nagging — the work proceeds normally. ## Branch naming Feature work happens on a topic branch named -`feature/-`, and `feature/` where the -ticket is `none`. The ticket is the reference the ticket-frontmatter -rule defines, carried plainly: a Jira key as it stands -(`feature/ABC-123-short-name`), a same-repo GitHub or GitLab issue as -its number without the `#` (`feature/123-short-name`). The convention -states the shape and does not argue for it — git would accept a `#` or a -second `/` in a ref, and two attempts at justifying their absence -shipped claims that did not hold. +`feature/-`, where `` is the work's tracker +reference as the ticket-frontmatter rule defines it, and +`feature/` where there is none. That rule owns what a ticket +value is; this one owns only the shape of the name. The branch of a worktree created with a generated name is renamed to this shape before its first commit, so the branch a reader sees is the @@ -447,11 +443,9 @@ for one unit of work — a plan inherits its spec's ticket, and artifacts of the same session reuse the established value. The branch naming convention that produces the names this order reads is the workflow rule's, which is where a branch is cut — before any `docs/` file of that -work exists, and so before this rule loads. Reading a name back: a -Jira-shaped segment is the key itself; a bare leading number is a -GitHub or GitLab issue and becomes `"#"`, quoted as the Values -section requires; an `org/repo#123` reference is not carried by the -convention, so it comes from the next source in the order above. +work exists, and so before this rule loads. Where a branch name yields +no usable reference, the order above simply falls through to its next +source, as it does for any work whose branch carries none. ``` - [ ] **Step 5: Verify** @@ -1176,9 +1170,10 @@ loop here, not the cap. - fixed 2026-09-11 — [Important] the six stated wrap counts hold only under a UTF-8 locale: `awk 'length'` counts bytes under `LC_ALL=C`, and these files are full of em dashes, so the same command reports 19 for `workflow.md` against the stated 3, 57 against 36 for the lifecycle rule and 72 against 50 for the glossary — the step would then report growth on text the plan prescribes verbatim and order the rewrap it forbids, which is the failure round two repaired for a different cause; license: the measurement, run both ways at this commit; both `awk` calls are now pinned to `LC_ALL=C.UTF-8` and the step says the numbers are character counts - fixed 2026-09-11 — [Important] the branch convention's second justification was also false: `bash -c 'echo feature/#123-short-name'` and `zsh -fc` both print the name, and only zsh with `extendedglob` set fails, so "awkward to type unquoted in a shell" does not hold either; license: those three runs; the convention now states the shape and argues for it not at all, and says why — two attempts at a reason shipped claims that did not hold, and a naming convention does not owe one -- held — [Important] the convention reduces "a GitHub or GitLab issue" to its number alone, while the ticket rule defines that phrase as `#123` **or** `org/repo#123`, so a cross-repo ticket lands as `feature/123-short-name` and the read-back turns it into `"#123"` — a same-repo reference to another repo's issue; question: what branch shape does a cross-repo issue take?; options: (a) it takes the `feature/` form that `ticket: none` uses, and the ticket comes from conversation, which makes the read-back sound — the session's recommendation; (b) the branch carries a marker the reader can key on, which costs a new convention; counter: the spec's W6 names only two cases and decides neither, so picking one here would settle a convention the spec left open +- fixed 2026-09-11 — [Important] the convention reduced "a GitHub or GitLab issue" to its number alone while the ticket rule defines that phrase as `#123` **or** `org/repo#123`, so a cross-repo ticket would land as `feature/123-short-name` and read back as a same-repo reference; license: the spec's W6, which asks for four things — the two name shapes, the rename before the first commit, and the `branch:` record — and for no encoding rule at all; the developer asked why a branch convention was deciding how to write a GitHub ticket, and the answer was that round one's F4 admitted material the spec never scoped, which then cost rounds two and three three findings between them. W6 is cut back to the spec: the shape names `` and defers to the ticket rule for what a ticket value is, and the pointer says only that the sourcing order falls through when a branch yields no reference — which is what that order already did - fixed 2026-09-11 — [Minor] Task 6 step 5 still opened "Read the block back and parse it" one paragraph before "It uses no YAML parser" — a leftover of the version round two removed; license: that removal; the clause now says "compare it" - fixed 2026-09-11 — [Minor] Task 7 step 4 said "four of these files already carry long lines" where all six do — a counter that does not re-derive, in the step the new rule's duty 4 is about; license: the six measured counts; it now says every one - fixed 2026-09-11 — [Minor] checks `J` and `K`, moved into Task 1 by round one, had after values and no before values, against the Global Constraint that every step states both; license: that constraint; both `echo` lines moved into step 1 and their before values are measured and stated, `J 1` and `K 0` - fixed 2026-09-11 — [Minor] the branch-naming section, read alone, left no room for the `.docs` branch this very work runs on, so the plan's own `branch:` field looked like a violation of the convention it ships; license: the spec-plan-lifecycle rule, which defines that branch as a sibling of the topic branch rather than a second one; the section now says the field records the topic branch and names the document branch as the sibling it is -- signal 2026-09-11 — a fourth round pays only as a few minutes on three sentences and one command line, and after this wave two of those three are settled; the third is the held question, which no round can answer. The residue belongs to the full-document confirming round the plan owes before its loop closes, which will read these repairs in place rather than in isolation +- signal 2026-09-11 — a fourth round pays only as a few minutes on three sentences and one command line, and after this wave all three are settled: the third was dissolved rather than answered, by cutting the text that raised it. The residue belongs to the full-document confirming round the plan owes before its loop closes, which will read these repairs in place rather than in isolation +- signal 2026-09-11 — a scope note the dispatcher owes the next reader: W6 grew past its spec item at round one and shrank back at round three. Three findings across three rounds were spent on text the spec never asked for, and none of them was wrong — the cost was admitting the scope, not reviewing it From 92fe143c1d7acc48ba2dbb21ff85f43019ae9015 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Sat, 12 Sep 2026 09:40:21 +0200 Subject: [PATCH 060/120] docs: apply the confirming round to the errata wave-four plan --- ...2026-09-11-review-loop-errata-wave-four.md | 90 +++++++++++++++---- 1 file changed, 71 insertions(+), 19 deletions(-) diff --git a/docs/plans/2026-09-11-review-loop-errata-wave-four.md b/docs/plans/2026-09-11-review-loop-errata-wave-four.md index 208aec4..f317d83 100644 --- a/docs/plans/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/plans/2026-09-11-review-loop-errata-wave-four.md @@ -2,7 +2,7 @@ ticket: none date: 2026-09-11 status: draft -adversary: blocking +adversary: concerns spec: ../specs/2026-09-11-review-loop-errata-wave-four.md branch: feature/process-wave-four base: develop @@ -77,7 +77,13 @@ the reason where the disagreement lives. by the edit an author just made") and the citation by duty number. A table is the shortest form that keeps trigger, enumeration and duty number on one row. Task 6 carries the reason. -3. **W7's prose never writes the literal `docs/domain/**`.** The spec's +3. **The spec disagrees with itself about the glossary, and the plan + follows its body.** The Scope preamble says two glossary changes are + already applied and one is owed; W1's body says the **Round heading** + entry "gains the continuity half too". Measured at authoring: that + clause is not in the file, so the preamble's count is one short and + the body is right. Task 4 step 2 writes it and Task 7 sweeps it. +4. **W7's prose never writes the literal `docs/domain/**`.** The spec's own check expects exactly one line matching it, and the frontmatter entry is that line; a second mention in prose would make the check read 2 where the spec says 1. Task 6 says "the domain directory" @@ -406,9 +412,10 @@ substitute pass and no install nagging — the work proceeds normally. Feature work happens on a topic branch named `feature/-`, where `` is the work's tracker -reference as the ticket-frontmatter rule defines it, and -`feature/` where there is none. That rule owns what a ticket -value is; this one owns only the shape of the name. +reference and `` says what the work is, and +`feature/` where there is no ticket. How a tracker's +reference is spelled in a branch name is the project's own — this +convention fixes the shape and not the spelling. The branch of a worktree created with a generated name is renamed to this shape before its first commit, so the branch a reader sees is the @@ -417,8 +424,8 @@ name and this convention does not govern it. The work's spec and plan record the result in their `branch:` field — the topic branch, not the `.docs` branch a review loop's per-round commits use, which the spec-plan-lifecycle rule names and which is a sibling of it rather than -a second topic branch. The ticket rule reads the ticket back out of the -recorded name. +a second topic branch. The ticket rule's sourcing order reads the +current branch name first when a new document needs a ticket. ## Dispatching a verdict agent ``` @@ -490,9 +497,10 @@ echo "C $(n $L | grep -o 'continue across loops' | wc -l)" echo "D $(n $L | grep -o 'where that heading is an .LGTM.' | wc -l)" echo "E $(n $L | grep -o 'wait for the confirming round' | wc -l)" echo "F $(n $L | grep -o 'never stands in for the' | wc -l)" +echo "G $(n $L | grep -o 'feature/-/docs' | wc -l)" ``` -Expected: `A 1`, `B 0`, `C 0`, `D 0`, `E 0`, `F 0`. +Expected: `A 1`, `B 0`, `C 0`, `D 0`, `E 0`, `F 0`, `G 0`. - [ ] **Step 2: Widen the Unresolved verdict owner leg** @@ -579,7 +587,28 @@ Replace with: frontmatter, where that entry is the only lever left. ``` -- [ ] **Step 5: Part the round sha from the integrity hash** +- [ ] **Step 5: Match the document-branch placeholder to the convention** + +W6 mints `feature/-` for the topic branch, and this +rule writes the same slot as `feature/-` inside its +document-branch paragraph. Two rules of one payload naming one slot +differently is the changed-interface class the wave's own new rule tells +an author to enumerate, so the older spelling joins the newer. + +Find: + +``` +so `feature/-/docs` cannot exist while its parent does; the +``` + +Replace with: + +``` +so `feature/-/docs` cannot exist while its parent +does; the +``` + +- [ ] **Step 6: Part the round sha from the integrity hash** In the per-round-commits section, find the final paragraph: @@ -610,13 +639,13 @@ do, which is what the sentence says; the spec named only "the per-round-commits paragraph", and the integrity audit reported that ambiguity (deviation 1). -- [ ] **Step 6: Verify** +- [ ] **Step 7: Verify** Run the Step 1 command again. -Expected: `A 0`, `B 1`, `C 1`, `D 1`, `E 1`, `F 1`. +Expected: `A 0`, `B 1`, `C 1`, `D 1`, `E 1`, `F 1`, `G 1`. -- [ ] **Step 7: Commit** +- [ ] **Step 8: Commit** ```bash git add plugins/working-process/rules/spec-plan-lifecycle.md @@ -756,13 +785,17 @@ Replace with: published later works the same way. Name the field on the line, so the developer can see which class went quiet. -Where two confirmed hits share one document, say so on one line and -point at the owner legs of the classes that produced them. The line is -the mapping read back — two hits, one file — and nothing more: it does -not say the duplicate is deliberate, and it does not explain how the two -relate, because both are process knowledge and this skill carries none. -A reader who wants the relationship finds it in the legs the line points -at. +Where two confirmed hits share one document, add one line beneath that +document's per-hit lines, naming the classes and pointing at their owner +legs: + + - two classes hit this document; one move may close both — see their owner legs + +The line is the mapping read back — two hits, one file — and nothing +more: it does not say the duplicate is deliberate, and it does not +explain how the two relate, because both are process knowledge and this +skill carries none. A reader who wants the relationship finds it in the +legs the line points at. ``` - [ ] **Step 3: Verify** @@ -973,6 +1006,7 @@ echo "W1n $(n $L | grep -o 'wait for the confirming round' | wc -l) want 1" echo "W1o $(grep -c '^\*\*Confirming round\*\*:' $G) want 1 (invariant)" echo "W1p $(n $G | grep -o 'inherits the gating' | wc -l) want 1 (invariant)" echo "W1q $(n $G | grep -o 'A plan already .implemented. owes none' | wc -l) want 1 (invariant)" +echo "W1r $(n $G | grep -o 'continue across loops' | wc -l) want 1" echo "W2a $(n $W | grep -o 'unbounded by the cap' | wc -l) want 1" echo "W2b $(n $W | grep -o 'records their close, and no session' | wc -l) want 0" echo "W2c $(n $W | grep -o 'on a plan, once the confirming round has run' | wc -l) want 1" @@ -1177,3 +1211,21 @@ loop here, not the cap. - fixed 2026-09-11 — [Minor] the branch-naming section, read alone, left no room for the `.docs` branch this very work runs on, so the plan's own `branch:` field looked like a violation of the convention it ships; license: the spec-plan-lifecycle rule, which defines that branch as a sibling of the topic branch rather than a second one; the section now says the field records the topic branch and names the document branch as the sibling it is - signal 2026-09-11 — a fourth round pays only as a few minutes on three sentences and one command line, and after this wave all three are settled: the third was dissolved rather than answered, by cutting the text that raised it. The residue belongs to the full-document confirming round the plan owes before its loop closes, which will read these repairs in place rather than in isolation - signal 2026-09-11 — a scope note the dispatcher owes the next reader: W6 grew past its spec item at round one and shrank back at round three. Three findings across three rounds were spent on text the spec never asked for, and none of them was wrong — the cost was admitting the scope, not reviewing it + +### 2026-09-12 — plan-adversary, fable 5.1, concerns (round 4, full-document) + +The confirming round the plan owed before its loop could close, and the +first non-`blocking` verdict it has returned. The gate ran full-scope +first and returned `CLEAN`; the reviewer then re-ran the plan's own +commands rather than reading them, and reported finding no disagreement +between the Order, Self-review, Deviations sections and the tasks — +after the scope cut the document says one thing. Every citation checked +here, including the branch this repo carries. + +- fixed 2026-09-12 — [Important] the round-three cut left a subordinate clause, "where `` is the work's tracker reference as the ticket-frontmatter rule defines it", which binds the slot to that rule's value forms and so answers the developer's question in the opposite direction: this repo's own `feature/12-python-content` becomes non-compliant with the rule the repo ships, and a session obeying "renamed before its first commit" would rewrite it to carry a `#`; license: this repo's `CLAUDE.md`, whose example is `feature/6-salesforce-standards`, and the spec's W6, which names the shape and no value clause; the section now says the spelling is the project's own and fixes only the shape +- fixed 2026-09-12 — [Minor] "The ticket rule reads the ticket back out of the recorded name" overstates that rule, whose sourcing order reads the current branch name as the first of three sources for a new document, before any `branch:` field exists; license: that order; the sentence now says what it does +- held — [Minor] after the edit the resolution-annotation bullet says a resolution carries "a body note saying what resolved them" and that on an implemented plan the frontmatter entry "is the only lever left", while the implemented-document bullet excepts ledger process-event annotations from the frontmatter-only rule — so the shipped rule leaves open whether the body note is written; question: on an implemented plan, is the resolution's body note written as a process record, or is the frontmatter annotation the whole close?; options: (a) say the body note rides the process-event exception and is written, which keeps every resolution's reasoning in the ledger — the session's recommendation; (b) drop "where that entry is the only lever left", leaving the bullet silent on the body note; counter: the wording is inherited from decision 5, which the developer ruled, so choosing here would re-decide a ruling rather than apply one +- fixed 2026-09-12 — [Minor] W4 described the co-firing line where the integrity audit had handed the plan "W4's literal line shape" as its agenda, and never said whether the line joins or replaces the per-hit lines the same step prescribes; license: that agenda item, recorded in the spec's ledger; the step now says the line goes beneath the per-hit lines and shows it +- fixed 2026-09-12 — [Minor] the spec disagrees with itself about the glossary — its Scope preamble counts the **Round heading** continuity clause as already applied while W1's body says the entry gains it, and the clause is measurably absent — and the plan followed the body without recording the disagreement or sweeping the edit; license: the measurement; a fourth Deviation records it and Task 7 sweeps it as `W1r` +- fixed 2026-09-12 — [Minor] W6 mints `feature/-` while `spec-plan-lifecycle.md` writes the same slot as `feature/-` in its document-branch paragraph, so the wave would ship two rules of one payload naming one slot differently — the changed-interface class the wave's own new rule tells an author to enumerate; license: that duty and the spec's choice of `` for a tracker-agnostic rule; Task 3 gains a step that matches the older spelling to the newer +- signal 2026-09-12 — another round would not repay: the one Important is a clause cut whose correctness is checked by reading `CLAUDE.md` beside the text, which needs no fresh context, and the rest is wording, one ruling and one Deviations entry. With this heading full-document the loop may close by annotation once the held line is answered From bc7fe56e6f5ed3abf523ef4c2abe909ffeb46c4c Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Sat, 12 Sep 2026 09:41:43 +0200 Subject: [PATCH 061/120] docs: rule that an implemented plan's resolution keeps its body note --- docs/plans/2026-09-11-review-loop-errata-wave-four.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/plans/2026-09-11-review-loop-errata-wave-four.md b/docs/plans/2026-09-11-review-loop-errata-wave-four.md index f317d83..3635491 100644 --- a/docs/plans/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/plans/2026-09-11-review-loop-errata-wave-four.md @@ -583,8 +583,10 @@ Replace with: the confirming round: neither is written while the latest round heading is diff-scoped, unless the plan is already `implemented`, where the annotation — or the developer's adjudication — is written - citing that standing decision, such a document being amended in - frontmatter, where that entry is the only lever left. + citing that standing decision. Its body note is written too: a note + recording what resolved a verdict is a process record, which the + implemented-document rule above excepts from its frontmatter-only + clause, as it excepts `, debt discharged `. ``` - [ ] **Step 5: Match the document-branch placeholder to the convention** @@ -1224,8 +1226,8 @@ here, including the branch this repo carries. - fixed 2026-09-12 — [Important] the round-three cut left a subordinate clause, "where `` is the work's tracker reference as the ticket-frontmatter rule defines it", which binds the slot to that rule's value forms and so answers the developer's question in the opposite direction: this repo's own `feature/12-python-content` becomes non-compliant with the rule the repo ships, and a session obeying "renamed before its first commit" would rewrite it to carry a `#`; license: this repo's `CLAUDE.md`, whose example is `feature/6-salesforce-standards`, and the spec's W6, which names the shape and no value clause; the section now says the spelling is the project's own and fixes only the shape - fixed 2026-09-12 — [Minor] "The ticket rule reads the ticket back out of the recorded name" overstates that rule, whose sourcing order reads the current branch name as the first of three sources for a new document, before any `branch:` field exists; license: that order; the sentence now says what it does -- held — [Minor] after the edit the resolution-annotation bullet says a resolution carries "a body note saying what resolved them" and that on an implemented plan the frontmatter entry "is the only lever left", while the implemented-document bullet excepts ledger process-event annotations from the frontmatter-only rule — so the shipped rule leaves open whether the body note is written; question: on an implemented plan, is the resolution's body note written as a process record, or is the frontmatter annotation the whole close?; options: (a) say the body note rides the process-event exception and is written, which keeps every resolution's reasoning in the ledger — the session's recommendation; (b) drop "where that entry is the only lever left", leaving the bullet silent on the body note; counter: the wording is inherited from decision 5, which the developer ruled, so choosing here would re-decide a ruling rather than apply one +- fixed 2026-09-12 — [Minor] after the edit the resolution-annotation bullet said a resolution carries "a body note saying what resolved them" and that on an implemented plan the frontmatter entry "is the only lever left", while the implemented-document bullet excepts ledger process records from its frontmatter-only clause — so the shipped rule left open whether the note is written; ruling: 2026-09-12; the clause now says the body note is written too, on that same exception, and names `, debt discharged ` as the exception's other instance - fixed 2026-09-12 — [Minor] W4 described the co-firing line where the integrity audit had handed the plan "W4's literal line shape" as its agenda, and never said whether the line joins or replaces the per-hit lines the same step prescribes; license: that agenda item, recorded in the spec's ledger; the step now says the line goes beneath the per-hit lines and shows it - fixed 2026-09-12 — [Minor] the spec disagrees with itself about the glossary — its Scope preamble counts the **Round heading** continuity clause as already applied while W1's body says the entry gains it, and the clause is measurably absent — and the plan followed the body without recording the disagreement or sweeping the edit; license: the measurement; a fourth Deviation records it and Task 7 sweeps it as `W1r` - fixed 2026-09-12 — [Minor] W6 mints `feature/-` while `spec-plan-lifecycle.md` writes the same slot as `feature/-` in its document-branch paragraph, so the wave would ship two rules of one payload naming one slot differently — the changed-interface class the wave's own new rule tells an author to enumerate; license: that duty and the spec's choice of `` for a tracker-agnostic rule; Task 3 gains a step that matches the older spelling to the newer -- signal 2026-09-12 — another round would not repay: the one Important is a clause cut whose correctness is checked by reading `CLAUDE.md` beside the text, which needs no fresh context, and the rest is wording, one ruling and one Deviations entry. With this heading full-document the loop may close by annotation once the held line is answered +- signal 2026-09-12 — another round would not repay: the one Important is a clause cut whose correctness is checked by reading `CLAUDE.md` beside the text, which needs no fresh context, and the rest is wording, one ruling and one Deviations entry. With this heading full-document the loop may close by annotation From 5ea3f6c961a77ba44948a507b65d608c62981d3b Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 09:58:49 +0200 Subject: [PATCH 062/120] docs: close the errata wave-four plan's review loop and approve it --- .../2026-09-11-review-loop-errata-wave-four.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/plans/2026-09-11-review-loop-errata-wave-four.md b/docs/plans/2026-09-11-review-loop-errata-wave-four.md index 3635491..69d450e 100644 --- a/docs/plans/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/plans/2026-09-11-review-loop-errata-wave-four.md @@ -1,8 +1,8 @@ --- ticket: none date: 2026-09-11 -status: draft -adversary: concerns +status: approved +adversary: concerns (resolved 2026-09-14) spec: ../specs/2026-09-11-review-loop-errata-wave-four.md branch: feature/process-wave-four base: develop @@ -1231,3 +1231,16 @@ here, including the branch this repo carries. - fixed 2026-09-12 — [Minor] the spec disagrees with itself about the glossary — its Scope preamble counts the **Round heading** continuity clause as already applied while W1's body says the entry gains it, and the clause is measurably absent — and the plan followed the body without recording the disagreement or sweeping the edit; license: the measurement; a fourth Deviation records it and Task 7 sweeps it as `W1r` - fixed 2026-09-12 — [Minor] W6 mints `feature/-` while `spec-plan-lifecycle.md` writes the same slot as `feature/-` in its document-branch paragraph, so the wave would ship two rules of one payload naming one slot differently — the changed-interface class the wave's own new rule tells an author to enumerate; license: that duty and the spec's choice of `` for a tracker-agnostic rule; Task 3 gains a step that matches the older spelling to the newer - signal 2026-09-12 — another round would not repay: the one Important is a clause cut whose correctness is checked by reading `CLAUDE.md` beside the text, which needs no fresh context, and the rest is wording, one ruling and one Deviations entry. With this heading full-document the loop may close by annotation + +The loop closes here. Four rounds — `blocking`, `blocking`, `blocking`, +`concerns` — returned thirty-three findings and every one is terminal: +thirty carry `license:`, three carry `ruling:`, and no `open` or `held` +line remains. The last round was full-document, so the confirming round +this plan owed is the round that produced the standing verdict rather +than one still due. Five propagation gates ran, two found hits and the +dispatcher found two more by running commands a gate only reads. + +What the verdict's `concerns` still names is the residue round four +judged not worth another read: wording, one Deviations entry, and the +ruling recorded above. Nothing in it blocks execution, which is why the +close is an annotation rather than a fresh round. From 89f86ce558bd8eba9c1440679b64b5674b3a94fd Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 10:19:20 +0200 Subject: [PATCH 063/120] feat(working-process): close a plan's loop only on a full-document round --- plugins/working-process/rules/workflow.md | 65 +++++++++++++++-------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/plugins/working-process/rules/workflow.md b/plugins/working-process/rules/workflow.md index aac42df..00afd3b 100644 --- a/plugins/working-process/rules/workflow.md +++ b/plugins/working-process/rules/workflow.md @@ -391,8 +391,9 @@ evidence to attack, never a defence to protect. by grade — and escalate with an offer of a fresh round instead of dispatching one. The escalation is a question, so the loop stays open until the developer answers it: the resolution annotation the - spec-plan-lifecycle rule defines records their close, and no session - writes it without their answer. + spec-plan-lifecycle rule defines records their close — on a plan, once + the confirming round has run — and no session writes it without their + answer. - Oscillation tripwire: a finding re-raised against a line carrying `license:` is never re-fixed autonomously. Two readings of one license are a contested reading, so it escalates as held, the flip named. A @@ -413,15 +414,15 @@ reviewed by the round that followed it. What that chain still owes differs by document. For a spec, the consumption gate before plan-writing offers the pair as -one question — an integrity audit or a confirming full-document round — -and never an offer followed by a re-offer of the option just declined. -When the `integrity-auditor` agent is absent the offer carries the -confirming round alone. The two arms cost differently and the offer says -so: an audit returns material for the dispatcher to dispose of and -leaves the verdict alone, while a confirming round on a spec is a new -loop's first round, since the spec's LGTM already closed its loop — it -mints its own verdict and stamps it, so a `concerns` there flips the -field back while plan-writing waits. The confirming-round arm therefore +one question — an integrity audit or a full-document round — and never +an offer followed by a re-offer of the option just declined. When the +`integrity-auditor` agent is absent the offer carries the full-document +round alone. The two arms cost differently and the offer says so: an +audit returns material for the dispatcher to dispose of and leaves the +verdict alone, while a full-document round on a spec is a new loop's +first round, since the spec's LGTM already closed its loop — it mints +its own verdict and stamps it, so a `concerns` there flips the field +back while plan-writing waits. The full-document-round arm therefore blocks plan-writing; the audit arm does not, and plan-writing follows its dispositions. @@ -432,18 +433,36 @@ diff-scoped LGTM heading, in the shape the spec-plan-lifecycle rule defines, and that annotation defeats the gate's re-ask. The other two paths write the same token. -For a plan the loop never terminates on a diff-scoped LGTM: one -full-document confirming round follows, and the confirming round's -verdict is the one stamped. That round runs under the loop's standing -consent like any other — the rules mandate it, so it is no decision of -the developer's and spends none of the round's one interruption. It -carries the single named exception to the relay-then-stamp order above — -a plan's diff-scoped LGTM is relayed and its round record written, and -only the frontmatter stamp waits for the confirming round. Recovery -therefore reads the ledger rather than the stamp: a plan whose latest -round heading is a diff-scoped LGTM that no later full-document round -follows is re-offered its confirming round at the document's next touch, -whatever the frontmatter says. +For a plan the loop closes only when the latest verdict round was +full-document: whatever ends its rounds while the latest round heading +is diff-scoped, one confirming full-document round follows, and that +round's verdict is the one stamped. A round already full-document owes +no successor — it is the close. The confirming round inherits the gating +of whatever ended the rounds. Where autonomy still stands — +a diff-scoped `LGTM`, or a `concerns` inside the cap — the rules mandate +the round, so it is no decision of the developer's and spends none of +the round's one interruption. After `blocking`, the round cap or the +all-Minor signal it is the developer's to order, because each of those +suspends autonomy by its own terminator, and a round the rules mandate +cannot outrank a terminator that stopped the loop. + +A session never judges that the rounds are ending. The terminators do, +as does the developer closing the loop. While none has fired and the +held set is empty, the next round is diff-scoped and the loop simply +continues; the confirming round is owed the moment one fires, which is +the moment a session would otherwise write the resolution annotation. + +The plan case carries the single named exception to the relay-then-stamp +order above — a plan's diff-scoped LGTM is relayed and its round record +written, and only the frontmatter stamp waits for the confirming round. +Recovery therefore reads the ledger rather than the stamp. A plan whose +latest round heading is a diff-scoped `LGTM` that no later full-document +round follows is re-offered its confirming round at the document's next +touch, whatever the frontmatter says, since that heading is itself an +end. A latest heading of any other verdict is not an end, so there the +round is re-offered when something tries to end the rounds — an +annotation, an adjudication, a `status` move — or when a terminator that +suspends autonomy has fired. Scoping never spans a close. An annotation close ends the loop, and a later round on the same document opens a new one, reading the whole From 1d14cc1f5933407442630af892e126410d2947a1 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 10:24:57 +0200 Subject: [PATCH 064/120] feat(working-process): bound the cap's promise and publish the branch-naming convention --- .../rules/ticket-frontmatter.md | 7 +++++- plugins/working-process/rules/workflow.md | 25 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/plugins/working-process/rules/ticket-frontmatter.md b/plugins/working-process/rules/ticket-frontmatter.md index eebdad4..3595fef 100644 --- a/plugins/working-process/rules/ticket-frontmatter.md +++ b/plugins/working-process/rules/ticket-frontmatter.md @@ -52,7 +52,12 @@ GitLab, or anything else; the field is always `ticket`. For a NEW document: branch name (`feature/ABC-123-...`) → conversation context → ask the developer once; no answer means `none`. Never ask twice for one unit of work — a plan inherits its spec's ticket, and artifacts -of the same session reuse the established value. +of the same session reuse the established value. The branch naming +convention that produces the names this order reads is the workflow +rule's, which is where a branch is cut — before any `docs/` file of that +work exists, and so before this rule loads. Where a branch name yields +no usable reference, the order above simply falls through to its next +source, as it does for any work whose branch carries none. When editing an existing `docs/` document that has no frontmatter, add the block as part of the edit — sourced from document content → branch → diff --git a/plugins/working-process/rules/workflow.md b/plugins/working-process/rules/workflow.md index 00afd3b..b65421b 100644 --- a/plugins/working-process/rules/workflow.md +++ b/plugins/working-process/rules/workflow.md @@ -120,6 +120,25 @@ run an explicit editing pass over the changed prose of an existing one. The pass binds wording, never decisions. Without the skill there is no substitute pass and no install nagging — the work proceeds normally. +## Branch naming + +Feature work happens on a topic branch named +`feature/-`, where `` is the work's tracker +reference and `` says what the work is, and +`feature/` where there is no ticket. How a tracker's +reference is spelled in a branch name is the project's own — this +convention fixes the shape and not the spelling. + +The branch of a worktree created with a generated name is renamed to +this shape before its first commit, so the branch a reader sees is the +branch the convention names; the worktree's own directory is a separate +name and this convention does not govern it. The work's spec and plan +record the result in their `branch:` field — the topic branch, not the +`.docs` branch a review loop's per-round commits use, which the +spec-plan-lifecycle rule names and which is a sibling of it rather than +a second topic branch. The ticket rule's sourcing order reads the +current branch name first when a new document needs a ticket. + ## Dispatching a verdict agent Dispatching a verdict agent (`architect`, `plan-adversary`), when @@ -385,7 +404,11 @@ evidence to attack, never a defence to protect. the cap is best-effort. A plan whose loop spent its three rounds therefore escalates once before its confirming round, which is the most expensive shape a round takes and the one a cap guarding spend - should guard first. + should guard first. A run of `blocking` verdicts is unbounded by the + cap, for the reason the cap is stated in: it counts autonomous rounds, + and `blocking` suspends autonomy, so every continuation after one is + the developer's own decision rather than a round the cap governs. + Consenting to three rounds consents to three autonomous ones. - All-Minor signal: two consecutive rounds whose findings are all Minor end the unattended run. Triage the round as always — by license, never by grade — and escalate with an offer of a fresh round instead of From 16f9344c20237b08cb4cc270e37841f3133d7e1b Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 10:29:51 +0200 Subject: [PATCH 065/120] feat(working-process): widen the plan exception, define the latest heading, part sha from hash --- .../rules/spec-plan-lifecycle.md | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/plugins/working-process/rules/spec-plan-lifecycle.md b/plugins/working-process/rules/spec-plan-lifecycle.md index 460d1f5..d5df639 100644 --- a/plugins/working-process/rules/spec-plan-lifecycle.md +++ b/plugins/working-process/rules/spec-plan-lifecycle.md @@ -53,7 +53,14 @@ base: master # optional: branch the topic branch was cut from whole value as usual. A `blocking` verdict the developer closes by explicit adjudication instead of a fresh round takes the same form with its own token — `architect: blocking (adjudicated 2026-08-17)` — and - the round's ledger record as that body note. + the round's ledger record as that body note. On a plan both wait for + the confirming round: neither is written while the latest round + heading is diff-scoped, unless the plan is already `implemented`, + where the annotation — or the developer's adjudication — is written + citing that standing decision. Its body note is written too: a note + recording what resolved a verdict is a process record, which the + implemented-document rule above excepts from its frontmatter-only + clause, as it excepts `, debt discharged `. - A verdict produced below the prescribed tier (the model-selection heuristic in the workflow rule) gains a companion `architect-fallback:` / `adversary-fallback:` field: the family alias of the model that @@ -132,6 +139,13 @@ and is omitted only on a round predating the distinction. A recovery reading keys on the absence of `full-document`, so a full-document round always says so. +The latest round heading is the one carrying the highest ordinal, +wherever it sits in the section: no order is prescribed for the blocks +and live ledgers run both ways. Ordinals run per document and per field, +and continue across loops — a later loop opens at the next number rather +than at one, which is what the round cap's derivation from the headings +already assumed. + Under the heading each finding takes one line, its disposition the leading token: @@ -375,12 +389,17 @@ leg, and the entries below that do so say it there. and nothing closed it. `rg -l --no-ignore --crlf '^\s*(architect|adversary): (blocking|concerns)$' docs/` Owner: a fresh round at the prescribed tier, or the resolution - annotation above — except on a plan whose latest round heading is a - diff-scoped `LGTM`, where the confirming full-document round closes - the verdict and the annotation may not, since a plan's loop never - terminates on that heading. Such a plan matches Chain debt as well: - one debt seen from two sides, both extinguished by that round, and the - duplicate is deliberate. + annotation above — except on a plan whose latest round heading is + diff-scoped, whatever its verdict, where the confirming full-document + round closes the verdict and the annotation may not, since a plan's + loop closes only on a full-document round. Such a plan matches Chain + debt as well where that heading is an `LGTM`: one debt seen from two + sides, both extinguished by that round, and the duplicate is + deliberate. On a plan already at `status: implemented` the round is + discharged by recorded decline without any dispatch, as the Chain debt + leg discharges its own debt — a session derives that decline under a + `concerns` heading, while a `blocking` one waits for the developer's + adjudication, which is theirs to make. - **Unfinished review-loop ledger** — a disposition line nobody closed: an `open` line whose remediation never ran, or a `held` line whose question still waits. @@ -492,7 +511,8 @@ it. The commits go to a local branch named `.docs`, never to the topic branch itself. Git refuses a ref nested under an existing branch, -so `feature/-/docs` cannot exist while its parent does; the +so `feature/-/docs` cannot exist while its parent +does; the suffix takes a dot because the branch convention already spends hyphens on name parts, where `-docs` would read as a topic about documenting. @@ -517,7 +537,11 @@ What per-round commits never do is replace the ledger. Git says which lines changed; the ledger says with what intent and on whose license, and a diff carries neither "narrowed the claim" nor a cited ADR. A `` clause may thin to a sentence where the commit carries -the detail; it does not go. +the detail; it does not go. Nor does a sha stand in for a hash: a round +heading that one day carried its commit's sha would name that round, +and it never stands in for the `integrity:` hash, which is computed from +the body and must work on a document nobody committed — the default +path, where no sha exists. Ticket value format, sourcing order, and backfill live in the ticket-frontmatter rule. From e823b78a1f859b37c3ee6db5617e18459a7c7828 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 10:36:00 +0200 Subject: [PATCH 066/120] docs(glossary): carry ordinal continuity and clear the Chain debt comma pile --- docs/domain/glossary.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/domain/glossary.md b/docs/domain/glossary.md index 9395214..9abc333 100644 --- a/docs/domain/glossary.md +++ b/docs/domain/glossary.md @@ -342,7 +342,8 @@ loop's derived state — round count, the all-Minor signal, the diff-scoped chain — is read by folding these. The latest round heading is the one carrying the highest ordinal, wherever it sits in the section: the rules prescribe no order for the blocks, and live ledgers -run both ways. +run both ways. Ordinals run per document and per field, and continue +across loops rather than restarting. _Avoid_: round title, round record (for the heading alone) **Disposition line**: @@ -366,9 +367,9 @@ responsibility for the part no round re-read — or explicitly decline to. The consumption gate owns it, not the loop, which owes nothing further once it terminates. Discharged three ways: an integrity audit, any later full-document round whatever its verdict, or the developer's recorded -decline — of the gate's pair offer, or, where the document is already -implemented and the gate never fired, of the question that offer would -have put. Recorded as `, debt discharged ` appended to that LGTM's +decline. What they decline is the gate's pair offer — or, where the +document is already implemented and the gate never fired, the question +that offer would have put. Recorded as `, debt discharged ` appended to that LGTM's round heading, and the record says only that it happened and when, never how or how well. Distinct from the diff-scoped chain itself, the round-one-plus-reviewed-waves structure the LGTM certifies. From 50eee1de2102e97c3b1c4c15ecfffe99436aab78 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 10:40:57 +0200 Subject: [PATCH 067/120] style(glossary): rewrap the Chain debt entry after the repunctuation --- docs/domain/glossary.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/domain/glossary.md b/docs/domain/glossary.md index 9abc333..3e869c8 100644 --- a/docs/domain/glossary.md +++ b/docs/domain/glossary.md @@ -369,10 +369,11 @@ once it terminates. Discharged three ways: an integrity audit, any later full-document round whatever its verdict, or the developer's recorded decline. What they decline is the gate's pair offer — or, where the document is already implemented and the gate never fired, the question -that offer would have put. Recorded as `, debt discharged ` appended to that LGTM's -round heading, and the record says only that it happened and when, never -how or how well. Distinct from the diff-scoped chain itself, the -round-one-plus-reviewed-waves structure the LGTM certifies. +that offer would have put. Recorded as `, debt discharged ` +appended to that LGTM's round heading, and the record says only that it +happened and when, never how or how well. Distinct from the diff-scoped +chain itself, the round-one-plus-reviewed-waves structure the LGTM +certifies. _Avoid_: chain accepted, chain closed, unclosed chain **Confirming round**: From 08ebd16be2b4f5f1cf9fcdc5e9721754f91ddc1e Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 10:43:58 +0200 Subject: [PATCH 068/120] feat(working-process): report two hits on one document as a shape --- .../working-process/skills/process-status/SKILL.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugins/working-process/skills/process-status/SKILL.md b/plugins/working-process/skills/process-status/SKILL.md index 603846b..656926e 100644 --- a/plugins/working-process/skills/process-status/SKILL.md +++ b/plugins/working-process/skills/process-status/SKILL.md @@ -74,6 +74,18 @@ the mapping, so you never need process knowledge to find it and a class published later works the same way. Name the field on the line, so the developer can see which class went quiet. +Where two confirmed hits share one document, add one line beneath that +document's per-hit lines, naming the classes and pointing at their owner +legs: + + - two classes hit this document; one move may close both — see their owner legs + +The line is the mapping read back — two hits, one file — and nothing +more: it does not say the duplicate is deliberate, and it does not +explain how the two relate, because both are process knowledge and this +skill carries none. A reader who wants the relationship finds it in the +legs the line points at. + Close the report with: - the rejected hits — how many, and in which documents — because From c84b8aea4c715213acdc7085d8f35d254a865742 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 10:50:51 +0200 Subject: [PATCH 069/120] docs: measure the wrap on the lines this wave adds, not on whole-file counts --- ...2026-09-11-review-loop-errata-wave-four.md | 54 +++++++------------ 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/docs/plans/2026-09-11-review-loop-errata-wave-four.md b/docs/plans/2026-09-11-review-loop-errata-wave-four.md index 69d450e..61a04fa 100644 --- a/docs/plans/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/plans/2026-09-11-review-loop-errata-wave-four.md @@ -1062,46 +1062,31 @@ this branch there. So `sync-rules` run today would report no drift and would re-copy the old rules, and the developer would keep running the process under them while believing this wave was installed. -- [ ] **Step 4: Confirm the wrap width did not worsen** +- [ ] **Step 4: Confirm no line this wave wrote breaks the wrap** -The constraint is "match the surrounding paragraph", not a hard column: -every one of these files already carries long lines this wave does not -touch, and `docs/domain/glossary.md` gained one more from the wave's own -authoring before implementation began. So the check compares against -stated counts rather than against a git baseline, which would move under -it. +The check reads the diff, not the files. A whole-file count of long +lines moves whenever anything else in the file moves, cannot tell this +wave's lines from the ones already there, and carries magic numbers that +go stale at the next unrelated edit — three ways of measuring something +other than what the constraint says. What the constraint says is that +the lines this wave writes are wrapped, so that is what is measured. -Both `awk` calls are pinned to a UTF-8 locale. `length` counts bytes -under `LC_ALL=C`, and these files are full of em dashes, so the same -command reports 19 for `workflow.md` instead of 3 — the six stated -numbers are character counts and only hold as such. +Lines indented four spaces or more are excluded: they are grammar +examples, a form the rules use throughout — `spec-plan-lifecycle.md` +carries four of them, up to 123 characters. Two-space bullet text stays +in scope, because that is where replacement prose lands. ```bash -while read -r f want; do - now=$(LC_ALL=C.UTF-8 awk 'length > 72 && $0 !~ /^\|/' "$f" | wc -l) - printf '%-58s was %-3s now %s\n' "$f" "$want" "$now" -done <<'EOF' -plugins/working-process/rules/workflow.md 3 -plugins/working-process/rules/spec-plan-lifecycle.md 36 -plugins/working-process/rules/ticket-frontmatter.md 1 -plugins/working-process/skills/process-status/SKILL.md 3 -docs/domain/glossary.md 50 -plugins/working-process/README.md 7 -EOF -LC_ALL=C.UTF-8 awk 'length > 72 && $0 !~ /^\|/' plugins/working-process/rules/propagation-duties.md | wc -l +BASE=$(git merge-base HEAD develop) +git diff "$BASE"..HEAD -- plugins/working-process docs/domain/glossary.md \ + | grep '^+' | grep -v '^+++' | sed 's/^+//' \ + | LC_ALL=C.UTF-8 awk 'length > 72 && $0 !~ /^ / {print "("length") "$0}' ``` -The loop reads its pairs rather than splitting a string: `set -- $pair` -would work in bash and silently fail in zsh, which does not word-split -an unquoted parameter, and this repo's sessions run zsh. - -Expected: `now` equals `was` for all six — the replacement texts are -wrapped, so none of them adds a long line — and `0` for the new rule -file, whose table rows the `awk` skips. - -On a count that grew, rewrap the offending paragraph in the file that -owns it and commit it as `style(working-process): rewrap `. Do not -reflow a paragraph this wave did not change. +Expected: no output. Any line printed is one this wave added that breaks +the wrap; rewrap it in the file that owns it and commit as +`style(working-process): rewrap `. Do not reflow a paragraph this +wave did not change — it will not appear here anyway. - [ ] **Step 5: Report the end state** @@ -1230,6 +1215,7 @@ here, including the branch this repo carries. - fixed 2026-09-12 — [Minor] W4 described the co-firing line where the integrity audit had handed the plan "W4's literal line shape" as its agenda, and never said whether the line joins or replaces the per-hit lines the same step prescribes; license: that agenda item, recorded in the spec's ledger; the step now says the line goes beneath the per-hit lines and shows it - fixed 2026-09-12 — [Minor] the spec disagrees with itself about the glossary — its Scope preamble counts the **Round heading** continuity clause as already applied while W1's body says the entry gains it, and the clause is measurably absent — and the plan followed the body without recording the disagreement or sweeping the edit; license: the measurement; a fourth Deviation records it and Task 7 sweeps it as `W1r` - fixed 2026-09-12 — [Minor] W6 mints `feature/-` while `spec-plan-lifecycle.md` writes the same slot as `feature/-` in its document-branch paragraph, so the wave would ship two rules of one payload naming one slot differently — the changed-interface class the wave's own new rule tells an author to enumerate; license: that duty and the spec's choice of `` for a tracker-agnostic rule; Task 3 gains a step that matches the older spelling to the newer +- fixed 2026-09-14 — [Important] Task 7's wrap check counted long lines per file against six stated numbers, which measures the wrong thing three ways: the count moves with any unrelated edit, it cannot separate this wave's lines from those already present, and the numbers go stale at the next change. It surfaced during execution, when Task 5 landed a prescribed indented example of 83 characters — a form the sibling rule uses four times, up to 123 — and the count went from 3 to 4 on a file whose deliverable was byte-exact; ruling: 2026-09-14, the developer's, that wrapping a line to fit a convention must not break a test and that the test was the thing mis-designed; the step now reads the diff and fails only on a line this wave added that exceeds 72 outside a four-space indent, which passes cleanly over every commit of the wave so far - signal 2026-09-12 — another round would not repay: the one Important is a clause cut whose correctness is checked by reading `CLAUDE.md` beside the text, which needs no fresh context, and the rest is wording, one ruling and one Deviations entry. With this heading full-document the loop may close by annotation The loop closes here. Four rounds — `blocking`, `blocking`, `blocking`, From b6c149510c20859c8c445897afde12a213847247 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 10:55:34 +0200 Subject: [PATCH 070/120] feat(working-process): ship the propagation duties as an author-facing rule --- plugins/working-process/README.md | 14 +++--- .../rules/propagation-duties.md | 46 +++++++++++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 plugins/working-process/rules/propagation-duties.md diff --git a/plugins/working-process/README.md b/plugins/working-process/README.md index ed155fc..a06efbc 100644 --- a/plugins/working-process/README.md +++ b/plugins/working-process/README.md @@ -181,13 +181,15 @@ new gates merely stay silent. ## Process rules -The plugin ships five rule files in `rules/` — the preferred workflow +The plugin ships six rule files in `rules/` — the preferred workflow (always loaded once installed), spec/plan frontmatter and lifecycle, -Process directory conventions, ticket frontmatter, and the review-report -contract (`review-reports.md`: where a code-review run writes its -Review report and what shape it takes; domain review skills locate the -installed contract via its contract probe — the project-level then -user-level install path, in that order). Claude Code +Process directory conventions, ticket frontmatter, the propagation +duties keyed by the edit that triggers them (`propagation-duties.md`, +loaded while a spec, plan or domain document is open), and the +review-report contract (`review-reports.md`: where a code-review run +writes its Review report and what shape it takes; domain review skills +locate the installed contract via its contract probe — the +project-level then user-level install path, in that order). Claude Code does not load plugin rules by itself: install them with the `working-process:sync-rules` skill. diff --git a/plugins/working-process/rules/propagation-duties.md b/plugins/working-process/rules/propagation-duties.md new file mode 100644 index 0000000..91a4359 --- /dev/null +++ b/plugins/working-process/rules/propagation-duties.md @@ -0,0 +1,46 @@ +--- +paths: + - "docs/specs/**" + - "docs/plans/**" + - "docs/domain/**" +--- + +# Propagation duties — the author's checklist + +Before a document goes to an expensive reader, eight duties fall due, +keyed by the nine edits that trigger them — one duty answers two +different edits. This rule lists them by the edit an author has just +made, so the enumeration can be done at the desk instead of paid +for at the gate. The list below is complete as it stands and needs +nothing else to be usable. + +When the `propagation-auditor` agent is available it walks the same +eight as a gate, and its card is their definition and keeps the +measurement behind each; the numbers in the last column are that card's, +so a hit it reports can be read back to the row that would have caught +it. Without the agent the rows still hold — what is lost is the second +pair of eyes, not the duties. + +| You have just… | Enumerate | Duty | +|---|---|---| +| changed an interface — a signature, a name, a heading, an anchor, a field | every consumer, by parsing the structure that defines them, never by text match | 1 | +| prescribed a verbatim block | the anchor it targets — the text it replaces must exist in that file byte-exactly, and once | 2 | +| reported a change already made | that block against the shipped file, both ways: one that never landed, and shipped text a block no longer matches | 2 | +| added a field, label or state | both ends of its chain — what writes it, and what reads it | 3 | +| asserted a count | the count itself, re-derived from what the tool prints or what the list holds | 4 | +| used a name your source does not define | the source's own names; an invented name is a gap in the source rather than an error in yours | 5 | +| reported another document's state | that sentence, against that document | 6 | +| written a verification command | the command, run against your own replacement text | 7 | +| copied a citation out of a review report | the file and line it names, read at the source | 8 | + +Two of these fire where an author does not expect them. A newly minted +glossary `_Avoid_` ban is a changed interface, so duty 1 reaches every +shipped occurrence of the banned term — which is why this rule loads at +the domain directory as well as at specs and plans. And a count asserted +about another document is duties 4 and 6 at once: re-derive it, then +read it back at its source. + +Self-checking never replaces a gate where one runs. A fresh context +finds what an author's eye has stopped seeing; what this buys is a +shorter list for it to find, which is what makes the round after it +cheaper. From 6a8ac6bedd7ca9701b9f252cfc340a74595eb803 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 11:01:48 +0200 Subject: [PATCH 071/120] docs: exempt table rows from the wrap check, as the form cannot wrap --- .../plans/2026-09-11-review-loop-errata-wave-four.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/plans/2026-09-11-review-loop-errata-wave-four.md b/docs/plans/2026-09-11-review-loop-errata-wave-four.md index 61a04fa..781fd90 100644 --- a/docs/plans/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/plans/2026-09-11-review-loop-errata-wave-four.md @@ -1071,16 +1071,18 @@ go stale at the next unrelated edit — three ways of measuring something other than what the constraint says. What the constraint says is that the lines this wave writes are wrapped, so that is what is measured. -Lines indented four spaces or more are excluded: they are grammar -examples, a form the rules use throughout — `spec-plan-lifecycle.md` -carries four of them, up to 123 characters. Two-space bullet text stays -in scope, because that is where replacement prose lands. +Two forms are excluded, both because they cannot wrap. Lines indented +four spaces or more are grammar examples, a form the rules use +throughout — `spec-plan-lifecycle.md` carries four of them, up to 123 +characters. Markdown table rows begin with a pipe and hold one cell per +column; the rule this wave adds is built on one. Two-space bullet text +stays in scope, because that is where replacement prose lands. ```bash BASE=$(git merge-base HEAD develop) git diff "$BASE"..HEAD -- plugins/working-process docs/domain/glossary.md \ | grep '^+' | grep -v '^+++' | sed 's/^+//' \ - | LC_ALL=C.UTF-8 awk 'length > 72 && $0 !~ /^ / {print "("length") "$0}' + | LC_ALL=C.UTF-8 awk 'length > 72 && $0 !~ /^ / && $0 !~ /^\|/ {print "("length") "$0}' ``` Expected: no output. Any line printed is one this wave added that breaks From 4e355624944c7b92ccb6efee23619d641deb2cb1 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 11:02:29 +0200 Subject: [PATCH 072/120] style(glossary): rewrap the Confirming round entry --- docs/domain/glossary.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/domain/glossary.md b/docs/domain/glossary.md index 3e869c8..2e5e220 100644 --- a/docs/domain/glossary.md +++ b/docs/domain/glossary.md @@ -381,14 +381,14 @@ The full-document round a plan's loop owes before it may close, whenever the latest round heading is diff-scoped — whatever path would otherwise end the rounds. A plan already `implemented` owes none: there the round is discharged by recorded decline without any dispatch, as the sibling -chain debt is, since completed work is not re-reviewed. It runs as the same loop's last round, counts against -the round cap like any other, and its verdict is the one stamped; on a -plan the resolution annotation and the adjudication wait for it. It -inherits the gating of whatever ended the rounds: autonomous where -autonomy still stands, the developer's to order after a terminator that -suspends it. A spec owes none: its diff-scoped chain is -discharged at the consumption gate, where a full-document round is one -arm of the pair offer. +chain debt is, since completed work is not re-reviewed. It runs as the +same loop's last round, counts against the round cap like any other, and +its verdict is the one stamped; on a plan the resolution annotation and +the adjudication wait for it. It inherits the gating of whatever ended +the rounds: autonomous where autonomy still stands, the developer's to +order after a terminator that suspends it. A spec owes none: its +diff-scoped chain is discharged at the consumption gate, where a +full-document round is one arm of the pair offer. _Avoid_: final round, closing round, full read (as the name) **Document branch**: From 8a38f3edd5edafc3b98b13d45798b0c717cceb59 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 11:15:12 +0200 Subject: [PATCH 073/120] fix(working-process): state the co-firing line as a shape, not a judgment --- plugins/working-process/skills/process-status/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/working-process/skills/process-status/SKILL.md b/plugins/working-process/skills/process-status/SKILL.md index 656926e..c62175f 100644 --- a/plugins/working-process/skills/process-status/SKILL.md +++ b/plugins/working-process/skills/process-status/SKILL.md @@ -78,7 +78,7 @@ Where two confirmed hits share one document, add one line beneath that document's per-hit lines, naming the classes and pointing at their owner legs: - - two classes hit this document; one move may close both — see their owner legs + - and hit this document — see their owner legs The line is the mapping read back — two hits, one file — and nothing more: it does not say the duplicate is deliberate, and it does not From a3c5e9e48311742807f746b9837aa1f90c778b4e Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 12:11:40 +0200 Subject: [PATCH 074/120] chore(working-process): mint the process-wave-four dogfood prerelease --- plugins/working-process/.claude-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/working-process/.claude-plugin/plugin.json b/plugins/working-process/.claude-plugin/plugin.json index 449ccc4..a1169b5 100644 --- a/plugins/working-process/.claude-plugin/plugin.json +++ b/plugins/working-process/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "working-process", "description": "Spec-driven working process on top of superpowers: grilling-session, architect-session, system-designer-session, process-status and sync-rules skills, architect and plan-adversary verdict agents, architect-consult and system-designer-consult consultation agents, propagation-auditor and integrity-auditor audit agents, and process rules distributed as a Rules payload; domain plugins hook in via *-plan-review checklist skills and their own rules/ payloads", - "version": "0.16.0", + "version": "0.17.0-dev.process-wave-four", "author": { "name": "Missing Bits (Jacek Nakonieczny)" }, "license": "MIT", "keywords": ["process", "workflow", "spec", "plan", "review", "glossary", "adr", "architecture"], From 8ff1cd1ed6655b74816fc39e31d7b89556a42a0f Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 13:15:10 +0200 Subject: [PATCH 075/120] docs: mark the errata wave-four spec and plan implemented --- docs/plans/2026-09-11-review-loop-errata-wave-four.md | 2 +- docs/specs/2026-09-11-review-loop-errata-wave-four.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plans/2026-09-11-review-loop-errata-wave-four.md b/docs/plans/2026-09-11-review-loop-errata-wave-four.md index 781fd90..befad06 100644 --- a/docs/plans/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/plans/2026-09-11-review-loop-errata-wave-four.md @@ -1,7 +1,7 @@ --- ticket: none date: 2026-09-11 -status: approved +status: implemented adversary: concerns (resolved 2026-09-14) spec: ../specs/2026-09-11-review-loop-errata-wave-four.md branch: feature/process-wave-four diff --git a/docs/specs/2026-09-11-review-loop-errata-wave-four.md b/docs/specs/2026-09-11-review-loop-errata-wave-four.md index e37bf6a..36a43e2 100644 --- a/docs/specs/2026-09-11-review-loop-errata-wave-four.md +++ b/docs/specs/2026-09-11-review-loop-errata-wave-four.md @@ -1,7 +1,7 @@ --- ticket: none date: 2026-09-11 -status: approved +status: implemented grilled: 2026-09-11 architect: blocking (adjudicated 2026-09-11) revises: [./2026-08-17-autonomous-review-loop-design.md, ./2026-09-07-diff-scoped-chain-debt-design.md] From 6eebb6eb98b24aef54e69c561c3d092fa7f62315 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 19:49:06 +0200 Subject: [PATCH 076/120] docs: grill the errata wave-five spec and mint the Origin term --- docs/domain/glossary.md | 8 + ...2026-09-14-review-loop-errata-wave-five.md | 305 ++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 docs/specs/2026-09-14-review-loop-errata-wave-five.md diff --git a/docs/domain/glossary.md b/docs/domain/glossary.md index 2e5e220..7aee38c 100644 --- a/docs/domain/glossary.md +++ b/docs/domain/glossary.md @@ -134,6 +134,14 @@ bracket slot (`[Minor]`). The collective noun is model-capability ladder. _Avoid_: severity tier +**Origin**: +The document a review finding traces to — `plan`, `spec`, or `both` — +named by the reviewer that found it rather than derived at triage. A +finding originating in the spec is held unless a written decision +licenses the edit, since editing a spec from inside a plan review is +design work. +_Avoid_: owner (for a document), source + **Rule tag**: The inline parenthesized annotation at a rule's (or sub-rule's) definition site carrying its identity and grade: diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md new file mode 100644 index 0000000..b70d8af --- /dev/null +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -0,0 +1,305 @@ +--- +ticket: none +date: 2026-09-14 +status: draft +grilled: 2026-09-14 +branch: feature/process-wave-five +base: develop +--- + +# Review loop — errata wave five + +Four defects in how a review's result reaches the developer. The loop +decides well and reports badly: a relay arrives as a wall of prose, a +consultation arrives as two walls, a finding names the section it +concerns but never the document it came from, and nothing the relay +condenses survives the session that condensed it. + +## Problem + +The developer has asked four times, in four different sessions, for the +same thing: *summarise the agents' findings in short bullets, which +decisions are mine* (2026-08-19); *what did the architect return — so +only minor finds, nothing touching design or critical decisions* +(2026-08-25); *a short, substantive bullet list of the code review* +(2026-09-07); and, first, simply *too much reading, can you shorten it* +(2026-08-10). Four asks for one missing contract. + +The rules define a relay's floor and nothing about its shape. The floor +is a safety property rather than a style: a relay is the developer's +standing veto, so a finding nobody printed is a finding nobody could +overrule. Shape was left to the session, and the session chose prose. + +A second defect sits beside it. Consultations return contributions +rather than findings, and two personas answering one briefing produce +the longest output the process generates. The rule governing that relay +reads `Relay each contribution attributed and substantially verbatim` +and says nothing more. Its own spec says more — see decision 2. + +The third defect is about provenance rather than length. After an +adversary round found a runbook order contradicting the spec's delivery +split, the developer asked which documents were up for correction +(2026-08-17); on 2026-08-26 they ordered both sweeps by hand, because +nothing in the report said which document a finding belonged to. The +adversary's finding schema carries `section` and names no document, the +ledger records dispositions on the reviewed document alone, and the +propagation auditor catches an invented name rather than a misplaced +decision. + +The fourth defect is what makes the first two safe to fix. A relay may +condense only what a reader can still recover, and today the full text +lives in the session alone. This repo has measured that failure twice +in one day: a requirement stated in a dispatch rather than a file was +invisible to the gate meant to check it, and an implementation wave's +only durable account of itself was a ledger written on purpose. Shorten +a relay without a file behind it and the process trades a wall of text +for a hole. + +## Decisions + +1. **The relay leads with a fixed header, and the finding list is never + condensed.** The header carries the verdict, the model self-report, + the finding count by severity, and how many decisions await the + developer. Each finding then takes one line — severity, origin, + claim. Narrative may be condensed, and the relay names the agent's + report file so an expansion is a quotation rather than a + reconstruction. The floor stays as written. + + The finding line carries what the finding carries. `origin` belongs + to a plan review, so an architect relay's line reads severity and + claim: the architect reads one document and every finding it returns + originates there, making the field a constant. A constant field + carries no information and invites a reviewer to manufacture variety + it does not have. + + The count of decisions is derived rather than judged: it is the + number of `held` lines this round wrote. The ledger grammar already + defines `held` as the finding that needs the developer, and requires + `question:` and `options:` on every such line, so the count is a + count of lines and a later reader can recompute it. Verdicts that + suspend autonomy — `blocking`, the spent round cap, the all-Minor + signal — stay out of it: they decide whether the loop continues + rather than what a document says, and the relay already reports them + in its own right. + + What makes the second clause load-bearing is new. Claude Code ships a + built-in `Concise` output style that condenses by default and expands + on request, which is the mechanism the developer wants for session + length. It publishes what it never condenses: error reports, security + warnings, and confirmations of destructive actions. A review relay is + absent from that list. Length therefore has a second owner now, and + the rule must name what that owner may not eat. `ruling: 2026-09-14` + +2. **A floor sits where its unit is bounded.** A verdict relay keeps + its floor in the relay: findings are enumerable and one line each, + so carrying every one of them costs little and leaves the veto + whole. A consultation's floor moves to the file: recommendations and + named risks have no bound, a persona may return fifteen, and that is + the wall this wave exists to remove. The consultation relay then + carries one paragraph answering each focusing question and the path + to the contribution. + + The move is possible only because decision 5 guarantees the file + before the relay. Without it the floor would move to the session's + own memory, which is where it already fails. + + Part of this restores rather than relaxes. The design-personas spec + (`./2026-07-28-working-process-design-personas-design.md`, + `status: implemented`) grants the compression: *Compression is + allowed; merging is not. The checkable floor: every recommendation + and every named risk survives, and text from two personas never + lands in one bullet.* The shipped rule carries the floor and drops + the permission — the word `compression` appears nowhere in + `workflow.md` — so a rule read literally says *verbatim*, full stop, + which is why consultation relays grew into walls. + + What this wave changes beyond that restoration is the floor's site + for consultations alone, and it says so rather than hiding the + change inside a restoration. The floor's content is untouched: every + recommendation and every named risk still survives, and two personas + still never land in one bullet — in the file. `revises:` stays + absent even so, since the design-personas document's design is not + superseded: its floor holds, at a place that document could not have + named, because no file existed then. `ruling: 2026-09-14` + +3. **The adversary emits the origin.** The finding schema gains + `origin` beside `section`, valued `plan`, `spec`, or `both`. The + reviewer saw the contradiction and knows which document is wrong; + the dispatcher, who did not, is the one who had to ask. Naming the + document a defect traces to is not a design review of the spec, so + the card's `Specs are out of scope` boundary holds and says so. + + The triage consequence is a specialisation rather than a new axis: + a finding originating in the spec is `held` unless a written + decision licenses the edit. Triage already holds whatever no written + decision licenses; what this adds is that spec origin is itself a + reason to hold, since editing a spec from inside a plan review is + design work. + + `both` holds like `spec`, and the `held` line names in its `options:` + which half is fixable at once. A finding whose origin is both + documents usually means the spec decided badly and the plan + implemented that decision faithfully, so its plan half is often + trivial and its spec half never is. Splitting the disposition would + need a ledger line to carry two authorizers, which the grammar + forbids and this wave does not reopen; `options:` already carries the + options and the session's recommendation, so one answer licenses both + halves. + + The field is named `origin` rather than `owner` because this process + already spends `owner` on an actor: the Unfinished-work list's third + leg names who makes the next move, seven times over, and + `review-reports.md` calls the author of a report the run's owner. + Grilling minted **Origin** in the glossary, with `owner` for a + document under its `_Avoid_`. `ruling: 2026-09-14` + +4. **Two items left this wave during brainstorming, and one was never + in it.** Both departures are recorded here so a later reader finds + the reasoning rather than a gap. `ruling: 2026-09-14` + + The ban on process noise in code comments left because the developer + refused all three of its delivery routes on a better ground than any + of them: a rule in a distributed plugin imposes one developer's + preference on everyone who installs it. The guidance belongs to a + worker a project opts into. It now rides a proposed implementer + agent, whose card body is neither a brief nor a dispatch and so can + be lost by neither. + + The `process-status` class for documents living only on an unmerged + branch left because measurement refuted every baseline that would + let it travel. Counted on this repo: against `develop`, zero, which + is correct; against what git itself reports as the default branch, + twenty-plus, every one healthy; against each document's own `base:` + field, eighteen false positives, because `base:` records where a + branch was cut rather than where the document should live. The class + needs the project's integration branch named, and the + `process-status` contract forbids rewriting a published command. + Its live instance is also gone, so nothing remains to test against. + +5. **A report is written to disk before it is relayed.** The relay may + condense narrative only because the full text stays recoverable, and + a copy living in the session dies at the next compaction — twice + over in this repo's own history. So the dispatcher writes the + agent's report to a file, then names that file in the relay. + + The store is `.working-process/`, a git-ignored directory at the + repository root, materialised with a `.gitignore` containing exactly + `*` at its first write. This is a declaration rather than a + first-create question: the artefacts are ephemeral by construction, + so the mode follows from what they are and no project is asked. + + One directory per subject, named for it — ``, the reviewed + document's basename without its extension. A consultation carrying + no document takes its subject as the stem. Grouping by subject + rather than by agent makes one wave one `ls` and one deletion, and + it follows the shape `subagent-driven-development` already uses for + its own workspace. + + A subject becomes a stem mechanically: the briefing's own naming of + it, kebab-cased and capped at sixty characters. The briefing already + has to name the subject, so nothing is invented. The limit is worth + stating — two sessions phrasing one subject differently produce two + directories — and it is small, since a document-less consultation is + a one-off and a follow-up dispatcher sees the directory already + there. + + Inside, the file names its agent and one discriminator: + + .working-process//-round-.md + .working-process//--.md + + A verdict agent takes the round ordinal, which the ledger already + owns. A consultation takes the timestamp, because a consultation + records nothing and so has no ordinal to derive, and because several + consultations of one persona on one subject in one day are expected + — a follow-up is a fresh dispatch rather than a resumption. The + timestamp also orders them, which a counter would not. + + Where a name is already taken, the new file appends the timestamp. + That covers the superseded round the rules already describe, whose + stale result is relayed and never stamped: the two rounds share an + ordinal, and the rule resolves it mechanically instead of asking a + session to judge which report is the stale one. `ruling: 2026-09-14` + +## Scope + +1. **W1 — the relay header and the uncondensable list.** + `workflow.md`, the relay bullet under *Dispatching a verdict agent*. + The bullet gains the header's contents, the one-line-per-finding + rule, and the report-file pointer. It states that condensation + reaches narrative alone, whatever a session's output style says. + +2. **W2 — the consultation relay shape and its floor's site.** + `workflow.md`, the *Dispatching a consultation* paragraph. One + paragraph per focusing question, then the path to the contribution. + The paragraph names where the floor is met — compression yes, + merging no, every recommendation and every named risk surviving in + the file — so a reader meets the floor and its site together. + +3. **W3 — the origin field.** Three sites, edited together: + `agents/plan-adversary.md` gains `origin` in the `Output` schema + with its three values and one sentence placing it inside the card's + existing spec boundary; `workflow.md` gains the triage sentence + decision 3 defines, beside the existing license clause; and + `docs/domain/glossary.md` carries the **Origin** entry grilling + already minted. + +4. **W4 — the report store.** `workflow.md`, the verdict-agent + dispatch bullet list and the consultation paragraph: both gain the + write-then-relay duty and the path shape decision 5 defines. The + store's ignored mode is stated where the path is, so a reader never + meets the path without the mode. + +## Out of scope + +- **Session length.** The `Concise` output style is configuration the + developer sets, and this wave neither ships a style nor mentions one + in a rule. A plugin *can* ship output styles, and `force-for-plugin` + would override the user's own setting; a process plugin dictating a + global response voice exceeds its remit. +- **A per-agent output style.** Not possible. Seventeen agent + frontmatter fields are supported and none sets one; a subagent runs + its own system prompt, so the session's style does not reach it + either. An agent's response shape comes from its card body alone. +- **The relay of audits.** `propagation-auditor` returns located hits + or one token, and `integrity-auditor` returns a structured report. + Neither is prose that grew, so neither earns a shape this wave, and + neither writes to the store either: a gate's hits already land in the + ledger as `hit fixed` and `hit dismissed` lines, and an integrity + audit's dispositions land as the edits it causes plus the + `integrity:` stamp. Their durable home exists, so decision 5 would + give one fact a second one. +- **A code review's reply.** One of the four asks in *Problem* — the + short bullet list of a code review, 2026-09-07 — lands on a different + surface: `review-reports.md` governs the run's reply and already + prescribes part of it, one line per proposed rule. Giving the reply a + shape is the same idea one rule over, and it belongs to a wave of its + own. Admitting it here would repeat the scope creep wave four paid two + review rounds for. + +## Verification + +Each check names the file, the phrase, and the value expected after the +change. Phrases normalise whitespace first, so a match survives the +wrap. + +- `workflow.md` contains the header's four elements in the relay bullet, + and one phrase naming the finding line as uncondensable: expected 1. +- `workflow.md` contains `compression`: expected ≥ 1, where today it is + 0 — the check that proves decision 2 landed. +- `workflow.md` contains a phrase naming one paragraph per focusing + question: expected 1. +- `plan-adversary.md` contains `"origin"` inside the `Output` block: + expected 1, with its three values on the same line. +- No `owner` names a document in `plan-adversary.md` or `workflow.md`: + expected 0, the check that proves the collision stayed resolved. +- `plan-adversary.md` still contains its spec-boundary sentence: + expected 1, an invariant this wave must not disturb. +- `workflow.md` contains the spec-origin triage sentence: expected 1. +- `workflow.md` contains `.working-process/` and the two filename + shapes: expected 1 each, and the word naming the store ignored: 1. +- `glossary.md` contains an `**Origin**:` entry: expected 1. +- `claude plugin validate` passes for the plugin and the marketplace: + an invariant. + +## Review rounds From 63314309cea775c547f521f87ed1ecd1c3d0fe5d Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 19:52:30 +0200 Subject: [PATCH 077/120] docs: recount the Owner legs the wave-five spec cites --- docs/specs/2026-09-14-review-loop-errata-wave-five.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index b70d8af..74556ac 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -148,7 +148,7 @@ for a hole. The field is named `origin` rather than `owner` because this process already spends `owner` on an actor: the Unfinished-work list's third - leg names who makes the next move, seven times over, and + leg names who makes the next move, six times over, and `review-reports.md` calls the author of a report the run's owner. Grilling minted **Origin** in the glossary, with `owner` for a document under its `_Avoid_`. `ruling: 2026-09-14` From a6ded8e47b104687617da267651fff59e4b9d2ea Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 20:04:13 +0200 Subject: [PATCH 078/120] docs: stamp architect round one on the wave-five spec and apply its licensed fixes --- ...2026-09-14-review-loop-errata-wave-five.md | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index 74556ac..beaa12a 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -3,6 +3,7 @@ ticket: none date: 2026-09-14 status: draft grilled: 2026-09-14 +architect: blocking branch: feature/process-wave-five base: develop --- @@ -90,14 +91,18 @@ for a hole. absent from that list. Length therefore has a second owner now, and the rule must name what that owner may not eat. `ruling: 2026-09-14` -2. **A floor sits where its unit is bounded.** A verdict relay keeps - its floor in the relay: findings are enumerable and one line each, - so carrying every one of them costs little and leaves the veto - whole. A consultation's floor moves to the file: recommendations and - named risks have no bound, a persona may return fifteen, and that is - the wall this wave exists to remove. The consultation relay then - carries one paragraph answering each focusing question and the path - to the contribution. +2. **A floor sits where its unit has a shape.** A verdict relay keeps + its floor in the relay: a finding arrives with a `claim` slot, so a + relay carries every one of them in one line each and the veto stays + whole however many arrive. A consultation's floor moves to the file: + a contribution is prose with no delimited unit, so "every + recommendation survives" cannot be carried in a relay at all without + carrying the whole thing — which is the wall this wave exists to + remove. The asymmetry is structural rather than numerical: a round + of twenty findings still relays in twenty lines. The consultation + relay then carries one paragraph per focusing question — or one per + briefing, where a single persona was consulted and none was appended + — and the path to the contribution. The move is possible only because decision 5 guarantees the file before the relay. Without it the floor would move to the session's @@ -208,12 +213,15 @@ for a hole. .working-process//-round-.md .working-process//--.md - A verdict agent takes the round ordinal, which the ledger already - owns. A consultation takes the timestamp, because a consultation - records nothing and so has no ordinal to derive, and because several - consultations of one persona on one subject in one day are expected - — a follow-up is a fresh dispatch rather than a resumption. The - timestamp also orders them, which a counter would not. +A verdict agent takes the round ordinal, which the ledger already + owns — except where it reviews no document and so has no ledger to + own one, as an `architect` dispatch on a bare question does; that + dispatch takes the timestamp shape. A consultation takes the + timestamp too, because a consultation records nothing and so has no + ordinal to derive, and because several consultations of one persona + on one subject in one day are expected — a follow-up is a fresh + dispatch rather than a resumption. The timestamp also orders them, + which a counter would not. Where a name is already taken, the new file appends the timestamp. That covers the superseded round the rules already describe, whose @@ -231,7 +239,8 @@ for a hole. 2. **W2 — the consultation relay shape and its floor's site.** `workflow.md`, the *Dispatching a consultation* paragraph. One - paragraph per focusing question, then the path to the contribution. + paragraph per focusing question — or per briefing, where none was + appended — then the path to the contribution. The paragraph names where the floor is met — compression yes, merging no, every recommendation and every named risk surviving in the file — so a reader meets the floor and its site together. @@ -303,3 +312,18 @@ wrap. an invariant. ## Review rounds + +### 2026-09-14 — architect, fable 5.1, blocking (round 1, full-document) + +- hit fixed 2026-09-14 — the spec cited seven `Owner:` legs where the Unfinished-work list carries six; recounted against the list's entries and corrected to six +- held — [Important] the header's decisions count is derived from `held` lines that do not exist at relay time: `open` is written at stamp time and triage follows the stamp, so the header forecasts triage rather than counting lines; question: does the header count findings the session can cite no license for, computed before any line is written, or does the count leave the header for the held batch?; options: (a) redefine as unlicensed findings and make the later `held` count the check that must equal it — my recommendation, since it keeps one number in the header and turns the ledger into its verification; (b) move the count into the held batch, which is where those decisions are actually put, leaving the header three elements +- held — [Important] `.working-process/` is a Process directory by the glossary's own definition, and the spec declares its mode without amending the class or the rule that owns the first-create question; question: earn the exception at the class, or place the store where the existing exception already applies?; options: (a) move the store under `.claude/working-process/`, inside the carve-out for configuration directories that the glossary already grants and Private memory already uses — my recommendation, one precedent, no glossary edit; (b) amend the **Process directory** entry and `process-artifacts.md` to name the store a derived, re-creatable cache outside the class +- held — [Important] decisions 2 and 5 disagree about what the file is: decision 2 moves a floor into it because decision 5 "guarantees" it, while decision 5 calls it ephemeral by construction and gives it no lifetime; question: what bounds the guarantee, and is that bound acceptable for the one artefact with no ledger behind it?; options: (a) state the lifetime in decision 5 — this checkout, until the `` directory is deleted — and have decision 2 cite the bound rather than a guarantee, my recommendation; (b) keep the consultation floor in the relay and move only its narrative to the file, which costs the wave its main saving; (c) give the store a close, as subagent-driven-development gives its workspace one +- held — [Important] the triage clause licenses editing a spec from a plan's fix wave and says nothing about where that edit is recorded or what it does to the spec's standing stamp; question: does a spec-origin fix follow the spec's own lifecycle?; options: (a) one sentence — the disposition line lands under the spec's latest round heading or as the lifecycle rule's resolution annotation, and the plan's line points at it, keeping one authorizer per line across two documents — my recommendation; (b) leave the recording unspecified and let each session decide, which is the state the wave set out to end +- fixed 2026-09-14 — [Minor] decision 2 argued the floor's move from the unboundedness of a contribution's count, where the real asymmetry is the unit's shape; license: `agents/plan-adversary.md`'s Output schema, which gives a finding a `claim` slot a relay carries in one line while a contribution has no delimited unit; the argument now runs from shape and survives a twenty-finding round +- held — [Minor] `revises:` is declined on the design-personas document although that document names the relay as the floor's site and this wave moves it; question: add `revises: ./2026-07-28-working-process-design-personas-design.md`?; options: (a) add it — the reviewer's reading is that the lifecycle rule's definition is met, and the pointer costs one line; (b) keep it absent on the recorded ground that the floor holds rather than lapses; counter: the decline carries `ruling: 2026-09-14`, so this is a re-raise against a recorded decision and belongs to the developer +- fixed 2026-09-14 — [Minor] "one paragraph answering each focusing question" has no value where a single persona is consulted and no focusing question was appended; license: the workflow rule's consultation paragraph, which appends one only when both personas are consulted; the shape now reads per focusing question, or per briefing where none was appended +- fixed 2026-09-14 — [Minor] the filename shape assumes a ledger ordinal for every verdict agent, which an `architect` dispatch on a bare question does not have; license: the glossary's **Consultation** entry, which records exactly that case; such a dispatch now takes the timestamp shape +- open — [Minor] the spec states as settled that a rule's condensation clause outranks an output style's system-prompt instruction, where the relative precedence is unmeasured +- open — [Minor] "report" and "report store" collide with the glossary's **Review report**, a different object under `docs/code-review/` +- signal 2026-09-14 — another round earns its cost: the four Important findings each reshape a sentence in the decision they concern, and repair-born drift between decisions 2 and 5 is the likely failure of that wave, so a diff-scoped round two attacking those fixes is worth a top-tier read; the Minors need no round of their own From 6ca1ffbbdf4ceed4b2970a37aeb664ea23c990b2 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 20:04:26 +0200 Subject: [PATCH 079/120] style: restore the list indent on the wave-five spec's ordinal clause --- docs/specs/2026-09-14-review-loop-errata-wave-five.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index beaa12a..880b1b6 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -213,7 +213,7 @@ for a hole. .working-process//-round-.md .working-process//--.md -A verdict agent takes the round ordinal, which the ledger already + A verdict agent takes the round ordinal, which the ledger already owns — except where it reviews no document and so has no ledger to own one, as an `architect` dispatch on a bare question does; that dispatch takes the timestamp shape. A consultation takes the From 7c0e499c3149dc0deeea7bd181f8871e9dfd088b Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 21:35:30 +0200 Subject: [PATCH 080/120] docs: apply architect round one's rulings to the errata wave-five spec --- ...2026-09-14-review-loop-errata-wave-five.md | 118 ++++++++++++------ 1 file changed, 80 insertions(+), 38 deletions(-) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index 880b1b6..3cb423a 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -4,6 +4,7 @@ date: 2026-09-14 status: draft grilled: 2026-09-14 architect: blocking +revises: ./2026-07-28-working-process-design-personas-design.md branch: feature/process-wave-five base: develop --- @@ -73,15 +74,20 @@ for a hole. carries no information and invites a reviewer to manufacture variety it does not have. - The count of decisions is derived rather than judged: it is the - number of `held` lines this round wrote. The ledger grammar already - defines `held` as the finding that needs the developer, and requires - `question:` and `options:` on every such line, so the count is a - count of lines and a later reader can recompute it. Verdicts that - suspend autonomy — `blocking`, the spent round cap, the all-Minor - signal — stay out of it: they decide whether the loop continues - rather than what a document says, and the relay already reports them - in its own right. + The count of decisions is derived rather than judged, and it is + derived from what exists at relay time: the findings for which the + session can cite no license. Triage's license test needs only the + report and the decisions already written down, so the count is + computable before any ledger line is. The `held` lines the same + round later writes are that count's check — they must come to the + same number, and a divergence means triage found a license the + relay missed or lost one it claimed. Deriving the count from the + lines themselves would have the header report a forecast, since + `open` is written at stamp time and triage follows the stamp, which + follows the relay. Verdicts that suspend autonomy — `blocking`, the + spent round cap, the all-Minor signal — stay out of the count: they + decide whether the loop continues rather than what a document says, + and the relay already reports them in its own right. What makes the second clause load-bearing is new. Claude Code ships a built-in `Concise` output style that condenses by default and expands @@ -104,9 +110,12 @@ for a hole. briefing, where a single persona was consulted and none was appended — and the path to the contribution. - The move is possible only because decision 5 guarantees the file - before the relay. Without it the floor would move to the session's - own memory, which is where it already fails. + The move rests on decision 5's dispatch record, and on the bound + that decision states: the record survives compaction, the session's + end and a branch switch, and no further. The floor is met on the + checkout that ran the dispatch. Without the record the floor would + move into the session's own memory, which is where it already + fails. Part of this restores rather than relaxes. The design-personas spec (`./2026-07-28-working-process-design-personas-design.md`, @@ -141,6 +150,14 @@ for a hole. reason to hold, since editing a spec from inside a plan review is design work. + A licensed spec-origin fix follows the spec's own lifecycle rather + than the plan's. Its disposition line lands under the spec's latest + round heading, or as the resolution annotation the lifecycle rule + already defines for a stamped document edited without a fresh round, + and the plan's line points at it. One authorizer per line still + holds — one authorizer, two documents, two lines — and the spec's + stamp stops certifying words that are gone. + `both` holds like `spec`, and the `held` line names in its `options:` which half is fixable at once. A finding whose origin is both documents usually means the spec decided badly and the plan @@ -181,17 +198,35 @@ for a hole. `process-status` contract forbids rewriting a published command. Its live instance is also gone, so nothing remains to test against. -5. **A report is written to disk before it is relayed.** The relay may - condense narrative only because the full text stays recoverable, and - a copy living in the session dies at the next compaction — twice - over in this repo's own history. So the dispatcher writes the - agent's report to a file, then names that file in the relay. - - The store is `.working-process/`, a git-ignored directory at the - repository root, materialised with a `.gitignore` containing exactly - `*` at its first write. This is a declaration rather than a - first-create question: the artefacts are ephemeral by construction, - so the mode follows from what they are and no project is asked. +5. **A dispatch record is written to disk before it is relayed.** The + relay may condense narrative only because the full text stays + recoverable, and a copy living in the session dies at the next + compaction — twice over in this repo's own history. So the + dispatcher writes what the agent returned to a file, then names that + file in the relay. The artefact is a **dispatch record**: one + background dispatch, one file. It is not a Review report, which is + the persistent, counted document a code-review run writes under + `docs/code-review/`. + + The store is `.claude/working-process/`, under the `.claude/` + namespace and ignored by a `.gitignore` containing exactly `*` + written at its first use. The location is what settles its mode. A + directory the process creates in a repo to hold work artifacts is a + Process directory and owes the first-create question, and + `.superpowers/` at the repo root is one — but a store under + `.claude/` is outside that class by the glossary's own carve-out, + and Private memory is the standing precedent: always ignored, never + asked, a store under the config namespace. This wave adds a second + store of that kind rather than a Process directory whose class it + would have to amend. + + What the store guarantees is bounded, and decision 2 leans on the + bound rather than on a promise. A dispatch record survives what the + session does not: compaction, the end of the session, a branch + switch. It does not survive `git clean -fdx`, which removes ignored + files, nor does it reach a second Environment or a reviewer reading + the document branch. That is the same durability Private memory has + and the same the developer relies on daily. One directory per subject, named for it — ``, the reviewed document's basename without its extension. A consultation carrying @@ -210,8 +245,8 @@ for a hole. Inside, the file names its agent and one discriminator: - .working-process//-round-.md - .working-process//--.md + .claude/working-process//-round-.md + .claude/working-process//--.md A verdict agent takes the round ordinal, which the ledger already owns — except where it reviews no document and so has no ledger to @@ -253,11 +288,14 @@ for a hole. `docs/domain/glossary.md` carries the **Origin** entry grilling already minted. -4. **W4 — the report store.** `workflow.md`, the verdict-agent - dispatch bullet list and the consultation paragraph: both gain the - write-then-relay duty and the path shape decision 5 defines. The - store's ignored mode is stated where the path is, so a reader never - meets the path without the mode. +4. **W4 — the dispatch record.** Two sites. `workflow.md`, the + verdict-agent dispatch bullet list and the consultation paragraph: + both gain the write-then-relay duty and the path shape decision 5 + defines, with the ignored mode stated where the path is, so a reader + never meets the path without the mode. `docs/domain/glossary.md` + gains a **Dispatch record** entry, since the rules now name an + artefact the glossary does not define and the nearest defined term, + **Review report**, is a different object under `docs/code-review/`. ## Out of scope @@ -305,8 +343,12 @@ wrap. - `plan-adversary.md` still contains its spec-boundary sentence: expected 1, an invariant this wave must not disturb. - `workflow.md` contains the spec-origin triage sentence: expected 1. -- `workflow.md` contains `.working-process/` and the two filename - shapes: expected 1 each, and the word naming the store ignored: 1. +- `workflow.md` contains `.claude/working-process/` and the two + filename shapes: expected 1 each, and the word naming the store + ignored: 1. +- `glossary.md` contains a `**Dispatch record**:` entry: expected 1. +- `workflow.md` contains no `.working-process/` outside the + `.claude/` prefix: expected 0. - `glossary.md` contains an `**Origin**:` entry: expected 1. - `claude plugin validate` passes for the plugin and the marketplace: an invariant. @@ -316,14 +358,14 @@ wrap. ### 2026-09-14 — architect, fable 5.1, blocking (round 1, full-document) - hit fixed 2026-09-14 — the spec cited seven `Owner:` legs where the Unfinished-work list carries six; recounted against the list's entries and corrected to six -- held — [Important] the header's decisions count is derived from `held` lines that do not exist at relay time: `open` is written at stamp time and triage follows the stamp, so the header forecasts triage rather than counting lines; question: does the header count findings the session can cite no license for, computed before any line is written, or does the count leave the header for the held batch?; options: (a) redefine as unlicensed findings and make the later `held` count the check that must equal it — my recommendation, since it keeps one number in the header and turns the ledger into its verification; (b) move the count into the held batch, which is where those decisions are actually put, leaving the header three elements -- held — [Important] `.working-process/` is a Process directory by the glossary's own definition, and the spec declares its mode without amending the class or the rule that owns the first-create question; question: earn the exception at the class, or place the store where the existing exception already applies?; options: (a) move the store under `.claude/working-process/`, inside the carve-out for configuration directories that the glossary already grants and Private memory already uses — my recommendation, one precedent, no glossary edit; (b) amend the **Process directory** entry and `process-artifacts.md` to name the store a derived, re-creatable cache outside the class -- held — [Important] decisions 2 and 5 disagree about what the file is: decision 2 moves a floor into it because decision 5 "guarantees" it, while decision 5 calls it ephemeral by construction and gives it no lifetime; question: what bounds the guarantee, and is that bound acceptable for the one artefact with no ledger behind it?; options: (a) state the lifetime in decision 5 — this checkout, until the `` directory is deleted — and have decision 2 cite the bound rather than a guarantee, my recommendation; (b) keep the consultation floor in the relay and move only its narrative to the file, which costs the wave its main saving; (c) give the store a close, as subagent-driven-development gives its workspace one -- held — [Important] the triage clause licenses editing a spec from a plan's fix wave and says nothing about where that edit is recorded or what it does to the spec's standing stamp; question: does a spec-origin fix follow the spec's own lifecycle?; options: (a) one sentence — the disposition line lands under the spec's latest round heading or as the lifecycle rule's resolution annotation, and the plan's line points at it, keeping one authorizer per line across two documents — my recommendation; (b) leave the recording unspecified and let each session decide, which is the state the wave set out to end +- fixed 2026-09-14 — [Important] the header's decisions count was derived from `held` lines that do not exist at relay time, since `open` is written at stamp time and triage follows the stamp; ruling: 2026-09-14; the count is now the findings for which the session can cite no license, computable from the report and the decisions already written, and the round's later `held` lines are its check +- fixed 2026-09-14 — [Important] `.working-process/` at the repo root is a Process directory by the glossary's definition, and the spec declared its mode without amending the class; ruling: 2026-09-14; the store moved to `.claude/working-process/`, inside the carve-out the glossary already grants and Private memory already occupies, so no class is amended and no project is asked +- fixed 2026-09-14 — [Important] decisions 2 and 5 disagreed about what the file is, one calling it guaranteed and the other ephemeral by construction; ruling: 2026-09-14; decision 5 now states the bound — it survives compaction, the session's end and a branch switch, and not `git clean -fdx`, a second Environment or the document branch — and decision 2 leans on that bound rather than on a promise +- fixed 2026-09-14 — [Important] the triage clause licensed editing a spec from a plan's fix wave and said nothing about where that edit is recorded or what it does to the spec's standing stamp; ruling: 2026-09-14; a spec-origin fix now follows the spec's own lifecycle — its line lands under the spec's latest round heading or as the resolution annotation, and the plan's line points at it - fixed 2026-09-14 — [Minor] decision 2 argued the floor's move from the unboundedness of a contribution's count, where the real asymmetry is the unit's shape; license: `agents/plan-adversary.md`'s Output schema, which gives a finding a `claim` slot a relay carries in one line while a contribution has no delimited unit; the argument now runs from shape and survives a twenty-finding round -- held — [Minor] `revises:` is declined on the design-personas document although that document names the relay as the floor's site and this wave moves it; question: add `revises: ./2026-07-28-working-process-design-personas-design.md`?; options: (a) add it — the reviewer's reading is that the lifecycle rule's definition is met, and the pointer costs one line; (b) keep it absent on the recorded ground that the floor holds rather than lapses; counter: the decline carries `ruling: 2026-09-14`, so this is a re-raise against a recorded decision and belongs to the developer +- fixed 2026-09-14 — [Minor] `revises:` was declined on the design-personas document although that document names the relay as the floor's site and this wave moves it; ruling: 2026-09-14, overturning the decline; the developer's stated ground for the decline — that the document is git-ignored and unlinkable — was refuted by checking the index, where all 28 files of `docs/specs/` are tracked; the pointer is added - fixed 2026-09-14 — [Minor] "one paragraph answering each focusing question" has no value where a single persona is consulted and no focusing question was appended; license: the workflow rule's consultation paragraph, which appends one only when both personas are consulted; the shape now reads per focusing question, or per briefing where none was appended - fixed 2026-09-14 — [Minor] the filename shape assumes a ledger ordinal for every verdict agent, which an `architect` dispatch on a bare question does not have; license: the glossary's **Consultation** entry, which records exactly that case; such a dispatch now takes the timestamp shape - open — [Minor] the spec states as settled that a rule's condensation clause outranks an output style's system-prompt instruction, where the relative precedence is unmeasured -- open — [Minor] "report" and "report store" collide with the glossary's **Review report**, a different object under `docs/code-review/` +- fixed 2026-09-14 — [Minor] "report" and "report store" collided with the glossary's **Review report**, a different object under `docs/code-review/`; license: that glossary term; the artefact is now a **dispatch record**, and W4 gains the glossary entry that mints it - signal 2026-09-14 — another round earns its cost: the four Important findings each reshape a sentence in the decision they concern, and repair-born drift between decisions 2 and 5 is the likely failure of that wave, so a diff-scoped round two attacking those fixes is worth a top-tier read; the Minors need no round of their own From 4a62dbf6210e2fa7446b668e9c51df74960e446f Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 21:37:38 +0200 Subject: [PATCH 081/120] docs: mark the output-style precedence an assumption in the wave-five spec --- .../2026-09-14-review-loop-errata-wave-five.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index 3cb423a..f3a78de 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -95,7 +95,17 @@ for a hole. length. It publishes what it never condenses: error reports, security warnings, and confirmations of destructive actions. A review relay is absent from that list. Length therefore has a second owner now, and - the rule must name what that owner may not eat. `ruling: 2026-09-14` + the rule must name what that owner may not eat. + + That the rule's clause outranks the style's instruction is an + assumption rather than a measurement. Both reach the model as + instructions — a style's with every request, a rule's from the + session's loaded instructions — and their relative precedence is + unmeasured. The observable that falsifies it: a relay produced under + the `Concise` style that drops a finding line. A project running the + style is where the probe belongs, and until one runs it the wave + proceeds on the assumption rather than on a result. + `ruling: 2026-09-14` 2. **A floor sits where its unit has a shape.** A verdict relay keeps its floor in the relay: a finding arrives with a `claim` slot, so a @@ -366,6 +376,6 @@ wrap. - fixed 2026-09-14 — [Minor] `revises:` was declined on the design-personas document although that document names the relay as the floor's site and this wave moves it; ruling: 2026-09-14, overturning the decline; the developer's stated ground for the decline — that the document is git-ignored and unlinkable — was refuted by checking the index, where all 28 files of `docs/specs/` are tracked; the pointer is added - fixed 2026-09-14 — [Minor] "one paragraph answering each focusing question" has no value where a single persona is consulted and no focusing question was appended; license: the workflow rule's consultation paragraph, which appends one only when both personas are consulted; the shape now reads per focusing question, or per briefing where none was appended - fixed 2026-09-14 — [Minor] the filename shape assumes a ledger ordinal for every verdict agent, which an `architect` dispatch on a bare question does not have; license: the glossary's **Consultation** entry, which records exactly that case; such a dispatch now takes the timestamp shape -- open — [Minor] the spec states as settled that a rule's condensation clause outranks an output style's system-prompt instruction, where the relative precedence is unmeasured +- fixed 2026-09-14 — [Minor] the spec stated as settled that a rule's condensation clause outranks an output style's system-prompt instruction, where the relative precedence is unmeasured; ruling: 2026-09-14; decision 1 now marks it an assumption and names the observable that falsifies it — a relay under `Concise` that drops a finding line - fixed 2026-09-14 — [Minor] "report" and "report store" collided with the glossary's **Review report**, a different object under `docs/code-review/`; license: that glossary term; the artefact is now a **dispatch record**, and W4 gains the glossary entry that mints it - signal 2026-09-14 — another round earns its cost: the four Important findings each reshape a sentence in the decision they concern, and repair-born drift between decisions 2 and 5 is the likely failure of that wave, so a diff-scoped round two attacking those fixes is worth a top-tier read; the Minors need no round of their own From 340cf08ad7de7bdababa5d0742a62fdd4a7f3edf Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Mon, 14 Sep 2026 21:48:09 +0200 Subject: [PATCH 082/120] docs: apply architect round two to the errata wave-five spec --- ...2026-09-14-review-loop-errata-wave-five.md | 71 +++++++++++++------ 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index f3a78de..6642ad1 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -3,7 +3,7 @@ ticket: none date: 2026-09-14 status: draft grilled: 2026-09-14 -architect: blocking +architect: concerns revises: ./2026-07-28-working-process-design-personas-design.md branch: feature/process-wave-five base: develop @@ -127,24 +127,35 @@ for a hole. move into the session's own memory, which is where it already fails. - Part of this restores rather than relaxes. The design-personas spec + Part of this re-aligns two shipped copies rather than relaxing + anything. The obligation exists twice by design: `PERSONA_COMMON.md` + carries the persona-facing copy and says so — *the dispatcher-facing + copy of these obligations lives in the plugin's workflow rule; the + two are edited together*. The persona copy kept the permission + (*substantially verbatim — compression is allowed, merging is not; + every recommendation and every named risk survives*); `workflow.md` + kept the floor and lost the permission, where the word `compression` + appears nowhere. Two copies declared edited together have drifted, + and the dispatcher reads the one that drifted — which is why + consultation relays grew into walls while the agents' own card said + compression was allowed all along. The design-personas spec (`./2026-07-28-working-process-design-personas-design.md`, - `status: implemented`) grants the compression: *Compression is - allowed; merging is not. The checkable floor: every recommendation - and every named risk survives, and text from two personas never - lands in one bullet.* The shipped rule carries the floor and drops - the permission — the word `compression` appears nowhere in - `workflow.md` — so a rule read literally says *verbatim*, full stop, - which is why consultation relays grew into walls. - - What this wave changes beyond that restoration is the floor's site + `status: implemented`) is where both copies come from. + + What this wave changes beyond that re-alignment is the floor's site for consultations alone, and it says so rather than hiding the - change inside a restoration. The floor's content is untouched: every - recommendation and every named risk still survives, and two personas - still never land in one bullet — in the file. `revises:` stays - absent even so, since the design-personas document's design is not - superseded: its floor holds, at a place that document could not have - named, because no file existed then. `ruling: 2026-09-14` + change inside a re-alignment. The floor's content is untouched: + every recommendation and every named risk still survives, and two + personas still never land in one bullet — in the file. + + The site is what `revises:` records. The design-personas document + places the floor in the relay; after this wave a consultation's + floor is met in the dispatch record, so that document's design no + longer matches what ships, which is the lifecycle rule's own test + for the pointer. The frontmatter therefore names it. What the + pointer does not claim is that the floor lapsed: it holds, at a + place the older document could not have named because no record + existed then. `ruling: 2026-09-14` 3. **The adversary emits the origin.** The finding schema gains `origin` beside `section`, valued `plan`, `spec`, or `both`. The @@ -223,12 +234,15 @@ for a hole. written at its first use. The location is what settles its mode. A directory the process creates in a repo to hold work artifacts is a Process directory and owes the first-create question, and - `.superpowers/` at the repo root is one — but a store under - `.claude/` is outside that class by the glossary's own carve-out, - and Private memory is the standing precedent: always ignored, never - asked, a store under the config namespace. This wave adds a second - store of that kind rather than a Process directory whose class it - would have to amend. + `.superpowers/` at the repo root is one. Private memory is the + standing exception, and it is excepted by its own definition rather + than by a rule about prefixes: the glossary says outright that + `.claude/memory/` is *not a Process directory — a per-user store + under the `.claude/` config namespace*, and the process-artifacts + rule says the same. The dispatch record takes that shape: W4's + glossary entry carries the same exclusion in the same words, so the + store is outside the class by its own definition and the class needs + no amendment. What the store guarantees is bounded, and decision 2 leans on the bound rather than on a promise. A dispatch record survives what the @@ -306,6 +320,10 @@ for a hole. gains a **Dispatch record** entry, since the rules now name an artefact the glossary does not define and the nearest defined term, **Review report**, is a different object under `docs/code-review/`. + That entry carries the store's exclusion in the words **Private + memory** uses — not a Process directory, a store under the + `.claude/` namespace — so the exclusion lives where the term is + defined rather than in the rule that uses it. ## Out of scope @@ -379,3 +397,10 @@ wrap. - fixed 2026-09-14 — [Minor] the spec stated as settled that a rule's condensation clause outranks an output style's system-prompt instruction, where the relative precedence is unmeasured; ruling: 2026-09-14; decision 1 now marks it an assumption and names the observable that falsifies it — a relay under `Concise` that drops a finding line - fixed 2026-09-14 — [Minor] "report" and "report store" collided with the glossary's **Review report**, a different object under `docs/code-review/`; license: that glossary term; the artefact is now a **dispatch record**, and W4 gains the glossary entry that mints it - signal 2026-09-14 — another round earns its cost: the four Important findings each reshape a sentence in the decision they concern, and repair-born drift between decisions 2 and 5 is the likely failure of that wave, so a diff-scoped round two attacking those fixes is worth a top-tier read; the Minors need no round of their own + +### 2026-09-14 — architect, fable 5.1, concerns (round 2, diff-scoped) + +- fixed 2026-09-14 — [Important] the `revises:` ruling landed in the frontmatter and the ledger but decision 2's closing paragraph still ruled the pointer absent, leaving two `ruling:` statements in contradiction; license: the round-one ledger line carrying `ruling: 2026-09-14, overturning the decline`, which settles which of the two stands; the paragraph now states what the pointer records — the older document places the floor in the relay, this wave meets a consultation's floor in the dispatch record, so its design no longer matches what ships — and no longer argues the overturned position +- fixed 2026-09-14 — [Minor] decision 5 argued the store's exclusion from a prefix-based carve-out the glossary does not state; license: the glossary's **Private memory** entry and the process-artifacts rule, which except that store by its own definition rather than by its prefix; the argument now runs from that precedent and W4's **Dispatch record** entry carries the same exclusion in the same words +- fixed 2026-09-14 — [Minor] decision 2 said the shipped rule dropped the permission, where the permission ships in the persona-facing copy; license: `PERSONA_COMMON.md`, which carries *compression is allowed, merging is not* and declares that the dispatcher-facing copy lives in the workflow rule and the two are edited together; W2 is now stated as re-aligning two copies that drifted, which is the stronger claim and names a defect in the shipped plugin +- signal 2026-09-14 — a further round does not earn its cost on the present text: the Important was a held decision plus a paragraph rewrite and the Minors are one sentence each; where the fixes leave anything behind, the consumption gate's integrity audit is the right reader for it From 561ca98039ee6b0730104ebd9657df36028ee8df Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 11:27:03 +0200 Subject: [PATCH 083/120] docs: close the architect loop on the errata wave-five spec --- ...2026-09-14-review-loop-errata-wave-five.md | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index 6642ad1..a955baa 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -3,7 +3,7 @@ ticket: none date: 2026-09-14 status: draft grilled: 2026-09-14 -architect: concerns +architect: concerns (resolved 2026-09-15) revises: ./2026-07-28-working-process-design-personas-design.md branch: feature/process-wave-five base: develop @@ -404,3 +404,22 @@ wrap. - fixed 2026-09-14 — [Minor] decision 5 argued the store's exclusion from a prefix-based carve-out the glossary does not state; license: the glossary's **Private memory** entry and the process-artifacts rule, which except that store by its own definition rather than by its prefix; the argument now runs from that precedent and W4's **Dispatch record** entry carries the same exclusion in the same words - fixed 2026-09-14 — [Minor] decision 2 said the shipped rule dropped the permission, where the permission ships in the persona-facing copy; license: `PERSONA_COMMON.md`, which carries *compression is allowed, merging is not* and declares that the dispatcher-facing copy lives in the workflow rule and the two are edited together; W2 is now stated as re-aligning two copies that drifted, which is the stronger claim and names a defect in the shipped plugin - signal 2026-09-14 — a further round does not earn its cost on the present text: the Important was a held decision plus a paragraph rewrite and the Minors are one sentence each; where the fixes leave anything behind, the consumption gate's integrity audit is the right reader for it + +The loop closed here on 2026-09-15, by the developer's decision and on +round 2's own stop signal, without a third round. What resolved the +concerns: every finding of both rounds reached a terminal disposition — +ten in round 1, three in round 2 — and the held set was empty at each +close. The four Important findings of round 1 were decided by the +developer and repaired; round 2 confirmed that the drift it had been +dispatched to hunt did not occur, and its own three findings were +licensed and fixed. Nothing was declined and no deviation from a +reviewer's suggestion was recorded, so the ledger above is the whole +account. + +One thing this loop produced that belongs to the plugin rather than to +this spec: `PERSONA_COMMON.md` declares that the dispatcher-facing copy +of the consultation-relay obligations lives in `workflow.md` and that +the two are edited together, and they have drifted — the persona copy +kept `compression is allowed, merging is not` and the rule copy did +not. W2 repairs that drift, which makes it a defect fix rather than the +addition this spec first took it for. From fdf023141ca17a17737d64e6a60ffe29bbcd9d21 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 11:29:53 +0200 Subject: [PATCH 084/120] docs: approve the errata wave-five spec --- docs/specs/2026-09-14-review-loop-errata-wave-five.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index a955baa..b5952b1 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -1,7 +1,7 @@ --- ticket: none date: 2026-09-14 -status: draft +status: approved grilled: 2026-09-14 architect: concerns (resolved 2026-09-15) revises: ./2026-07-28-working-process-design-personas-design.md From 38fadff278a33148de109dafe40bc94bbea6769c Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 11:40:12 +0200 Subject: [PATCH 085/120] docs: dispose the integrity audit of the errata wave-five spec --- ...2026-09-14-review-loop-errata-wave-five.md | 61 +++++++++++++------ 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index b5952b1..4dce401 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -63,9 +63,9 @@ for a hole. condensed.** The header carries the verdict, the model self-report, the finding count by severity, and how many decisions await the developer. Each finding then takes one line — severity, origin, - claim. Narrative may be condensed, and the relay names the agent's - report file so an expansion is a quotation rather than a - reconstruction. The floor stays as written. + claim. Narrative may be condensed, and the relay names the dispatch + record decision 5 defines, so an expansion is a quotation rather + than a reconstruction. The floor stays as written. The finding line carries what the finding carries. `origin` belongs to a plan review, so an architect relay's line reads severity and @@ -196,9 +196,11 @@ for a hole. Grilling minted **Origin** in the glossary, with `owner` for a document under its `_Avoid_`. `ruling: 2026-09-14` -4. **Two items left this wave during brainstorming, and one was never - in it.** Both departures are recorded here so a later reader finds - the reasoning rather than a gap. `ruling: 2026-09-14` +4. **Two items left this wave during brainstorming.** Both departures + are recorded here so a later reader finds the reasoning rather than + a gap. What never entered the wave at all is listed under *Out of + scope* instead, which is where a reader looks for it. + `ruling: 2026-09-14` The ban on process noise in code comments left because the developer refused all three of its delivery routes on a better ground than any @@ -230,8 +232,12 @@ for a hole. `docs/code-review/`. The store is `.claude/working-process/`, under the `.claude/` - namespace and ignored by a `.gitignore` containing exactly `*` - written at its first use. The location is what settles its mode. A + namespace and git-ignored by a `.gitignore` containing exactly `*` + written at its first use. It is git-ignored rather than in **Ignored + mode**: the glossary defines that term for a Process directory, and + the point of the paragraph below is that this store is not one. + Private memory is described the same way — *always ignored*, with no + mode named. The location is what settles its mode. A directory the process creates in a repo to hold work artifacts is a Process directory and owes the first-create question, and `.superpowers/` at the repo root is one. Private memory is the @@ -293,8 +299,9 @@ for a hole. 1. **W1 — the relay header and the uncondensable list.** `workflow.md`, the relay bullet under *Dispatching a verdict agent*. The bullet gains the header's contents, the one-line-per-finding - rule, and the report-file pointer. It states that condensation - reaches narrative alone, whatever a session's output style says. + rule, and the pointer to the dispatch record. It states that + condensation reaches narrative alone, whatever a session's output + style says. 2. **W2 — the consultation relay shape and its floor's site.** `workflow.md`, the *Dispatching a consultation* paragraph. One @@ -315,11 +322,12 @@ for a hole. 4. **W4 — the dispatch record.** Two sites. `workflow.md`, the verdict-agent dispatch bullet list and the consultation paragraph: both gain the write-then-relay duty and the path shape decision 5 - defines, with the ignored mode stated where the path is, so a reader - never meets the path without the mode. `docs/domain/glossary.md` - gains a **Dispatch record** entry, since the rules now name an - artefact the glossary does not define and the nearest defined term, - **Review report**, is a different object under `docs/code-review/`. + defines, stating where the path is that the store is git-ignored, so + a reader never meets the path without that fact. + `docs/domain/glossary.md` gains a **Dispatch record** entry, since + the rules now name an artefact the glossary does not define and the + nearest defined term, **Review report**, is a different object under + `docs/code-review/`. That entry carries the store's exclusion in the words **Private memory** uses — not a Process directory, a store under the `.claude/` namespace — so the exclusion lives where the term is @@ -371,9 +379,11 @@ wrap. - `plan-adversary.md` still contains its spec-boundary sentence: expected 1, an invariant this wave must not disturb. - `workflow.md` contains the spec-origin triage sentence: expected 1. -- `workflow.md` contains `.claude/working-process/` and the two - filename shapes: expected 1 each, and the word naming the store - ignored: 1. +- `workflow.md` contains the two filename shapes: expected 1 each. The + path `.claude/working-process/` is expected 3 — once where the store + is named and once inside each shape — so the check states three + rather than one, and a count of one would mean a shape went missing. + The sentence naming the store git-ignored: expected 1. - `glossary.md` contains a `**Dispatch record**:` entry: expected 1. - `workflow.md` contains no `.working-process/` outside the `.claude/` prefix: expected 0. @@ -423,3 +433,18 @@ the two are edited together, and they have drifted — the persona copy kept `compression is allowed, merging is not` and the rule copy did not. W2 repairs that drift, which makes it a defect fix rather than the addition this spec first took it for. + +### 2026-09-15 — integrity audit, fable, at the consumption gate + +Coverage tell: 425 lines read, highest line cited 424 — a whole-document +read. Seven defects and twelve implementer questions; every defect's two +quotes were checked against the files they name before any disposition +was written, and all seven held. + +- fixed 2026-09-15 — decision 1 and W1 still called the artefact a "report file" after the rename to dispatch record; license: the glossary's **Review report** entry, whose `_Avoid_` list carries `report file` verbatim; both now name the dispatch record +- fixed 2026-09-15 — decision 4's heading promised three items — two that left and one never in the wave — where its body describes two; license: the document itself, whose *Out of scope* section is where what never entered is listed; the heading now claims two and points at that section for the rest +- fixed 2026-09-15 — the verification check expected `.claude/working-process/` once in `workflow.md`, where the two prescribed filename shapes each carry the prefix; license: the check's own derivation, as a recounted expectation decides itself; the expectation is three, and the check now says a count of one would mean a shape went missing +- fixed 2026-09-15 — decision 5 and W4 called the store's state **Ignored mode**, a glossary term defined for a Process directory, in a paragraph arguing the store is not one; license: the glossary's **Ignored mode** and **Private memory** entries, the second describing the same shape as *always ignored* with no mode named; both now say git-ignored +- held — decision 2 claims `workflow.md` "kept the floor and lost the permission", and the rule kept neither: it carries `attributed and substantially verbatim, disagreements presented as disagreements` and nothing else — no recommendation clause, no named-risk clause, no one-bullet clause, zero matches for all three; question: does W2 add the floor to the rule as well as the permission, or does the rule stay floorless with the floor living only in `PERSONA_COMMON.md`?; options: (a) W2 writes both, since a dispatcher-facing rule that never states the floor is why the floor was unenforced — my recommendation; (b) W2 writes the permission only and the rule points at the persona copy, which keeps the edit small and leaves the dispatcher reading a rule that states no floor +- held — W2 edits `workflow.md` alone while `PERSONA_COMMON.md` places the consultation floor in the relay, so the wave would recreate in the other direction the drift it names as a defect; question: does `PERSONA_COMMON.md` join W2's sites?; options: (a) add it, since the two copies declare themselves edited together and this wave's own argument rests on that declaration — my recommendation; (b) leave it and accept a known drift, which forfeits the argument decision 2 is built on +- held — decision 3 prescribes where a disposition line lands for a spec-origin fix, which is `spec-plan-lifecycle.md`'s grammar, while W3 names three sites and none is that rule; question: does the lifecycle rule join W3's sites, or does decision 3 narrow to what the grammar already allows?; options: (a) add the rule as a fourth site and let it carry the cross-document clause, since the grammar today has none — my recommendation; (b) narrow decision 3 to cite the resolution annotation the rule already defines and prescribe nothing new, which is smaller but leaves the plan-side pointer undefined From 74320d82d37a91408f1e79c1865abc1642ec725a Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 12:11:34 +0200 Subject: [PATCH 086/120] docs: rule on the integrity audit's held defects and widen W2 and W3 --- ...2026-09-14-review-loop-errata-wave-five.md | 74 ++++++++++++++----- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index 4dce401..e172431 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -131,14 +131,19 @@ for a hole. anything. The obligation exists twice by design: `PERSONA_COMMON.md` carries the persona-facing copy and says so — *the dispatcher-facing copy of these obligations lives in the plugin's workflow rule; the - two are edited together*. The persona copy kept the permission + two are edited together*. The persona copy kept the whole contract (*substantially verbatim — compression is allowed, merging is not; every recommendation and every named risk survives*); `workflow.md` - kept the floor and lost the permission, where the word `compression` - appears nowhere. Two copies declared edited together have drifted, - and the dispatcher reads the one that drifted — which is why - consultation relays grew into walls while the agents' own card said - compression was allowed all along. The design-personas spec + kept neither half. It carries `attributed and substantially + verbatim, disagreements presented as disagreements` and stops there: + no recommendation clause, no named-risk clause, no one-bullet + clause, and the word `compression` nowhere. Two copies declared + edited together have drifted that far apart, and the dispatcher + reads the emptier one — which is why consultation relays grew into + walls while the agents' own card stated both the floor and the + permission all along. The floor has therefore never bound the side + that relays, which is the half of this defect the wave found last, + and the reason W2 writes the floor rather than only its site. The design-personas spec (`./2026-07-28-working-process-design-personas-design.md`, `status: implemented`) is where both copies come from. @@ -303,21 +308,42 @@ for a hole. condensation reaches narrative alone, whatever a session's output style says. -2. **W2 — the consultation relay shape and its floor's site.** - `workflow.md`, the *Dispatching a consultation* paragraph. One - paragraph per focusing question — or per briefing, where none was - appended — then the path to the contribution. - The paragraph names where the floor is met — compression yes, - merging no, every recommendation and every named risk surviving in - the file — so a reader meets the floor and its site together. +2. **W2 — the consultation relay shape and its floor's site.** Two + sites, edited together because they declare themselves so. -3. **W3 — the origin field.** Three sites, edited together: + `workflow.md`, the *Dispatching a consultation* paragraph: one + paragraph per focusing question — or per briefing, where none was + appended — then the path to the contribution. The paragraph states + the floor as well as its site, because the rule states no floor + today: it carries `attributed and substantially verbatim, + disagreements presented as disagreements` and nothing more. So the + dispatcher-facing copy gains the whole contract — compression yes, + merging no, every recommendation and every named risk surviving, two + personas never in one bullet — with the file named as where it is + met. A rule that never states a floor is why this floor went + unenforced on the side that relays. + + `PERSONA_COMMON.md`, the relay bullet of *The reply*: the same move + of the floor's site, so the two copies agree. That file already + carries the whole floor and the permission, and it declares that the + dispatcher-facing copy lives in the workflow rule and the two are + edited together. Editing one alone would recreate, in the other + direction, the very drift this wave names as the defect it repairs. + +3. **W3 — the origin field.** Four sites, edited together: `agents/plan-adversary.md` gains `origin` in the `Output` schema with its three values and one sentence placing it inside the card's existing spec boundary; `workflow.md` gains the triage sentence - decision 3 defines, beside the existing license clause; and + decision 3 defines, beside the existing license clause; `docs/domain/glossary.md` carries the **Origin** entry grilling - already minted. + already minted; and `spec-plan-lifecycle.md` gains the + cross-document clause its ledger grammar lacks — where a fix lands + in a document other than the reviewed one, that document's ledger + takes the disposition line and the reviewed document's line points + at it. The grammar owns where a line lands, so the clause belongs + there rather than in the rule that invokes it, and `every terminal + line carries exactly one authorizer` stands: one authorizer, two + documents, two lines. 4. **W4 — the dispatch record.** Two sites. `workflow.md`, the verdict-agent dispatch bullet list and the consultation paragraph: @@ -369,7 +395,15 @@ wrap. - `workflow.md` contains the header's four elements in the relay bullet, and one phrase naming the finding line as uncondensable: expected 1. - `workflow.md` contains `compression`: expected ≥ 1, where today it is - 0 — the check that proves decision 2 landed. + 0 — the check that proves decision 2 landed. It also contains the + floor's three clauses — every recommendation, every named risk, never + in one bullet — expected 1 each, and 0 today. +- `PERSONA_COMMON.md` names the dispatch record as where a + consultation's floor is met: expected 1, where today its relay bullet + places the floor in the relay alone. +- `spec-plan-lifecycle.md` contains the cross-document clause: expected + 1, and `every terminal line carries exactly one authorizer` is + unchanged: expected 1, an invariant. - `workflow.md` contains a phrase naming one paragraph per focusing question: expected 1. - `plan-adversary.md` contains `"origin"` inside the `Output` block: @@ -445,6 +479,6 @@ was written, and all seven held. - fixed 2026-09-15 — decision 4's heading promised three items — two that left and one never in the wave — where its body describes two; license: the document itself, whose *Out of scope* section is where what never entered is listed; the heading now claims two and points at that section for the rest - fixed 2026-09-15 — the verification check expected `.claude/working-process/` once in `workflow.md`, where the two prescribed filename shapes each carry the prefix; license: the check's own derivation, as a recounted expectation decides itself; the expectation is three, and the check now says a count of one would mean a shape went missing - fixed 2026-09-15 — decision 5 and W4 called the store's state **Ignored mode**, a glossary term defined for a Process directory, in a paragraph arguing the store is not one; license: the glossary's **Ignored mode** and **Private memory** entries, the second describing the same shape as *always ignored* with no mode named; both now say git-ignored -- held — decision 2 claims `workflow.md` "kept the floor and lost the permission", and the rule kept neither: it carries `attributed and substantially verbatim, disagreements presented as disagreements` and nothing else — no recommendation clause, no named-risk clause, no one-bullet clause, zero matches for all three; question: does W2 add the floor to the rule as well as the permission, or does the rule stay floorless with the floor living only in `PERSONA_COMMON.md`?; options: (a) W2 writes both, since a dispatcher-facing rule that never states the floor is why the floor was unenforced — my recommendation; (b) W2 writes the permission only and the rule points at the persona copy, which keeps the edit small and leaves the dispatcher reading a rule that states no floor -- held — W2 edits `workflow.md` alone while `PERSONA_COMMON.md` places the consultation floor in the relay, so the wave would recreate in the other direction the drift it names as a defect; question: does `PERSONA_COMMON.md` join W2's sites?; options: (a) add it, since the two copies declare themselves edited together and this wave's own argument rests on that declaration — my recommendation; (b) leave it and accept a known drift, which forfeits the argument decision 2 is built on -- held — decision 3 prescribes where a disposition line lands for a spec-origin fix, which is `spec-plan-lifecycle.md`'s grammar, while W3 names three sites and none is that rule; question: does the lifecycle rule join W3's sites, or does decision 3 narrow to what the grammar already allows?; options: (a) add the rule as a fourth site and let it carry the cross-document clause, since the grammar today has none — my recommendation; (b) narrow decision 3 to cite the resolution annotation the rule already defines and prescribe nothing new, which is smaller but leaves the plan-side pointer undefined +- fixed 2026-09-15 — decision 2 claimed `workflow.md` "kept the floor and lost the permission" where the rule kept neither: it carries `attributed and substantially verbatim, disagreements presented as disagreements` and nothing else, with zero matches for all three floor clauses; ruling: 2026-09-15; the claim is corrected and W2 now writes the floor into the rule as well as the permission, on the ground that a dispatcher-facing rule stating no floor is why the floor went unenforced on the side that relays +- fixed 2026-09-15 — W2 edited `workflow.md` alone while `PERSONA_COMMON.md` places the consultation floor in the relay, so the wave would have recreated its own named defect in the other direction; ruling: 2026-09-15; `PERSONA_COMMON.md` joins W2 as a second site, edited together as both files declare +- fixed 2026-09-15 — decision 3 prescribed where a disposition line lands, which is `spec-plan-lifecycle.md`'s grammar, while W3 named three sites and not that rule; ruling: 2026-09-15; the rule joins W3 as a fourth site and carries the cross-document clause its grammar lacks, leaving `every terminal line carries exactly one authorizer` intact From 309221299bb64ec10477a7042d5f5ff0517d2596 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 12:11:53 +0200 Subject: [PATCH 087/120] style: rewrap the wave-five spec's design-personas citation --- docs/specs/2026-09-14-review-loop-errata-wave-five.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index e172431..2da915d 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -143,7 +143,8 @@ for a hole. walls while the agents' own card stated both the floor and the permission all along. The floor has therefore never bound the side that relays, which is the half of this defect the wave found last, - and the reason W2 writes the floor rather than only its site. The design-personas spec + and the reason W2 writes the floor rather than only its site. The + design-personas spec (`./2026-07-28-working-process-design-personas-design.md`, `status: implemented`) is where both copies come from. From 163ed700f12f7cc8aad91b722a45c06b66e6c599 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 12:13:03 +0200 Subject: [PATCH 088/120] docs: dispose the integrity audit's implementer questions --- ...2026-09-14-review-loop-errata-wave-five.md | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index 2da915d..252491b 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -80,8 +80,12 @@ for a hole. report and the decisions already written down, so the count is computable before any ledger line is. The `held` lines the same round later writes are that count's check — they must come to the - same number, and a divergence means triage found a license the - relay missed or lost one it claimed. Deriving the count from the + same number, and a divergence means one of three things: triage + found a license the relay missed, triage lost one the relay claimed, + or the oscillation tripwire held a finding that does carry a + license, which the rules already require. The third is lawful and + the first two are errors, so the check reports the number rather + than asserting a fault. Deriving the count from the lines themselves would have the header report a forecast, since `open` is written at stamp time and triage follows the stamp, which follows the relay. Verdicts that suspend autonomy — `blocking`, the @@ -271,6 +275,13 @@ for a hole. it follows the shape `subagent-driven-development` already uses for its own workspace. + A dispatch that reviews no document takes its subject as the stem + too, whatever kind of dispatch it is. An `architect` dispatch on a + bare question is a review round rather than a consultation — it + grades and stamps nothing — so the rule keys on whether a document + exists rather than on the agent's kind, and both the stem and the + timestamp follow from that one test. + A subject becomes a stem mechanically: the briefing's own naming of it, kebab-cased and capped at sixty characters. The briefing already has to name the subject, so nothing is invented. The limit is worth @@ -294,6 +305,14 @@ for a hole. dispatch rather than a resumption. The timestamp also orders them, which a counter would not. + The record is written before the relay and the round heading is + written after it, so the ordinal the filename needs does not yet + exist in the ledger. The dispatcher derives it the way the heading + will: the highest ordinal the document's rounds carry, plus one, + ordinals continuing across loops. A parallel round from another + session can collide there, which the rule already accepts as + undetectable and which the next clause resolves mechanically. + Where a name is already taken, the new file appends the timestamp. That covers the superseded round the rules already describe, whose stale result is relayed and never stamped: the two rounds share an @@ -355,10 +374,12 @@ for a hole. the rules now name an artefact the glossary does not define and the nearest defined term, **Review report**, is a different object under `docs/code-review/`. - That entry carries the store's exclusion in the words **Private - memory** uses — not a Process directory, a store under the - `.claude/` namespace — so the exclusion lives where the term is - defined rather than in the rule that uses it. + That entry carries the store's exclusion the way **Private memory** + carries its own — stating outright that it is not a Process + directory, and why — so the exclusion lives where the term is + defined rather than in the rule that uses it. The words differ where + the facts differ: Private memory is per-user, and a dispatch record + store is per-checkout, shared by whoever works in it. ## Out of scope @@ -483,3 +504,24 @@ was written, and all seven held. - fixed 2026-09-15 — decision 2 claimed `workflow.md` "kept the floor and lost the permission" where the rule kept neither: it carries `attributed and substantially verbatim, disagreements presented as disagreements` and nothing else, with zero matches for all three floor clauses; ruling: 2026-09-15; the claim is corrected and W2 now writes the floor into the rule as well as the permission, on the ground that a dispatcher-facing rule stating no floor is why the floor went unenforced on the side that relays - fixed 2026-09-15 — W2 edited `workflow.md` alone while `PERSONA_COMMON.md` places the consultation floor in the relay, so the wave would have recreated its own named defect in the other direction; ruling: 2026-09-15; `PERSONA_COMMON.md` joins W2 as a second site, edited together as both files declare - fixed 2026-09-15 — decision 3 prescribed where a disposition line lands, which is `spec-plan-lifecycle.md`'s grammar, while W3 named three sites and not that rule; ruling: 2026-09-15; the rule joins W3 as a fourth site and carries the cross-document clause its grammar lacks, leaving `every terminal line carries exactly one authorizer` intact + +The audit's twelve implementer questions were disposed by where their +answer belongs. Four were design and are answered in the decisions +above: the stem for a dispatch that reviews no document (the rule keys +on whether a document exists, not on the agent's kind), a third and +lawful cause of divergence in the decisions count (the oscillation +tripwire holds a licensed finding), the glossary entry's wording (it +cannot copy **Private memory**'s words, since that store is per-user +and this one per-checkout), and the ordinal a filename needs before the +heading that owns it is written. + +Five are implementation detail and belong to the plan, which is where +prescribed text and published checks are written: the exact clause +excluding audits from the store, the exact concatenation a collision +produces, which of two candidate sentences in the adversary's card +carries the `origin` boundary, the concrete patterns behind two checks +stated in prose, and the `.gitignore` edge cases at first use. + +- held — the dispatch record's own content and shape are unspecified: decision 5 says the dispatcher writes what the agent returned, and nothing about whether that is the returned text verbatim, whether a header carries date, agent, model self-report and subject, whether the file takes frontmatter, or what an agent returning JSON writes into a Markdown file; question: what does a dispatch record contain?; options: (a) the returned text verbatim under a four-line header — date, agent, model self-report, subject — and no frontmatter, since the record is evidence rather than a process artefact and the store is outside the class that owes `ticket` — my recommendation; (b) a process artefact with frontmatter, which makes it findable by the ticket sweep at the cost of putting a Process-directory convention on a store the wave argues is not one +- held — a spec-origin fix on a spec whose loop closed at `LGTM` has no round heading to land under, and the resolution annotation the lifecycle rule defines exists only for `concerns`; the same fix re-arms the spec's `integrity:` hash, which decision 3 does not mention while it does mention the architect stamp; question: where does the line land on such a spec, and what happens to a standing `integrity:` stamp?; options: (a) the fix opens a new heading of its own, dated and naming no round, and the `integrity:` stamp goes stale by the rule that already covers any body edit, which decision 3 states rather than invents — my recommendation; (b) the fix is refused on a spec with no open heading, sending it back to the developer as a fresh round, which is safer and costs a round for a clause +- held — the consultation relay's contract carries `disagreements presented as disagreements` in both copies, and the new shape says nothing about whether that presentation stays in the relay or moves to the file with the floor; question: where is a disagreement presented after this wave?; options: (a) it stays in the relay, since a disagreement is the one thing the developer must decide and the digest exists to surface decisions — my recommendation; (b) it moves with the floor, which is simpler to state and buries the decision the relay exists to raise From fa77c9925b6173740cb80cd660c6bf8ba45406f1 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 12:18:06 +0200 Subject: [PATCH 089/120] docs: rule on the integrity audit's held questions --- ...2026-09-14-review-loop-errata-wave-five.md | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index 252491b..730223e 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -122,7 +122,11 @@ for a hole. of twenty findings still relays in twenty lines. The consultation relay then carries one paragraph per focusing question — or one per briefing, where a single persona was consulted and none was appended - — and the path to the contribution. + — and the path to the contribution. One thing does not move with the + floor: where the personas disagree, the disagreement is presented as + a disagreement, in the relay, with both positions. A disagreement is + the one thing in a contribution the developer must decide, and the + digest exists to raise decisions rather than bury them. The move rests on decision 5's dispatch record, and on the bound that decision states: the record survives compaction, the session's @@ -185,7 +189,14 @@ for a hole. than the plan's. Its disposition line lands under the spec's latest round heading, or as the resolution annotation the lifecycle rule already defines for a stamped document edited without a fresh round, - and the plan's line points at it. One authorizer per line still + and the plan's line points at it. Where the spec's loop closed at + `LGTM` it has neither — the annotation is defined for `concerns` + alone — so the fix opens a heading of its own: dated, naming the fix + rather than a round, and carrying the one line. Two consequences + this decision states rather than invents: a spec's standing + `integrity:` stamp goes stale on any body edit, this one included, + by the rule that already says so; and the architect stamp stops + certifying the words that changed. One authorizer per line still holds — one authorizer, two documents, two lines — and the spec's stamp stops certifying words that are gone. @@ -268,6 +279,16 @@ for a hole. the document branch. That is the same durability Private memory has and the same the developer relies on daily. + A record contains what the agent returned, verbatim, under a header + of four lines: the date, the agent, the model self-report, and the + subject. Nothing else — no frontmatter, no `ticket`. The record is + evidence rather than a process artefact, and the store sits outside + the class that owes a ticket field, so adding one would import a + Process-directory convention into a store this decision argues is + not one. An agent returning structured output writes it as it came, + fenced; the header is the only thing the dispatcher adds, and it + exists so a reader who opens the file cold knows what produced it. + One directory per subject, named for it — ``, the reviewed document's basename without its extension. A consultation carrying no document takes its subject as the stem. Grouping by subject @@ -522,6 +543,6 @@ produces, which of two candidate sentences in the adversary's card carries the `origin` boundary, the concrete patterns behind two checks stated in prose, and the `.gitignore` edge cases at first use. -- held — the dispatch record's own content and shape are unspecified: decision 5 says the dispatcher writes what the agent returned, and nothing about whether that is the returned text verbatim, whether a header carries date, agent, model self-report and subject, whether the file takes frontmatter, or what an agent returning JSON writes into a Markdown file; question: what does a dispatch record contain?; options: (a) the returned text verbatim under a four-line header — date, agent, model self-report, subject — and no frontmatter, since the record is evidence rather than a process artefact and the store is outside the class that owes `ticket` — my recommendation; (b) a process artefact with frontmatter, which makes it findable by the ticket sweep at the cost of putting a Process-directory convention on a store the wave argues is not one -- held — a spec-origin fix on a spec whose loop closed at `LGTM` has no round heading to land under, and the resolution annotation the lifecycle rule defines exists only for `concerns`; the same fix re-arms the spec's `integrity:` hash, which decision 3 does not mention while it does mention the architect stamp; question: where does the line land on such a spec, and what happens to a standing `integrity:` stamp?; options: (a) the fix opens a new heading of its own, dated and naming no round, and the `integrity:` stamp goes stale by the rule that already covers any body edit, which decision 3 states rather than invents — my recommendation; (b) the fix is refused on a spec with no open heading, sending it back to the developer as a fresh round, which is safer and costs a round for a clause -- held — the consultation relay's contract carries `disagreements presented as disagreements` in both copies, and the new shape says nothing about whether that presentation stays in the relay or moves to the file with the floor; question: where is a disagreement presented after this wave?; options: (a) it stays in the relay, since a disagreement is the one thing the developer must decide and the digest exists to surface decisions — my recommendation; (b) it moves with the floor, which is simpler to state and buries the decision the relay exists to raise +- fixed 2026-09-15 — the dispatch record's content and shape were unspecified, decision 5 saying only that the dispatcher writes what the agent returned; ruling: 2026-09-15; a record is the returned text verbatim under a four-line header — date, agent, model self-report, subject — with no frontmatter and no `ticket`, since it is evidence rather than a process artefact and the store lies outside the class that owes the field +- fixed 2026-09-15 — a spec-origin fix on a spec closed at `LGTM` had no heading to land under, the resolution annotation being defined for `concerns` alone, and decision 3 said nothing about the spec's `integrity:` stamp; ruling: 2026-09-15; such a fix opens a dated heading of its own naming the fix rather than a round, and the decision now states both consequences — the `integrity:` hash goes stale by the rule that already covers any body edit, and the architect stamp stops certifying the changed words +- fixed 2026-09-15 — the new consultation shape said nothing about where a disagreement between personas is presented once the floor moved to the file; ruling: 2026-09-15; it stays in the relay with both positions, because a disagreement is the one thing in a contribution the developer must decide and the digest exists to raise decisions From 878a8ae318cae380177e44af49e338ae3626d370 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 12:18:18 +0200 Subject: [PATCH 090/120] docs: stamp the integrity audit on the errata wave-five spec --- docs/specs/2026-09-14-review-loop-errata-wave-five.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index 730223e..d21a5a0 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -4,6 +4,7 @@ date: 2026-09-14 status: approved grilled: 2026-09-14 architect: concerns (resolved 2026-09-15) +integrity: 2026-09-15 (sha: 2673d32) revises: ./2026-07-28-working-process-design-personas-design.md branch: feature/process-wave-five base: develop From c4fb211141fade0bf8bfa594c90093fc0d35b2ea Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 13:17:56 +0200 Subject: [PATCH 091/120] docs: write the implementation plan for errata wave five --- ...2026-09-15-review-loop-errata-wave-five.md | 1049 +++++++++++++++++ 1 file changed, 1049 insertions(+) create mode 100644 docs/plans/2026-09-15-review-loop-errata-wave-five.md diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md new file mode 100644 index 0000000..89d1fe3 --- /dev/null +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -0,0 +1,1049 @@ +--- +ticket: none +date: 2026-09-15 +status: draft +spec: ../specs/2026-09-14-review-loop-errata-wave-five.md +branch: feature/process-wave-five +base: develop +--- + +# Review loop — errata wave five Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> superpowers:subagent-driven-development (recommended) or +> superpowers:executing-plans to implement this plan task-by-task. Steps +> use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Land the four changes of +`docs/specs/2026-09-14-review-loop-errata-wave-five.md` — the relay +header, the consultation relay and its floor's new site, the adversary's +`origin` field, and the dispatch record — across the `working-process` +plugin and this repo's glossary. + +**Architecture:** Every deliverable is prose in a shipped rule, agent +card or glossary entry. There is no code and no test suite; a task's +test is the grep pair it publishes, run before and after the edit. Tasks +are drawn one per site, so no two touch the same text — `workflow.md` +takes three tasks because the wave edits three sections of it that share +no line. + +**Tech Stack:** Markdown rule and agent files distributed as a Claude +Code Rules payload; `grep`, `tr` and `claude plugin validate` for +verification. + +## Global Constraints + +- **The spec is the source.** Where this plan and the spec disagree, the + spec wins and the plan is wrong — except where *Deviations from the + spec* below records a departure and its reason. +- **Prose wraps at 72 characters** in every rule, agent card and + glossary file. Match the surrounding paragraph; never reflow a + paragraph this plan does not change. +- **A check over prose normalizes whitespace first** + (`tr -s '[:space:]' ' '`), so a phrase matches wherever a line wraps. + Only a check anchoring something that cannot wrap — a path, a filename + shape, a heading — is written plain. +- **Every step states its before value and its after value**, and an + invariant says so and says why. The before values in this plan were + measured against the files on 2026-09-15, not predicted. +- **`workflow.md` carries the literal `.claude/working-process/` + exactly three times** when the wave is done — once where the store is + named, once in each of the two filename shapes. The spec's own check + expects three, so no other task may add a fourth. +- **The glossary binds.** `docs/domain/glossary.md` terms and `_Avoid_` + bans govern this text. `Origin`, `Relay`, `Contribution`, + `Consultation`, `Review report`, `Private memory` and `Process + directory` are canonical here; `report file` is banned by **Review + report**'s `_Avoid_` list and appears nowhere in a replacement block. +- **Public repo hygiene**: no machine-specific paths, no company or + client names, all committed text in English. +- **The replacement texts already carry the elements-of-style pass.** + It was applied to every Replace block at authoring, so an implementer + copying a block verbatim is complying rather than skipping it. A block + an implementer rewords leaves that guarantee and owes the pass again. +- **Commit messages are one line** — a conventional-commit subject, no + body, no trailers. +- **Do not run `sync-rules`, do not bump the plugin version, and do not + move the spec's `status`.** All three are the developer's, and the + spec puts the first outside the wave. + +## Deviations from the spec + +Recorded here and beside the text they concern, so a reviewer trips over +the reason where the disagreement lives. + +1. **Task 7 amends the glossary's **Relay** and **Contribution** + entries, which the spec's W4 does not name.** Both state the floor + this wave moves: **Relay** says a consultation's Contribution reaches + the developer "attributed and substantially verbatim", and + **Contribution** repeats it. The glossary binds specs, plans and + reviews, so leaving them would ship a glossary contradicting the rule + the same wave writes — the propagation duty an added field owes both + ends of its chain. Task 7 carries the reason. +2. **Task 4 amends the two `*-consult` cards**, which the spec's W2 does + not name either. Each tells its persona "the contract has it relayed + attributed and substantially verbatim", which stops being true of the + relay once the floor moves to the file. The sentence's purpose — + write for the developer, not as a report to the dispatcher — + survives the correction and is why the sentence stays rather than + going. +3. **W3's glossary site is already in the file.** The **Origin** entry + was minted at the grilling, so this plan verifies it as an invariant + instead of writing it. Measured at authoring: one entry present. +4. **The consultation paragraph points at the filename shapes rather + than repeating them.** The spec's check expects exactly three + occurrences of `.claude/working-process/` in `workflow.md`, and + Task 1 writes all three; Task 2 names "the timestamp shape of the two + the verdict-agent dispatch subsection defines" instead. + +## File structure + +Modified: + +- `plugins/working-process/rules/workflow.md` — Task 1 (the + completion-notification bullet and the relay bullet, inside + *Dispatching a verdict agent*), Task 2 (the consultation paragraph, + above *Branch naming*) and Task 3 (one bullet in the triage list of + *The review loop*). The three tasks share the file and touch no common + line. +- `plugins/working-process/PERSONA_COMMON.md` — Task 4: the first bullet + of *The reply* in the consultation contract. +- `plugins/working-process/agents/architect-consult.md` and + `plugins/working-process/agents/system-designer-consult.md` — Task 4: + one *Hard limits* bullet each. +- `plugins/working-process/agents/plan-adversary.md` — Task 5: the + `Output` schema and the close of *Specs: decline*. +- `plugins/working-process/rules/spec-plan-lifecycle.md` — Task 6: one + passage after the one-authorizer invariant in *The disposition + ledger*. +- `docs/domain/glossary.md` — Task 7: **Relay** and **Contribution** + amended, **Dispatch record** minted between them. + +Created: nothing. + +Not modified, and why: `plugins/working-process/README.md` describes the +relay-then-stamp sequence and names the files that hold the consultation +contract, and this wave changes neither — no count, list or file name in +it moves. The two `*-auditor` cards are out of scope by the spec's own +*Out of scope* section: audits write no dispatch record. + +## Order and independence + +- Tasks 1, 2 and 3 all edit `workflow.md`. Task 1 runs first because + Task 2's text points at the filename shapes Task 1 writes; Task 3 is + independent of both. Every check anchors text rather than a line + number, so the order is a convenience except for that one pointer. +- Task 3's bullet cites "the cross-document clause the + spec-plan-lifecycle rule defines", which Task 6 writes. The citation + names a rule and a clause rather than an anchor or a line, so it does + not break if Task 6 runs later — but a reviewer reading Task 3 alone + should know Task 6 is what makes it true. +- Task 5 is the only task touching an agent card that carries a schema; + nothing else reads that schema, and the triage sentence Task 3 writes + consumes the field by name rather than by position. +- `claude plugin validate` runs once, at Task 8, after every file has + changed. Validation before then proves nothing about the end state, + and it never reads `rules/` at all — see Task 8. + +--- + +### Task 1: `workflow.md` — the relay header and the dispatch record + +**Files:** +- Modify: `plugins/working-process/rules/workflow.md` — the + completion-notification bullet and the relay bullet of + `## Dispatching a verdict agent`. + +**Interfaces:** +- Consumes: nothing from earlier tasks. +- Produces: the two filename shapes and the store path that Task 2's + consultation paragraph and Task 7's glossary entry both point at, and + the three occurrences of `.claude/working-process/` the Global + Constraints cap. + +- [ ] **Step 1: Record the before values** + +```bash +W=plugins/working-process/rules/workflow.md +n() { tr -s '[:space:]' ' ' < "$1"; } +echo "A $(n $W | grep -o 'The relay opens with one header line' | wc -l)" +echo "B $(n $W | grep -o 'finding list is never condensed' | wc -l)" +echo "C $(n $W | grep -o 'output style' | wc -l)" +echo "D $(grep -c '\.claude/working-process/' $W)" +echo "E $(grep -c '\.working-process/' $W)" +echo "F $(grep -c '-round-\.md' $W)" +echo "G $(grep -c '--\.md' $W)" +echo "H $(n $W | grep -o 'git-ignored by a .\.gitignore. containing exactly' | wc -l)" +echo "I $(n $W | grep -o 'write the dispatch record defined below' | wc -l)" +``` + +Expected: `A 0`, `B 0`, `C 0`, `D 0`, `E 0`, `F 0`, `G 0`, `H 0`, `I 0`. + +`D` and `E` are the same count measured two ways: `E` catches a store +path written without the `.claude/` prefix, which the spec forbids, so +the two must stay equal at every later reading. + +- [ ] **Step 2: Put the record into the completion sequence** + +Find: + +``` +- On the completion notification, in one turn and in this order: + verify the agent's model self-report (the comparison the lifecycle + rule defines), relay the report to the developer, check every + citation the report supplies against what it names, then stamp the + verdict (`LGTM` | `concerns` | `blocking`) into the reviewed + document's `architect:` / `adversary:` frontmatter field. The + citation check follows the relay rather than preceding it, because a + wrong citation in conversation costs a correction while a wrong one + in the record outlives the loop. The order + has one named exception, defined under *What a diff-scoped LGTM + certifies* below: a plan's diff-scoped LGTM. +``` + +Replace with: + +``` +- On the completion notification, in one turn and in this order: + verify the agent's model self-report (the comparison the lifecycle + rule defines), write the dispatch record defined below, relay the + report to the developer, check every citation the report supplies + against what it names, then stamp the verdict (`LGTM` | `concerns` | + `blocking`) into the reviewed document's `architect:` / + `adversary:` frontmatter field. The citation check follows the relay + rather than preceding it, because a wrong citation in conversation + costs a correction while a wrong one in the record outlives the + loop. The order has one named exception, defined under *What a + diff-scoped LGTM certifies* below: a plan's diff-scoped LGTM. +``` + +- [ ] **Step 3: Replace the relay bullet with five** + +Find: + +``` +- The relay carries the verdict, the model self-report, and every + finding in substance — condense narrative prose, never drop a + finding or its severity. +``` + +Replace with: + +``` +- The dispatch record is written before the relay. The dispatcher + saves what the agent returned, verbatim, under a four-line header — + the date, the agent, the model self-report, and the subject — with + no frontmatter and no `ticket`, since the record is evidence rather + than a process artefact. It goes to `.claude/working-process/`, a + store git-ignored by a `.gitignore` containing exactly `*` written + at its first use. A record survives compaction, the session's end + and a branch switch, and no more; that bound is what the relay's + condensation below leans on. Audits write none — a propagation + gate's hits land in the ledger and an integrity audit's dispositions + land as the edits it causes plus the `integrity:` stamp, so a record + would give one fact a second home. +- The record's name carries its agent and one discriminator: + + .claude/working-process//-round-.md + .claude/working-process//--.md + + `` is the reviewed document's basename without its extension, + one directory per subject. A verdict agent takes the round ordinal, + derived the way the heading will derive it — the highest ordinal the + document's rounds carry, plus one, ordinals continuing across + loops — because the record is written before that heading exists. A + dispatch that reviews no document takes its subject as the stem, + kebab-cased and capped at sixty characters, and the timestamp shape, + having no ledger to draw an ordinal from; an `architect` dispatch on + a bare question is one such, and the test is whether a document + exists rather than which agent ran. Where a name is already taken, + the new file appends the timestamp, which settles the superseded + round two sessions can produce on one ordinal without asking either + to judge which report is stale. +- The relay opens with one header line, then one line per finding. The + header carries the verdict, the model self-report, the finding count + by severity, and how many decisions await the developer. Each + finding takes one line — severity, origin, claim — except where the + reviewer emits no origin: the `architect` reads one document and + every finding it returns originates there, so an architect relay's + line carries severity and claim alone. +- Condensation reaches narrative prose alone, whatever a session's + output style prescribes. The finding list is never condensed, and no + finding or its severity is ever dropped: the relay is the + developer's standing veto, so a finding nobody printed is a finding + nobody could overrule. Narrative is safe to condense because the + dispatch record holds the full text, which makes an expansion a + quotation rather than a reconstruction. +- The header's count of decisions is the findings for which the + session can cite no license, derived at relay time from the report + and the decisions already written down — before any ledger line + exists, since `open` is written at stamp time and triage follows the + stamp. The `held` lines the same round later writes are that count's + check: they come to the same number, and a divergence means triage + found a license the relay missed, triage lost one the relay claimed, + or the oscillation tripwire held a finding that does carry a + license. The third is lawful, so the check reports the number rather + than asserting a fault. Verdicts that suspend autonomy — `blocking`, + the spent round cap, the all-Minor signal — stay out of the count: + they decide whether the loop continues rather than what a document + says, and the relay reports them in its own right. +``` + +The indented block is six spaces, which is the bullet's two-space +content indent plus the four a code block needs. It renders as a code +block and is exempt from the wrap constraint, like every grammar example +the rules already carry. + +- [ ] **Step 4: Verify** + +Run the Step 1 command again. + +Expected: `A 1`, `B 1`, `C 1`, `D 3`, `E 3`, `F 1`, `G 1`, `H 1`, +`I 1`. + +`D 3` is the Global Constraints cap: once in the store sentence, once in +each filename shape. A count of one would mean a shape went missing, +and `D` differing from `E` would mean a path lost its `.claude/` prefix. + +- [ ] **Step 5: Commit** + +```bash +git add plugins/working-process/rules/workflow.md +git commit -m "feat(working-process): give the verdict relay a header and a dispatch record" +``` + +--- + +### Task 2: `workflow.md` — the consultation relay and its floor's site + +**Files:** +- Modify: `plugins/working-process/rules/workflow.md` — the + `Dispatching a consultation` paragraph, between the model-selection + paragraph and the glossary paragraph. + +**Interfaces:** +- Consumes: Task 1's filename shapes, which this paragraph names rather + than repeats. +- Produces: nothing later tasks read. Task 4 writes the persona-facing + copy of the same floor and the two are edited together, as both files + declare. + +- [ ] **Step 1: Record the before values** + +```bash +W=plugins/working-process/rules/workflow.md +n() { tr -s '[:space:]' ' ' < "$1"; } +echo "A $(n $W | grep -o 'compression' | wc -l)" +echo "B $(n $W | grep -o 'every recommendation and every named risk' | wc -l)" +echo "C $(n $W | grep -o 'never .\{0,12\}in one bullet' | wc -l)" +echo "D $(n $W | grep -o 'paragraph per focusing question' | wc -l)" +echo "E $(n $W | grep -o 'focusing question' | wc -l)" +echo "F $(grep -c '\.claude/working-process/' $W)" +echo "G $(n $W | grep -o 'Relay each contribution attributed' | wc -l)" +``` + +Expected: `A 0`, `B 0`, `C 0`, `D 0`, `E 1`, `F 3`, `G 1`. + +`A 0`, `B 0` and `C 0` are the measurement behind decision 2: the rule +that governs the dispatcher states no floor at all today. `E 1` counts +the briefing's existing mention of a focusing question, which this task +keeps; `F 3` is Task 1's cap, an invariant here. + +- [ ] **Step 2: Rewrite the paragraph's close and add the shape** + +Find: + +``` +never a resumption. When both personas are consulted on one subject, +give both the same canonical briefing, each with its own focusing +question appended, and tell neither what the other said. Relay each +contribution attributed and substantially verbatim, disagreements +presented as disagreements, and dispatch as a named background agent so +the transcript stays inspectable. +``` + +Replace with: + +``` +never a resumption. When both personas are consulted on one subject, +give both the same canonical briefing, each with its own focusing +question appended, and tell neither what the other said. Dispatch as a +named background agent, so the transcript stays inspectable. + +A contribution is written to its dispatch record before it is relayed, +taking the timestamp shape of the two the verdict-agent dispatch +subsection defines: a consultation records nothing, so it has no +ordinal to derive, and several consultations of one persona on one +subject in one day are expected. The floor is met in that file — +attributed and substantially verbatim, compression allowed and merging +forbidden, every recommendation and every named risk surviving, and +text from two personas never landing in one bullet. The relay then +carries one paragraph per focusing question — or one per briefing, +where a single persona was consulted and none was appended — and the +path to the record. One thing stays in the relay whatever the floor +does: where the personas disagree, the disagreement is presented as a +disagreement with both positions, because a disagreement is the one +thing in a contribution the developer must decide and the digest +exists to raise decisions rather than bury them. +``` + +- [ ] **Step 3: Verify** + +Run the Step 1 command again. + +Expected: `A 1`, `B 1`, `C 1`, `D 1`, `E 2`, `F 3`, `G 0`. + +`A 1` is the check the spec names as proof decision 2 landed. `E 2` is +the briefing's mention plus the relay's. `F 3` unchanged is the +deviation working: this paragraph points at the shapes instead of +repeating them. + +- [ ] **Step 4: Commit** + +```bash +git add plugins/working-process/rules/workflow.md +git commit -m "feat(working-process): state the consultation floor and digest its relay" +``` + +--- + +### Task 3: `workflow.md` — triage holds a spec-origin finding + +**Files:** +- Modify: `plugins/working-process/rules/workflow.md` — the consequences + list of the triage paragraph in `### The review loop`, after the + "Only written decisions license fixes" bullet. + +**Interfaces:** +- Consumes: the field name `origin`, which Task 5 adds to the + adversary's schema, and the cross-document clause Task 6 writes into + the lifecycle rule — cited by rule and clause name, never by anchor. +- Produces: nothing later tasks read. + +- [ ] **Step 1: Record the before values** + +```bash +W=plugins/working-process/rules/workflow.md +n() { tr -s '[:space:]' ' ' < "$1"; } +echo "A $(n $W | grep -o 'origin' | wc -l)" +echo "B $(n $W | grep -o 'editing a spec from inside a plan review is design work' | wc -l)" +echo "C $(n $W | grep -o 'Only written decisions license fixes' | wc -l)" +echo "D $(grep -ci 'owner' $W)" +``` + +Expected: `A 1`, `B 0`, `C 1`, `D 0`. + +`A 1` counts the single occurrence Task 1 wrote into the relay bullet — +run this task after Task 1 or the expectation is `A 0`, and either way +the after value is one higher. `C` is an invariant: the bullet this task +follows must survive it. `D` is the collision check the spec publishes, +and it is an invariant in both directions: `owner` is spent on an actor +elsewhere in the process and must never name a document here. + +- [ ] **Step 2: Add the spec-origin bullet** + +Find: + +``` +- Only written decisions license fixes. A decision settled in + conversation becomes citable by being written into the document, + which the fix itself accomplishes. +``` + +Replace with: + +``` +- Only written decisions license fixes. A decision settled in + conversation becomes citable by being written into the document, + which the fix itself accomplishes. +- A finding whose `origin` names the spec is held unless a written + decision licenses the edit, since editing a spec from inside a plan + review is design work; `both` holds the same way, and its `held` + line names in `options:` which half is fixable at once. A licensed + spec-origin fix lands in the spec's own ledger and the plan's line + points at it, by the cross-document clause the spec-plan-lifecycle + rule defines — which also leaves the spec's `integrity:` stamp + stale, as any body edit does. +``` + +- [ ] **Step 3: Verify** + +Run the Step 1 command again. + +Expected: `A 2`, `B 1`, `C 1`, `D 0`. + +- [ ] **Step 4: Commit** + +```bash +git add plugins/working-process/rules/workflow.md +git commit -m "feat(working-process): hold a spec-origin finding at triage" +``` + +--- + +### Task 4: the persona-facing copies of the consultation floor + +**Files:** +- Modify: `plugins/working-process/PERSONA_COMMON.md` — the first bullet + of *The reply* in `## The consultation contract`. +- Modify: `plugins/working-process/agents/architect-consult.md` — one + bullet in `## Hard limits`. +- Modify: `plugins/working-process/agents/system-designer-consult.md` — + the same bullet in its own `## Hard limits`. + +**Interfaces:** +- Consumes: Task 2's dispatcher-facing text. `PERSONA_COMMON.md` + declares that the dispatcher-facing copy lives in the workflow rule + and that the two are edited together, which is why this task exists: + editing one alone would recreate, in the other direction, the drift + this wave repairs. +- Produces: nothing later tasks read. + +- [ ] **Step 1: Record the before values** + +```bash +P=plugins/working-process/PERSONA_COMMON.md +AC=plugins/working-process/agents/architect-consult.md +SD=plugins/working-process/agents/system-designer-consult.md +n() { tr -s '[:space:]' ' ' < "$1"; } +echo "A $(n $P | grep -o 'relayed to the developer attributed' | wc -l)" +echo "B $(n $P | grep -o 'dispatch record' | wc -l)" +echo "C $(n $P | grep -o 'every recommendation and every named risk survives' | wc -l)" +echo "D $(n $P | grep -o 'edited together' | wc -l)" +echo "E $(n $AC | grep -o 'relayed attributed and substantially verbatim' | wc -l)" +echo "F $(n $SD | grep -o 'relayed attributed and substantially verbatim' | wc -l)" +echo "G $(n $AC | grep -o 'kept attributed and substantially verbatim' | wc -l)" +echo "H $(n $SD | grep -o 'kept attributed and substantially verbatim' | wc -l)" +``` + +Expected: `A 1`, `B 0`, `C 1`, `D 1`, `E 1`, `F 1`, `G 0`, `H 0`. + +`C` and `D` are invariants: the floor's content does not move, only its +site, and the sentence declaring the two copies edited together is what +licenses this task. + +- [ ] **Step 2: Move the floor's site in `PERSONA_COMMON.md`** + +Find: + +``` +- relayed to the developer attributed, in its own block, and + substantially verbatim — compression is allowed, merging is not; every + recommendation and every named risk survives, and text from two + personas never lands in one bullet; +``` + +Replace with: + +``` +- written to the dispatch record and relayed from it. The record holds + the contribution attributed, in its own block, and substantially + verbatim — compression is allowed, merging is not; every + recommendation and every named risk survives, and text from two + personas never lands in one bullet. The relay carries one paragraph + per focusing question — or one per briefing, where none was + appended — and the path to the record; the workflow rule defines + that record and names it; +``` + +- [ ] **Step 3: Correct the same claim on the architect's card** + +In `plugins/working-process/agents/architect-consult.md`, find: + +``` +- Write the reply for the developer, not as a report to the dispatcher — + the contract has it relayed attributed and substantially verbatim. +``` + +Replace with: + +``` +- Write the reply for the developer, not as a report to the dispatcher — + the contract has it kept attributed and substantially verbatim in the + dispatch record, which the developer can open. +``` + +- [ ] **Step 4: Correct it on the system designer's card** + +In `plugins/working-process/agents/system-designer-consult.md`, find: + +``` +- Write the reply for the developer, not as a report to the dispatcher — + the contract has it relayed attributed and substantially verbatim. +``` + +Replace with: + +``` +- Write the reply for the developer, not as a report to the dispatcher — + the contract has it kept attributed and substantially verbatim in the + dispatch record, which the developer can open. +``` + +The two blocks are identical on purpose: the sentence is duplicated in +the shipped cards, and correcting one alone would leave the personas +disagreeing about where their own words land. + +- [ ] **Step 5: Verify** + +Run the Step 1 command again. + +Expected: `A 0`, `B 2`, `C 1`, `D 1`, `E 0`, `F 0`, `G 1`, `H 1`. + +`B 2` counts the bullet's two mentions of the record. `C 1` and `D 1` +unchanged prove the floor's content and the edited-together declaration +both survived. + +- [ ] **Step 6: Commit** + +```bash +git add plugins/working-process/PERSONA_COMMON.md \ + plugins/working-process/agents/architect-consult.md \ + plugins/working-process/agents/system-designer-consult.md +git commit -m "feat(working-process): meet the consultation floor in the dispatch record" +``` + +--- + +### Task 5: `plan-adversary.md` — the `origin` field + +**Files:** +- Modify: `plugins/working-process/agents/plan-adversary.md` — the + `## Output` schema and the close of `## Specs: decline`. + +**Interfaces:** +- Consumes: nothing from earlier tasks. +- Produces: the field name `origin` and its three values, which Task 3's + triage bullet and the glossary's **Origin** entry both read. + +- [ ] **Step 1: Record the before values** + +```bash +A=plugins/working-process/agents/plan-adversary.md +n() { tr -s '[:space:]' ' ' < "$1"; } +echo "A $(grep -c '"origin"' $A)" +echo "B $(grep -c '"plan" | "spec" | "both"' $A)" +echo "C $(grep -c 'Handed a spec' $A)" +echo "D $(n $A | grep -o 'Naming a document is not reviewing it' | wc -l)" +echo "E $(grep -ci 'owner' $A)" +echo "F $(grep -c '"section": "",' $A)" +``` + +Expected: `A 0`, `B 0`, `C 1`, `D 0`, `E 0`, `F 1`. + +`C` is the spec-boundary invariant the spec names: this wave must not +disturb the decline. `E` is the `owner` collision check, an invariant at +zero in both directions. + +- [ ] **Step 2: Add the field to the schema** + +Find: + +``` + "section": "", +``` + +Replace with: + +``` + "section": "", + "origin": "plan" | "spec" | "both", +``` + +The three values sit on one line, matching how the schema already writes +`verdict` and `severity`. + +- [ ] **Step 3: Place the field inside the spec boundary** + +Find: + +``` +Handed a spec (a design document, not an implementation plan)? Decline +the review and point the dispatcher at the `architect` agent. Plan +mechanics — named tests, per-phase commits, concrete paths — do not apply +to a design document and would misfire as findings. +``` + +Replace with: + +``` +Handed a spec (a design document, not an implementation plan)? Decline +the review and point the dispatcher at the `architect` agent. Plan +mechanics — named tests, per-phase commits, concrete paths — do not apply +to a design document and would misfire as findings. + +Naming a document is not reviewing it. A finding whose `origin` is +`spec` or `both` says where the defect traces to and proposes no change +to the spec, so this boundary holds: what to do about a spec-origin +finding is the dispatcher's, and its own rules hold one for the +developer unless a written decision licenses the edit. +``` + +The sentence lands here rather than under `## Output` because the +boundary it qualifies is stated here, and a reader who wonders whether +`origin: spec` breaches the decline is reading this section. + +- [ ] **Step 4: Verify** + +Run the Step 1 command again. + +Expected: `A 1`, `B 1`, `C 1`, `D 1`, `E 0`, `F 1`. + +- [ ] **Step 5: Commit** + +```bash +git add plugins/working-process/agents/plan-adversary.md +git commit -m "feat(working-process): emit the finding's origin from the plan adversary" +``` + +--- + +### Task 6: `spec-plan-lifecycle.md` — the cross-document clause + +**Files:** +- Modify: `plugins/working-process/rules/spec-plan-lifecycle.md` — after + the one-authorizer invariant in `## The disposition ledger`. + +**Interfaces:** +- Consumes: nothing from earlier tasks. +- Produces: the clause Task 3's triage bullet cites by name. + +- [ ] **Step 1: Record the before values** + +```bash +L=plugins/working-process/rules/spec-plan-lifecycle.md +n() { tr -s '[:space:]' ' ' < "$1"; } +echo "A $(n $L | grep -o 'Every terminal line carries exactly one authorizer' | wc -l)" +echo "B $(n $L | grep -o 'other than the reviewed one' | wc -l)" +echo "C $(grep -c '^ ### — fix from' $L)" +echo "D $(n $L | grep -o 'one authorizer, two documents, two lines' | wc -l)" +``` + +Expected: `A 1`, `B 0`, `C 0`, `D 0`. + +`A` is the invariant the spec names: the clause must leave "every +terminal line carries exactly one authorizer" standing, because the +clause is what makes it hold across two documents rather than an +exception to it. + +- [ ] **Step 2: Write the clause** + +Find: + +``` +Every terminal line carries exactly one authorizer. +``` + +Replace with: + +``` +Every terminal line carries exactly one authorizer. + +A fix one review licenses can land in a document other than the +reviewed one — a plan review's finding carrying `origin: spec` is the +case the workflow rule names. The disposition line then lands in the +ledger of the document that changed, and the reviewed document's line +points at it: one authorizer, two documents, two lines, and the +invariant above holds on each. In the changed document the line goes +under its latest round heading, or with the body note the resolution +annotation above already owes, where that document is stamped and no +fresh round ran. A document whose loop closed at `LGTM` has neither — +the annotation is defined for `concerns` and `blocking` alone — so the +fix opens a heading of its own: + + ### — fix from + +It carries no ordinal and no verdict, which keeps it out of the round +cap's derivation and out of the Unfinished-work commands that anchor a +round heading: it records a fix, not a round. Two stamps go stale as on +any body edit — the `integrity:` hash stops matching, and the verdict +stops certifying the words that changed. +``` + +- [ ] **Step 3: Verify** + +Run the Step 1 command again. + +Expected: `A 1`, `B 1`, `C 1`, `D 1`. + +- [ ] **Step 4: Commit** + +```bash +git add plugins/working-process/rules/spec-plan-lifecycle.md +git commit -m "feat(working-process): land a cross-document fix in its own ledger" +``` + +--- + +### Task 7: the glossary — Dispatch record, Relay, Contribution + +**Files:** +- Modify: `docs/domain/glossary.md` — the **Relay** entry, a new + **Dispatch record** entry after it, and the **Contribution** entry. + +**Interfaces:** +- Consumes: Task 1's store path and durability bound, which the new + entry restates in the term's own voice. +- Produces: nothing later tasks read. + +- [ ] **Step 1: Record the before values** + +```bash +G=docs/domain/glossary.md +n() { tr -s '[:space:]' ' ' < "$1"; } +echo "A $(grep -c '^\*\*Origin\*\*:' $G)" +echo "B $(grep -c '^\*\*Dispatch record\*\*:' $G)" +echo "C $(n $G | grep -o "consultation's Contribution attributed and substantially verbatim" | wc -l)" +echo "D $(n $G | grep -o 'NOT a Process directory' | wc -l)" +echo "E $(n $G | grep -o 'held attributed and substantially verbatim in its Dispatch record' | wc -l)" +echo "F $(grep -c '^\*\*Review report\*\*:' $G)" +``` + +Expected: `A 1`, `B 0`, `C 1`, `D 1`, `E 0`, `F 1`. + +`A 1` is W3's third site, already delivered: the **Origin** entry was +minted at the grilling, so this task verifies it rather than writing it +(deviation 3). `D 1` counts **Private memory**'s exclusion, which the +new entry copies in the same words; after this task it is `2`. `F` is an +invariant — **Review report** stays the distinct term the new entry +contrasts itself with. + +- [ ] **Step 2: Amend Relay and mint Dispatch record** + +Find: + +``` +**Relay**: +The delivery of a background agent's result to the developer before any +further action on it — a verdict agent's report before its stamp (the +verdict, the model self-report, and every finding in substance), a +consultation's Contribution attributed and substantially verbatim. The +developer's standing veto point. +_Avoid_: report back, forward (as the term) +``` + +Replace with: + +``` +**Relay**: +The delivery of a background agent's result to the developer before any +further action on it — a verdict agent's report before its stamp (a +header carrying the verdict, the model self-report and the counts, then +one line per finding, that list never condensed), a consultation's +Contribution digested one paragraph per focusing question. Either way it +names the Dispatch record holding the full text. The developer's +standing veto point. +_Avoid_: report back, forward (as the term) + +**Dispatch record**: +The file a dispatcher writes before relaying a background agent's +result — one dispatch, one file, holding what the agent returned +verbatim under a four-line header (date, agent, model self-report, +subject). Kept per subject under `.claude/working-process//`, +git-ignored, and NOT a Process directory — a per-checkout store under +the `.claude/` config namespace, as Private memory is a per-user one. It +survives compaction, the session's end and a branch switch, and no more: +that bound is what lets a Relay condense. Never a Review report, the +counted document a code-review run writes under `docs/code-review/`. +_Avoid_: dispatch log, transcript file +``` + +The exclusion is worded as **Private memory** words its own, which is +what keeps the store outside the Process-directory class by definition +rather than by a rule about prefixes. The two differ where the facts +do — Private memory is per-user, a dispatch record store per-checkout. + +- [ ] **Step 3: Amend Contribution** + +Find: + +``` +**Contribution**: +What a consultation returns: reasoning, options, and the questions the +persona would need answered next, relayed to the developer attributed and +substantially verbatim. Never graded and never counted — the graded, +counted unit is a Finding, which belongs to verdict-bearing reviews. +``` + +Replace with: + +``` +**Contribution**: +What a consultation returns: reasoning, options, and the questions the +persona would need answered next, held attributed and substantially +verbatim in its Dispatch record and relayed from there as a digest. +Never graded and never counted — the graded, counted unit is a Finding, +which belongs to verdict-bearing reviews. +``` + +- [ ] **Step 4: Verify** + +Run the Step 1 command again. + +Expected: `A 1`, `B 1`, `C 0`, `D 2`, `E 1`, `F 1`. + +- [ ] **Step 5: Commit** + +```bash +git add docs/domain/glossary.md +git commit -m "docs(glossary): mint the dispatch record and move the relay's floor" +``` + +--- + +### Task 8: end-state sweep + +**Files:** none modified. This task reads. + +**Interfaces:** +- Consumes: every earlier task. +- Produces: the report the developer acts on. + +- [ ] **Step 1: Run every check the spec publishes** + +```bash +W=plugins/working-process/rules/workflow.md +P=plugins/working-process/PERSONA_COMMON.md +A=plugins/working-process/agents/plan-adversary.md +L=plugins/working-process/rules/spec-plan-lifecycle.md +G=docs/domain/glossary.md +n() { tr -s '[:space:]' ' ' < "$1"; } +echo "01 $(n $W | grep -o 'The relay opens with one header line' | wc -l)" +echo "02 $(n $W | grep -o 'finding list is never condensed' | wc -l)" +echo "03 $(n $W | grep -o 'output style' | wc -l)" +echo "04 $(n $W | grep -o 'compression' | wc -l)" +echo "05 $(n $W | grep -o 'every recommendation and every named risk' | wc -l)" +echo "06 $(n $W | grep -o 'never .\{0,12\}in one bullet' | wc -l)" +echo "07 $(n $W | grep -o 'paragraph per focusing question' | wc -l)" +echo "08 $(n $P | grep -o 'dispatch record' | wc -l)" +echo "09 $(n $L | grep -o 'other than the reviewed one' | wc -l)" +echo "10 $(n $L | grep -o 'Every terminal line carries exactly one authorizer' | wc -l)" +echo "11 $(grep -c '"origin"' $A)" +echo "12 $(grep -c '"plan" | "spec" | "both"' $A)" +echo "13 $(grep -ci 'owner' $A) $(grep -ci 'owner' $W)" +echo "14 $(grep -c 'Handed a spec' $A)" +echo "15 $(n $W | grep -o 'editing a spec from inside a plan review is design work' | wc -l)" +echo "16 $(grep -c '-round-\.md' $W) $(grep -c '--\.md' $W)" +echo "17 $(grep -c '\.claude/working-process/' $W) $(grep -c '\.working-process/' $W)" +echo "18 $(n $W | grep -o 'git-ignored by a .\.gitignore. containing exactly' | wc -l)" +echo "19 $(grep -c '^\*\*Dispatch record\*\*:' $G)" +echo "20 $(grep -c '^\*\*Origin\*\*:' $G)" +``` + +Expected, in order: `1`, `1`, `1`, `1`, `1`, `1`, `1`, `2`, `1`, `1`, +`1`, `1`, `0 0`, `1`, `1`, `1 1`, `3 3`, `1`, `1`, `1`. + +Line `13` is the collision check in both files and `17` is the store's +path count measured two ways — a pair that differs means a path lost +its `.claude/` prefix. Lines `10`, `13`, `14` and `20` are invariants: +they read the same before and after the wave, and each is here because +something in it could have disturbed them. + +- [ ] **Step 2: Validate the plugin and the marketplace** + +```bash +claude plugin validate plugins/working-process && claude plugin validate . +``` + +Expected: both pass. + +What this does not cover: `validate` reads the manifest and the +component frontmatter, never a rule's body — `rules/*.md` is not a +component, so nothing here proves a rule file parses or says what it +means. The agent cards Tasks 4 and 5 touched do have frontmatter, and +none of those edits goes near it. + +- [ ] **Step 3: Confirm no line this wave wrote breaks the wrap** + +The check reads the diff, not the files: a whole-file count of long +lines moves whenever anything else moves and cannot tell this wave's +lines from the ones already there. Two forms are excluded because they +cannot wrap — lines indented four spaces or more are grammar examples, +a form the rules use throughout, and the JSON schema line Task 5 adds is +one of them; table rows begin with a pipe. + +```bash +BASE=$(git merge-base HEAD develop) +git diff "$BASE"..HEAD -- plugins/working-process docs/domain/glossary.md \ + | grep '^+' | grep -v '^+++' | sed 's/^+//' \ + | LC_ALL=C.UTF-8 awk 'length > 72 && $0 !~ /^ / && $0 !~ /^\|/ {print "("length") "$0}' +``` + +Report how many added lines the check examined before reporting the +result, so an empty output is read as "none of N broke the wrap" rather +than as a command that matched nothing: + +```bash +git diff "$BASE"..HEAD -- plugins/working-process docs/domain/glossary.md \ + | grep -c '^+[^+]' +``` + +Expected: no output from the first command. Any line it prints is one +this wave added; rewrap it in the file that owns it and commit as +`style(working-process): rewrap `. Do not reflow a paragraph this +wave did not change — it will not appear here anyway. + +- [ ] **Step 4: Confirm the two declared-together copies agree** + +```bash +n() { tr -s '[:space:]' ' ' < "$1"; } +for f in plugins/working-process/rules/workflow.md \ + plugins/working-process/PERSONA_COMMON.md; do + echo "$f $(n $f | grep -o 'every recommendation and every named risk' | wc -l)" \ + "$(n $f | grep -o 'compression' | wc -l)" +done +``` + +Expected: each file reports `1 1`. + +This is the wave's own defect turned into a check. `PERSONA_COMMON.md` +declares that the dispatcher-facing copy lives in the workflow rule and +the two are edited together; they had drifted until this wave, with the +persona copy holding the whole floor and the rule holding none of it. +Equal counts are what the declaration claims. + +- [ ] **Step 5: Report the end state** + +State to the developer: four changes landed across six files, nothing +created, every published check at its expected value, and +`claude plugin validate` passing for the plugin and the marketplace. + +Then state the delivery gap in its own sentence: this branch's rules are +not the rules any session is running, and will not be until the plugin +cache carries this branch — either through a release, or through a +prerelease install whose version convention the repo's plugin-versioning +rule defines and whose install procedure is the developer's own. Neither +the drift hook nor `sync-rules` will announce that, because both read +the cache rather than this checkout. + +Name the three things this plan deliberately left undone — the install, +the version bump, and moving the spec's `status` to `implemented` — and +that all three are theirs. + +Do not commit in this task; it modifies no file. + +## Self-review + +**Spec coverage.** W1 → Task 1. W2 → Task 2 (`workflow.md`) and Task 4 +(`PERSONA_COMMON.md`, plus the two consult cards deviation 2 adds). W3 → +Task 5 (`plan-adversary.md`), Task 3 (`workflow.md`), Task 6 +(`spec-plan-lifecycle.md`) and Task 7's invariant on the **Origin** +entry the grilling already minted. W4 → Task 1 (`workflow.md`, verdict +side), Task 2 (consultation side) and Task 7 (the glossary entry). The +spec's five Out-of-scope entries produce no task by construction. Every +check in the spec's *Verification* section appears in Task 8 Step 1, +with the path count corrected to three as the integrity audit ruled. + +**Placeholder scan.** Every step carries the literal text to write and +the command to run. The only bracketed tokens are ``, ``, +``, ``, `` and ``, which are the shipped +rules' own placeholders and are meant to reach the file verbatim. + +**Type consistency.** The field is `origin` in all four places that name +it — the schema (Task 5), the triage bullet (Task 3), the glossary entry +(already in the file) and the relay's finding line (Task 1). Its values +are `plan`, `spec`, `both` everywhere. The store path is +`.claude/working-process//` in Tasks 1 and 7 and is named rather +than spelled in Task 2, which is what holds the count at three. The +artefact is a **dispatch record** in every task; `report file` appears +nowhere, being banned by **Review report**'s `_Avoid_` list. From 6177f73ae13cf20c864f22980376f56da33f8c45 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 13:18:10 +0200 Subject: [PATCH 092/120] style: fix nested emphasis in the wave-five plan's deviation list --- docs/plans/2026-09-15-review-loop-errata-wave-five.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md index 89d1fe3..3706967 100644 --- a/docs/plans/2026-09-15-review-loop-errata-wave-five.md +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -72,8 +72,8 @@ verification. Recorded here and beside the text they concern, so a reviewer trips over the reason where the disagreement lives. -1. **Task 7 amends the glossary's **Relay** and **Contribution** - entries, which the spec's W4 does not name.** Both state the floor +1. **Task 7 amends the glossary's Relay and Contribution entries, which + the spec's W4 does not name.** Both state the floor this wave moves: **Relay** says a consultation's Contribution reaches the developer "attributed and substantially verbatim", and **Contribution** repeats it. The glossary binds specs, plans and From 9903751526316f3d1d36a9f92de339b5f948fa42 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 13:26:24 +0200 Subject: [PATCH 093/120] fix: correct the dispatch-record count in the wave-five plan's checks --- docs/plans/2026-09-15-review-loop-errata-wave-five.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md index 3706967..e6a2fdc 100644 --- a/docs/plans/2026-09-15-review-loop-errata-wave-five.md +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -588,11 +588,12 @@ disagreeing about where their own words land. Run the Step 1 command again. -Expected: `A 0`, `B 2`, `C 1`, `D 1`, `E 0`, `F 0`, `G 1`, `H 1`. +Expected: `A 0`, `B 1`, `C 1`, `D 1`, `E 0`, `F 0`, `G 1`, `H 1`. -`B 2` counts the bullet's two mentions of the record. `C 1` and `D 1` -unchanged prove the floor's content and the edited-together declaration -both survived. +`B 1` counts the one place the bullet names the term; its later mentions +read "the record", and the check anchors the whole phrase. `C 1` and +`D 1` unchanged prove the floor's content and the edited-together +declaration both survived. - [ ] **Step 6: Commit** @@ -930,7 +931,7 @@ echo "19 $(grep -c '^\*\*Dispatch record\*\*:' $G)" echo "20 $(grep -c '^\*\*Origin\*\*:' $G)" ``` -Expected, in order: `1`, `1`, `1`, `1`, `1`, `1`, `1`, `2`, `1`, `1`, +Expected, in order: `1`, `1`, `1`, `1`, `1`, `1`, `1`, `1`, `1`, `1`, `1`, `1`, `0 0`, `1`, `1`, `1 1`, `3 3`, `1`, `1`, `1`. Line `13` is the collision check in both files and `17` is the store's From 38386cde788b23a4c637dc5a1f3a3ef88ef14f03 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 13:32:28 +0200 Subject: [PATCH 094/120] fix: correct the bare store-path check in the wave-five plan --- ...2026-09-15-review-loop-errata-wave-five.md | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md index e6a2fdc..435059c 100644 --- a/docs/plans/2026-09-15-review-loop-errata-wave-five.md +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -169,8 +169,8 @@ n() { tr -s '[:space:]' ' ' < "$1"; } echo "A $(n $W | grep -o 'The relay opens with one header line' | wc -l)" echo "B $(n $W | grep -o 'finding list is never condensed' | wc -l)" echo "C $(n $W | grep -o 'output style' | wc -l)" -echo "D $(grep -c '\.claude/working-process/' $W)" -echo "E $(grep -c '\.working-process/' $W)" +echo "D $(grep -o '\.claude/working-process/' $W | wc -l)" +echo "E $(grep -o '\.working-process/' $W | wc -l)" echo "F $(grep -c '-round-\.md' $W)" echo "G $(grep -c '--\.md' $W)" echo "H $(n $W | grep -o 'git-ignored by a .\.gitignore. containing exactly' | wc -l)" @@ -179,9 +179,13 @@ echo "I $(n $W | grep -o 'write the dispatch record defined below' | wc -l)" Expected: `A 0`, `B 0`, `C 0`, `D 0`, `E 0`, `F 0`, `G 0`, `H 0`, `I 0`. -`D` and `E` are the same count measured two ways: `E` catches a store -path written without the `.claude/` prefix, which the spec forbids, so -the two must stay equal at every later reading. +`D` counts the store path; `E` counts a store path written without the +`.claude/` prefix, which the spec forbids. The two are not the same +count: `\.working-process/` needs a dot immediately before +`working-process`, and inside `.claude/working-process/` a slash sits +there, so `E` reads 0 over text `D` reads 3 over. `E` is therefore an +invariant at zero — it is the spec's own check that no bare store path +appears, and it must read 0 before and after. - [ ] **Step 2: Put the record into the completion sequence** @@ -298,12 +302,13 @@ the rules already carry. Run the Step 1 command again. -Expected: `A 1`, `B 1`, `C 1`, `D 3`, `E 3`, `F 1`, `G 1`, `H 1`, +Expected: `A 1`, `B 1`, `C 1`, `D 3`, `E 0`, `F 1`, `G 1`, `H 1`, `I 1`. `D 3` is the Global Constraints cap: once in the store sentence, once in -each filename shape. A count of one would mean a shape went missing, -and `D` differing from `E` would mean a path lost its `.claude/` prefix. +each filename shape, and a count of one would mean a shape went missing. +`E 0` is the invariant: any store path this wave wrote without the +`.claude/` prefix would raise it off zero. - [ ] **Step 5: Commit** @@ -338,7 +343,7 @@ echo "B $(n $W | grep -o 'every recommendation and every named risk' | wc -l)" echo "C $(n $W | grep -o 'never .\{0,12\}in one bullet' | wc -l)" echo "D $(n $W | grep -o 'paragraph per focusing question' | wc -l)" echo "E $(n $W | grep -o 'focusing question' | wc -l)" -echo "F $(grep -c '\.claude/working-process/' $W)" +echo "F $(grep -o '\.claude/working-process/' $W | wc -l)" echo "G $(n $W | grep -o 'Relay each contribution attributed' | wc -l)" ``` @@ -925,18 +930,21 @@ echo "13 $(grep -ci 'owner' $A) $(grep -ci 'owner' $W)" echo "14 $(grep -c 'Handed a spec' $A)" echo "15 $(n $W | grep -o 'editing a spec from inside a plan review is design work' | wc -l)" echo "16 $(grep -c '-round-\.md' $W) $(grep -c '--\.md' $W)" -echo "17 $(grep -c '\.claude/working-process/' $W) $(grep -c '\.working-process/' $W)" +echo "17 $(grep -o '\.claude/working-process/' $W | wc -l) $(grep -o '\.working-process/' $W | wc -l)" echo "18 $(n $W | grep -o 'git-ignored by a .\.gitignore. containing exactly' | wc -l)" echo "19 $(grep -c '^\*\*Dispatch record\*\*:' $G)" echo "20 $(grep -c '^\*\*Origin\*\*:' $G)" ``` Expected, in order: `1`, `1`, `1`, `1`, `1`, `1`, `1`, `1`, `1`, `1`, -`1`, `1`, `0 0`, `1`, `1`, `1 1`, `3 3`, `1`, `1`, `1`. - -Line `13` is the collision check in both files and `17` is the store's -path count measured two ways — a pair that differs means a path lost -its `.claude/` prefix. Lines `10`, `13`, `14` and `20` are invariants: +`1`, `1`, `0 0`, `1`, `1`, `1 1`, `3 0`, `1`, `1`, `1`. + +Line `13` is the collision check in both files. Line `17` reads two +different things: the store path, expected three, and a store path +written without the `.claude/` prefix, expected zero — the spec's own +check, and zero because the pattern needs a dot immediately before +`working-process`, where `.claude/working-process/` puts a slash. Lines +`10`, `13`, `14`, `17`'s second half and `20` are invariants: they read the same before and after the wave, and each is here because something in it could have disturbed them. From 0a60c69e5a2063921d8549a610455ac402355f9b Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 13:47:54 +0200 Subject: [PATCH 095/120] docs: stamp the adversary round on the wave-five plan and dispose its findings --- ...2026-09-15-review-loop-errata-wave-five.md | 67 ++++++++++++++----- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md index 435059c..2f8b28b 100644 --- a/docs/plans/2026-09-15-review-loop-errata-wave-five.md +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -2,6 +2,7 @@ ticket: none date: 2026-09-15 status: draft +adversary: blocking spec: ../specs/2026-09-14-review-loop-errata-wave-five.md branch: feature/process-wave-five base: develop @@ -95,6 +96,15 @@ the reason where the disagreement lives. occurrences of `.claude/working-process/` in `workflow.md`, and Task 1 writes all three; Task 2 names "the timestamp shape of the two the verdict-agent dispatch subsection defines" instead. +5. **Task 6 says the resolution annotation is defined for `concerns` + and `blocking` alone, where the spec says `concerns` alone.** The + shipped rule is what the plan follows: the resolution-annotation + bullet of `spec-plan-lifecycle.md` gives a `blocking` verdict the + developer closes by explicit adjudication the same form, with the + round's ledger record as its body note. So a `blocking` document + has somewhere for the line to land and does not need the fix + heading, which is what the sentence claims. Task 6 carries the + reason. ## File structure @@ -238,9 +248,9 @@ Replace with: saves what the agent returned, verbatim, under a four-line header — the date, the agent, the model self-report, and the subject — with no frontmatter and no `ticket`, since the record is evidence rather - than a process artefact. It goes to `.claude/working-process/`, a - store git-ignored by a `.gitignore` containing exactly `*` written - at its first use. A record survives compaction, the session's end + than a process artefact. It goes to `.claude/working-process/` at + the repo root (`git rev-parse --show-toplevel`), a store git-ignored + by a `.gitignore` containing exactly `*` written at its first use. A record survives compaction, the session's end and a branch switch, and no more; that bound is what the relay's condensation below leans on. Audits write none — a propagation gate's hits land in the ledger and an integrity audit's dispositions @@ -261,9 +271,10 @@ Replace with: having no ledger to draw an ordinal from; an `architect` dispatch on a bare question is one such, and the test is whether a document exists rather than which agent ran. Where a name is already taken, - the new file appends the timestamp, which settles the superseded - round two sessions can produce on one ordinal without asking either - to judge which report is stale. + the new file appends the timestamp to the name it collided with — + `-round---.md` — which settles the + superseded round two sessions can produce on one ordinal without + asking either to judge which report is stale. - The relay opens with one header line, then one line per finding. The header carries the verdict, the model self-report, the finding count by severity, and how many decisions await the developer. Each @@ -377,8 +388,8 @@ named background agent, so the transcript stays inspectable. A contribution is written to its dispatch record before it is relayed, taking the timestamp shape of the two the verdict-agent dispatch -subsection defines: a consultation records nothing, so it has no -ordinal to derive, and several consultations of one persona on one +subsection defines: a consultation mints no round heading, so it has +no ordinal to derive, and several consultations of one persona on one subject in one day are expected. The floor is met in that file — attributed and substantially verbatim, compression allowed and merging forbidden, every recommendation and every named risk surviving, and @@ -430,17 +441,19 @@ git commit -m "feat(working-process): state the consultation floor and digest it ```bash W=plugins/working-process/rules/workflow.md n() { tr -s '[:space:]' ' ' < "$1"; } -echo "A $(n $W | grep -o 'origin' | wc -l)" +echo "A $(n $W | grep -ow 'origin' | wc -l)" echo "B $(n $W | grep -o 'editing a spec from inside a plan review is design work' | wc -l)" echo "C $(n $W | grep -o 'Only written decisions license fixes' | wc -l)" echo "D $(grep -ci 'owner' $W)" ``` -Expected: `A 1`, `B 0`, `C 1`, `D 0`. +Expected: `A 2`, `B 0`, `C 1`, `D 0`. -`A 1` counts the single occurrence Task 1 wrote into the relay bullet — -run this task after Task 1 or the expectation is `A 0`, and either way -the after value is one higher. `C` is an invariant: the bullet this task +`A 2` counts `origin` as a whole word: the two Task 1 wrote into the +relay bullet, with `originates` in that same bullet correctly left out — +which a bare `grep -o 'origin'` would have counted. Run this task after +Task 1, or the expectation is `A 0`; either way this task adds two, the +field name and `spec-origin`. `C` is an invariant: the bullet this task follows must survive it. `D` is the collision check the spec publishes, and it is an invariant in both directions: `owner` is spent on an actor elsewhere in the process and must never name a document here. @@ -475,7 +488,7 @@ Replace with: Run the Step 1 command again. -Expected: `A 2`, `B 1`, `C 1`, `D 0`. +Expected: `A 4`, `B 1`, `C 1`, `D 0`. - [ ] **Step 4: Commit** @@ -749,8 +762,11 @@ A fix one review licenses can land in a document other than the reviewed one — a plan review's finding carrying `origin: spec` is the case the workflow rule names. The disposition line then lands in the ledger of the document that changed, and the reviewed document's line -points at it: one authorizer, two documents, two lines, and the -invariant above holds on each. In the changed document the line goes +points at it in its `` clause, naming that document and +the heading the line sits under: one authorizer, two documents, two +lines, and the invariant above holds on each. The pointer needs no +clause of its own — `` already belongs to the `fixed` +line's shape, so the clause table stays closed. In the changed document the line goes under its latest round heading, or with the body note the resolution annotation above already owes, where that document is stamped and no fresh round ran. A document whose loop closed at `LGTM` has neither — @@ -1056,3 +1072,22 @@ are `plan`, `spec`, `both` everywhere. The store path is than spelled in Task 2, which is what holds the count at three. The artefact is a **dispatch record** in every task; `report file` appears nowhere, being banned by **Review report**'s `_Avoid_` list. + +## Review rounds + +### 2026-09-15 — plan-adversary, fable 5.1, blocking (round 1, full-document) + +The propagation gate ran three times before this dispatch; its lines are written here, under the first round heading, as the grammar directs for an episode that precedes a document's first round. + +- hit fixed 2026-09-15 — Task 4 expected the phrase `dispatch record` twice where its replacement names the term once, later mentions reading "the record"; recounted by applying the replacement to a scratch copy, corrected to one, and the same figure corrected in Task 8's sweep line +- hit fixed 2026-09-15 — the bare store-path check expected three where `\.working-process/` cannot match inside `.claude/working-process/`, a slash sitting where the pattern needs a dot; measured on constructed text, corrected to zero and declared an invariant, and four counts moved from `grep -c` to `grep -o | wc -l` so they count occurrences rather than lines +- hit dismissed 2026-09-15 — Task 7's Find block was reported as failing the byte-exact duty for covering only part of the **Contribution** entry; counter: a Find block is a substring and the duty is byte-exact and unique, measured at exactly one occurrence, and applying the replacement leaves that entry's `_Avoid_` line in place +- fixed 2026-09-15 — [Important] Task 3's `A` counted `origin` as a bare substring, so `originates` in Task 1's relay bullet leaked in and both expectations were wrong in both directions; license: the plan's Global Constraints, which require a step's values measured rather than predicted; the check is now `grep -ow` and the expectations are 2 and 4, verified by simulating both tasks on a scratch copy +- fixed 2026-09-15 — [Important] Task 6 said the resolution annotation is defined for `concerns` and `blocking` alone where the spec says `concerns` alone, departing from the spec with no Deviations entry; license: `spec-plan-lifecycle.md`'s resolution-annotation bullet, which gives an adjudicated `blocking` verdict the same form with the round's ledger record as its body note; deviation 5 now records the departure and its ground +- fixed 2026-09-15 — [Important] the collision rule said only that the new file appends the timestamp, leaving two sessions to spell one name two ways, where the spec's integrity audit routed the exact concatenation to this plan; license: spec decision 5, which decides the append, and that routing; the bullet now carries `-round---.md`, measured not to disturb either filename check +- held — [Important] Task 6 mints a heading shape inside `## Review rounds` while `spec-plan-lifecycle.md` keeps "One annotation extends those shapes, and nothing else does" and, for gate lines, "the round heading's grammar is closed", so the shipped rule would contradict itself after the edit; question: should Task 6 also amend those two sentences to admit the fix heading as the one other extension?; options: (a) amend both sentences inside Task 6 and add a check anchoring the amended text — recommended, since spec decision 3 rules that the fix opens a heading of its own, and the closure sentences are simply where that ruling propagates; (b) drop the fix heading and land the line under the document's latest round heading whatever its verdict, which contradicts decision 3's `ruling:` and would reopen the spec +- held — [Important] the store's git-ignored guarantee rests on a `.gitignore` "written at its first use" and nothing defines first use, so a store directory that already exists without the file never gets one and its records become untracked files a `git add -A` would commit; question: should the rule make the guard idempotent — before every record write, ensure `.claude/working-process/.gitignore` holds exactly `*` — instead of writing it once at first use?; options: (a) the idempotent guard — recommended, since it costs one file read per dispatch and is the only form surviving a directory made by hand, a truncated file, or a `git clean` that took the file and left the directory; (b) keep "at first use" and define it as "the directory is absent, or holds no `.gitignore`", cheaper to read but leaving a wrong-content file unrepaired; either answer sits within this plan's remit, since the integrity audit routed these edge cases here +- fixed 2026-09-15 — [Minor] Task 2 said a consultation records nothing one sentence after saying a contribution is written to its record; license: the glossary's **Consultation** entry, whose discriminator is stamping rather than recording; the clause now says a consultation mints no round heading, which is the property the ordinal argument actually needs +- fixed 2026-09-15 — [Minor] the store path carried no base, where the plugin's own shared file records that a dispatch inherits a working directory possibly below the root and that the miss is silent; license: `PERSONA_COMMON.md`'s glossary-duty paragraph, which resolves its own path against the repo root for that reason; the bullet now says at the repo root and names the command +- fixed 2026-09-15 — [Minor] "the reviewed document's line points at it" named no clause while the ledger's clause table is closed; license: the `fixed` line's own shape, whose `` clause carries the pointer without a new row; the clause now says so and states that the table stays closed +- signal 2026-09-15 — a further round earns its cost only as a diff-scoped read over Tasks 1 and 6, where the fixes reshape rule text and repair-born defects are the likely failure; the counter fix and the deviation entry are mechanical and the propagation gate verifies them without a round, and the three Minors are one sentence each From 481ba9accc3d30398ef3baf1a8b999e0aff08235 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 13:48:08 +0200 Subject: [PATCH 096/120] style: rewrap the wave-five plan's gate-episode note --- docs/plans/2026-09-15-review-loop-errata-wave-five.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md index 2f8b28b..a0473eb 100644 --- a/docs/plans/2026-09-15-review-loop-errata-wave-five.md +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -1077,7 +1077,9 @@ nowhere, being banned by **Review report**'s `_Avoid_` list. ### 2026-09-15 — plan-adversary, fable 5.1, blocking (round 1, full-document) -The propagation gate ran three times before this dispatch; its lines are written here, under the first round heading, as the grammar directs for an episode that precedes a document's first round. +The propagation gate ran three times before this dispatch. Its lines are +written here, under the first round heading, as the grammar directs for +an episode that precedes a document's first round. - hit fixed 2026-09-15 — Task 4 expected the phrase `dispatch record` twice where its replacement names the term once, later mentions reading "the record"; recounted by applying the replacement to a scratch copy, corrected to one, and the same figure corrected in Task 8's sweep line - hit fixed 2026-09-15 — the bare store-path check expected three where `\.working-process/` cannot match inside `.claude/working-process/`, a slash sitting where the pattern needs a dot; measured on constructed text, corrected to zero and declared an invariant, and four counts moved from `grep -c` to `grep -o | wc -l` so they count occurrences rather than lines From 12729bcbff8c0cbf286b69998ea1862df4d6bcd1 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 14:42:53 +0200 Subject: [PATCH 097/120] feat(working-process): rule on the adversary's held findings in the wave-five plan --- ...2026-09-15-review-loop-errata-wave-five.md | 96 +++++++++++++++---- 1 file changed, 79 insertions(+), 17 deletions(-) diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md index a0473eb..8ddc016 100644 --- a/docs/plans/2026-09-15-review-loop-errata-wave-five.md +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -185,9 +185,11 @@ echo "F $(grep -c '-round-\.md' $W)" echo "G $(grep -c '--\.md' $W)" echo "H $(n $W | grep -o 'git-ignored by a .\.gitignore. containing exactly' | wc -l)" echo "I $(n $W | grep -o 'write the dispatch record defined below' | wc -l)" +echo "J $(n $W | grep -o 'Before writing a record the dispatcher ensures' | wc -l)" ``` -Expected: `A 0`, `B 0`, `C 0`, `D 0`, `E 0`, `F 0`, `G 0`, `H 0`, `I 0`. +Expected: `A 0`, `B 0`, `C 0`, `D 0`, `E 0`, `F 0`, `G 0`, `H 0`, `I 0`, +`J 0`. `D` counts the store path; `E` counts a store path written without the `.claude/` prefix, which the spec forbids. The two are not the same @@ -249,13 +251,19 @@ Replace with: the date, the agent, the model self-report, and the subject — with no frontmatter and no `ticket`, since the record is evidence rather than a process artefact. It goes to `.claude/working-process/` at - the repo root (`git rev-parse --show-toplevel`), a store git-ignored - by a `.gitignore` containing exactly `*` written at its first use. A record survives compaction, the session's end - and a branch switch, and no more; that bound is what the relay's - condensation below leans on. Audits write none — a propagation - gate's hits land in the ledger and an integrity audit's dispositions - land as the edits it causes plus the `integrity:` stamp, so a record - would give one fact a second home. + the repo root (`git rev-parse --show-toplevel`). A record survives + compaction, the session's end and a branch switch, and no more; that + bound is what the relay's condensation below leans on. Audits write + none — a propagation gate's hits land in the ledger and an integrity + audit's dispositions land as the edits it causes plus the + `integrity:` stamp, so a record would give one fact a second home. +- The store is git-ignored by a `.gitignore` containing exactly `*`. + Before writing a record the dispatcher ensures that file holds + exactly that, rather than writing it once: the guard repairs a store + directory made by hand, a file someone truncated, and a `git clean` + that took the file and left the directory. A project already + ignoring `.claude/` still gets the file, since the store may not + depend on another file's contents. - The record's name carries its agent and one discriminator: .claude/working-process//-round-.md @@ -314,7 +322,10 @@ the rules already carry. Run the Step 1 command again. Expected: `A 1`, `B 1`, `C 1`, `D 3`, `E 0`, `F 1`, `G 1`, `H 1`, -`I 1`. +`I 1`, `J 1`. + +`J 1` is the idempotent guard: the store's git-ignored state is ensured +before every record write rather than at a first use nothing defines. `D 3` is the Global Constraints cap: once in the store sentence, once in each filename shape, and a count of one would mean a shape went missing. @@ -736,15 +747,25 @@ echo "A $(n $L | grep -o 'Every terminal line carries exactly one authorizer' | echo "B $(n $L | grep -o 'other than the reviewed one' | wc -l)" echo "C $(grep -c '^ ### — fix from' $L)" echo "D $(n $L | grep -o 'one authorizer, two documents, two lines' | wc -l)" +echo "E $(n $L | grep -o 'One annotation extends those shapes' | wc -l)" +echo "F $(n $L | grep -o 'Two additions extend those shapes' | wc -l)" +echo "G $(n $L | grep -o 'is no counter-example' | wc -l)" ``` -Expected: `A 1`, `B 0`, `C 0`, `D 0`. +Expected: `A 1`, `B 0`, `C 0`, `D 0`, `E 1`, `F 0`, `G 0`. `A` is the invariant the spec names: the clause must leave "every terminal line carries exactly one authorizer" standing, because the clause is what makes it hold across two documents rather than an exception to it. +`E`, `F` and `G` belong to the two sentences Steps 3 and 4 amend. The +rule says today that one annotation extends the heading shapes and +nothing else does, and gives a gate's heading-lessness the ground that +the round heading's grammar is closed. Step 2 mints a heading, so +leaving those sentences would ship a rule contradicting itself — +`ruling: 2026-09-15`. + - [ ] **Step 2: Write the clause** Find: @@ -766,8 +787,9 @@ points at it in its `` clause, naming that document and the heading the line sits under: one authorizer, two documents, two lines, and the invariant above holds on each. The pointer needs no clause of its own — `` already belongs to the `fixed` -line's shape, so the clause table stays closed. In the changed document the line goes -under its latest round heading, or with the body note the resolution +line's shape, so the clause table stays closed. In the changed +document the line goes under its latest round heading, or with the +body note the resolution annotation above already owes, where that document is stamped and no fresh round ran. A document whose loop closed at `LGTM` has neither — the annotation is defined for `concerns` and `blocking` alone — so the @@ -782,13 +804,53 @@ any body edit — the `integrity:` hash stops matching, and the verdict stops certifying the words that changed. ``` -- [ ] **Step 3: Verify** +- [ ] **Step 3: Admit the fix heading to the closed shape set** + +Find: + +``` +One annotation extends those shapes, and nothing else does. A +diff-scoped `LGTM` heading gains `, debt discharged ` once the +``` + +Replace with: + +``` +Two additions extend those shapes, and nothing else does: the +annotation here, and the cross-document fix heading above. A +diff-scoped `LGTM` heading gains `, debt discharged ` once the +``` + +- [ ] **Step 4: Say why a gate still mints no heading** + +Find: + +``` +one. A gate still never mints a heading of its own, on the separate +ground that the round heading's grammar is closed and a gate is not a +round. Writing at gate +``` + +Replace with: + +``` +one. A gate still never mints a heading of its own, on the separate +ground that the round heading's grammar is closed and a gate is not a +round. The fix heading above is no counter-example: a gate's lines can +wait for the round that is coming, while a fix landing on a document +whose loop closed at `LGTM` waits for nothing. Writing at gate +``` + +- [ ] **Step 5: Verify** Run the Step 1 command again. -Expected: `A 1`, `B 1`, `C 1`, `D 1`. +Expected: `A 1`, `B 1`, `C 1`, `D 1`, `E 0`, `F 1`, `G 1`. -- [ ] **Step 4: Commit** +`E 0` is the point of Step 3: the sentence claiming one extension is +gone, replaced by the one claiming two. + +- [ ] **Step 6: Commit** ```bash git add plugins/working-process/rules/spec-plan-lifecycle.md @@ -1087,8 +1149,8 @@ an episode that precedes a document's first round. - fixed 2026-09-15 — [Important] Task 3's `A` counted `origin` as a bare substring, so `originates` in Task 1's relay bullet leaked in and both expectations were wrong in both directions; license: the plan's Global Constraints, which require a step's values measured rather than predicted; the check is now `grep -ow` and the expectations are 2 and 4, verified by simulating both tasks on a scratch copy - fixed 2026-09-15 — [Important] Task 6 said the resolution annotation is defined for `concerns` and `blocking` alone where the spec says `concerns` alone, departing from the spec with no Deviations entry; license: `spec-plan-lifecycle.md`'s resolution-annotation bullet, which gives an adjudicated `blocking` verdict the same form with the round's ledger record as its body note; deviation 5 now records the departure and its ground - fixed 2026-09-15 — [Important] the collision rule said only that the new file appends the timestamp, leaving two sessions to spell one name two ways, where the spec's integrity audit routed the exact concatenation to this plan; license: spec decision 5, which decides the append, and that routing; the bullet now carries `-round---.md`, measured not to disturb either filename check -- held — [Important] Task 6 mints a heading shape inside `## Review rounds` while `spec-plan-lifecycle.md` keeps "One annotation extends those shapes, and nothing else does" and, for gate lines, "the round heading's grammar is closed", so the shipped rule would contradict itself after the edit; question: should Task 6 also amend those two sentences to admit the fix heading as the one other extension?; options: (a) amend both sentences inside Task 6 and add a check anchoring the amended text — recommended, since spec decision 3 rules that the fix opens a heading of its own, and the closure sentences are simply where that ruling propagates; (b) drop the fix heading and land the line under the document's latest round heading whatever its verdict, which contradicts decision 3's `ruling:` and would reopen the spec -- held — [Important] the store's git-ignored guarantee rests on a `.gitignore` "written at its first use" and nothing defines first use, so a store directory that already exists without the file never gets one and its records become untracked files a `git add -A` would commit; question: should the rule make the guard idempotent — before every record write, ensure `.claude/working-process/.gitignore` holds exactly `*` — instead of writing it once at first use?; options: (a) the idempotent guard — recommended, since it costs one file read per dispatch and is the only form surviving a directory made by hand, a truncated file, or a `git clean` that took the file and left the directory; (b) keep "at first use" and define it as "the directory is absent, or holds no `.gitignore`", cheaper to read but leaving a wrong-content file unrepaired; either answer sits within this plan's remit, since the integrity audit routed these edge cases here +- fixed 2026-09-15 — [Important] Task 6 mints a heading shape inside `## Review rounds` while `spec-plan-lifecycle.md` keeps "One annotation extends those shapes, and nothing else does" and, for gate lines, "the round heading's grammar is closed", so the shipped rule would contradict itself after the edit; ruling: 2026-09-15; Task 6 gains Steps 3 and 4 amending both sentences — the shape set now admits two additions, the annotation and the fix heading, and the gate sentence says why the fix heading is no counter-example, a gate's lines being able to wait for the round that is coming — plus checks E, F and G anchoring the amended text +- fixed 2026-09-15 — [Important] the store's git-ignored guarantee rested on a `.gitignore` "written at its first use" with nothing defining first use, so a store directory already present without the file never got one and its records became untracked files a `git add -A` would commit; ruling: 2026-09-15; the guard is idempotent — before every record write the dispatcher ensures the file holds exactly `*`, which repairs a directory made by hand, a truncated file and a `git clean` that took the file and left the directory — the store bullet is split so the guard stands on its own, and check J anchors it - fixed 2026-09-15 — [Minor] Task 2 said a consultation records nothing one sentence after saying a contribution is written to its record; license: the glossary's **Consultation** entry, whose discriminator is stamping rather than recording; the clause now says a consultation mints no round heading, which is the property the ordinal argument actually needs - fixed 2026-09-15 — [Minor] the store path carried no base, where the plugin's own shared file records that a dispatch inherits a working directory possibly below the root and that the miss is silent; license: `PERSONA_COMMON.md`'s glossary-duty paragraph, which resolves its own path against the repo root for that reason; the bullet now says at the repo root and names the command - fixed 2026-09-15 — [Minor] "the reviewed document's line points at it" named no clause while the ledger's clause table is closed; license: the `fixed` line's own shape, whose `` clause carries the pointer without a new row; the clause now says so and states that the table stays closed From 1eca9202ae57916c8bc7134f8e4e440773c7746f Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 14:49:25 +0200 Subject: [PATCH 098/120] fix: recount the wave-five plan's out-of-scope and file totals --- docs/plans/2026-09-15-review-loop-errata-wave-five.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md index 8ddc016..2de75f6 100644 --- a/docs/plans/2026-09-15-review-loop-errata-wave-five.md +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -1091,7 +1091,7 @@ Equal counts are what the declaration claims. - [ ] **Step 5: Report the end state** -State to the developer: four changes landed across six files, nothing +State to the developer: four changes landed across seven files, nothing created, every published check at its expected value, and `claude plugin validate` passing for the plugin and the marketplace. @@ -1117,7 +1117,7 @@ Task 5 (`plan-adversary.md`), Task 3 (`workflow.md`), Task 6 (`spec-plan-lifecycle.md`) and Task 7's invariant on the **Origin** entry the grilling already minted. W4 → Task 1 (`workflow.md`, verdict side), Task 2 (consultation side) and Task 7 (the glossary entry). The -spec's five Out-of-scope entries produce no task by construction. Every +spec's four Out-of-scope entries produce no task by construction. Every check in the spec's *Verification* section appears in Task 8 Step 1, with the path count corrected to three as the integrity audit ruled. @@ -1146,6 +1146,8 @@ an episode that precedes a document's first round. - hit fixed 2026-09-15 — Task 4 expected the phrase `dispatch record` twice where its replacement names the term once, later mentions reading "the record"; recounted by applying the replacement to a scratch copy, corrected to one, and the same figure corrected in Task 8's sweep line - hit fixed 2026-09-15 — the bare store-path check expected three where `\.working-process/` cannot match inside `.claude/working-process/`, a slash sitting where the pattern needs a dot; measured on constructed text, corrected to zero and declared an invariant, and four counts moved from `grep -c` to `grep -o | wc -l` so they count occurrences rather than lines - hit dismissed 2026-09-15 — Task 7's Find block was reported as failing the byte-exact duty for covering only part of the **Contribution** entry; counter: a Find block is a substring and the duty is byte-exact and unique, measured at exactly one occurrence, and applying the replacement leaves that entry's `_Avoid_` line in place +- hit fixed 2026-09-15 — the Self-review claimed the spec has five Out-of-scope entries where its section lists four; recounted against the section's bullets and corrected to four +- hit fixed 2026-09-15 — Task 8's end-state report claimed four changes across six files where the plan's own `Modify:` lines name seven distinct paths, the Task 4 bullet carrying two of them; enumerated those lines and corrected to seven - fixed 2026-09-15 — [Important] Task 3's `A` counted `origin` as a bare substring, so `originates` in Task 1's relay bullet leaked in and both expectations were wrong in both directions; license: the plan's Global Constraints, which require a step's values measured rather than predicted; the check is now `grep -ow` and the expectations are 2 and 4, verified by simulating both tasks on a scratch copy - fixed 2026-09-15 — [Important] Task 6 said the resolution annotation is defined for `concerns` and `blocking` alone where the spec says `concerns` alone, departing from the spec with no Deviations entry; license: `spec-plan-lifecycle.md`'s resolution-annotation bullet, which gives an adjudicated `blocking` verdict the same form with the round's ledger record as its body note; deviation 5 now records the departure and its ground - fixed 2026-09-15 — [Important] the collision rule said only that the new file appends the timestamp, leaving two sessions to spell one name two ways, where the spec's integrity audit routed the exact concatenation to this plan; license: spec decision 5, which decides the append, and that routing; the bullet now carries `-round---.md`, measured not to disturb either filename check From 4cc7f4ad17f610eb0075c605a117c1082d01463e Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 15:33:06 +0200 Subject: [PATCH 099/120] docs: stamp the adversary's second round on the wave-five plan --- docs/plans/2026-09-15-review-loop-errata-wave-five.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md index 2de75f6..79bf5bb 100644 --- a/docs/plans/2026-09-15-review-loop-errata-wave-five.md +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -2,7 +2,7 @@ ticket: none date: 2026-09-15 status: draft -adversary: blocking +adversary: concerns spec: ../specs/2026-09-14-review-loop-errata-wave-five.md branch: feature/process-wave-five base: develop @@ -1157,3 +1157,9 @@ an episode that precedes a document's first round. - fixed 2026-09-15 — [Minor] the store path carried no base, where the plugin's own shared file records that a dispatch inherits a working directory possibly below the root and that the miss is silent; license: `PERSONA_COMMON.md`'s glossary-duty paragraph, which resolves its own path against the repo root for that reason; the bullet now says at the repo root and names the command - fixed 2026-09-15 — [Minor] "the reviewed document's line points at it" named no clause while the ledger's clause table is closed; license: the `fixed` line's own shape, whose `` clause carries the pointer without a new row; the clause now says so and states that the table stays closed - signal 2026-09-15 — a further round earns its cost only as a diff-scoped read over Tasks 1 and 6, where the fixes reshape rule text and repair-born defects are the likely failure; the counter fix and the deviation entry are mechanical and the propagation gate verifies them without a round, and the three Minors are one sentence each + +### 2026-09-15 — plan-adversary, fable 5.1, concerns (round 2, diff-scoped) + +- held — [Important] the collision rule appends the timestamp to the name it collided with, which settles the ordinal shape but not the timestamp shape: two dispatches of one agent on one subject landing in the same second collide on `--.md`, and appending the same timestamp reproduces a name already taken, so the second record overwrites the first; question: how does a timestamp-shape collision resolve?; options: (a) scope the timestamp append to the ordinal shape, which is the case spec decision 5 gives as its reason, and give the timestamp shape the lowest free counter suffix, `-2` then `-3` — recommended, deterministic and clock-free, with the spec's decision 5 sentence corrected by the cross-document clause this wave defines; (b) sub-second precision in every timestamp name, which changes the filename shape the developer settled and Task 1's `G` check; (c) state the collision as accepted, which loses a record rather than merely misordering one; counter: the finding traces to both documents — spec decision 5 states the append for every taken name under `ruling: 2026-09-14`, so its spec half is not the session's to edit +- held — [Minor] spec decision 3 says the resolution annotation "is defined for `concerns` alone" where the shipped rule defines it for an adjudicated `blocking` as well; the plan already follows the rule through deviation 5, so only the spec carries the error, and it would freeze into an `implemented` document amendable only in frontmatter; question: correct the spec's clause now, while the spec is still `approved`?; options: (a) correct it through the cross-document clause this wave defines, landing the line with the spec's existing resolution note and pointing at it from here, accepting that the spec's `integrity:` stamp goes stale as the rule says any body edit makes it — recommended, and one spec edit can carry the spec half of the finding above with it, so the stamp goes stale once rather than twice; (b) leave the spec as it stands, relying on deviation 5 to record the disagreement +- signal 2026-09-15 — a further round does not earn its cost: the Important is a one-or-two-sentence fix the propagation gate verifies mechanically, and the Minor is held for the developer whatever a round finds, being spec-origin; a diff-scoped round three would mostly re-confirm a small patch From 01b7e46f15a28086f31206cb383da2efaf002855 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 15:38:11 +0200 Subject: [PATCH 100/120] docs: rule on the adversary's second round across the wave-five spec and plan --- ...2026-09-15-review-loop-errata-wave-five.md | 29 ++++++++--------- ...2026-09-14-review-loop-errata-wave-five.md | 31 ++++++++++++++----- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md index 79bf5bb..4d81e82 100644 --- a/docs/plans/2026-09-15-review-loop-errata-wave-five.md +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -96,15 +96,13 @@ the reason where the disagreement lives. occurrences of `.claude/working-process/` in `workflow.md`, and Task 1 writes all three; Task 2 names "the timestamp shape of the two the verdict-agent dispatch subsection defines" instead. -5. **Task 6 says the resolution annotation is defined for `concerns` - and `blocking` alone, where the spec says `concerns` alone.** The - shipped rule is what the plan follows: the resolution-annotation - bullet of `spec-plan-lifecycle.md` gives a `blocking` verdict the - developer closes by explicit adjudication the same form, with the - round's ledger record as its body note. So a `blocking` document - has somewhere for the line to land and does not need the fix - heading, which is what the sentence claims. Task 6 carries the - reason. +5. **Resolved at the source.** Task 6 says the resolution annotation is + defined for `concerns` and `blocking` alone, following the + resolution-annotation bullet of `spec-plan-lifecycle.md`, which gives + a `blocking` verdict closed by adjudication the same form. The spec + said `concerns` alone when this entry was written; adversary round 2 + corrected the spec to match, so no departure remains. The entry keeps + its number because the round-1 ledger cites it. ## File structure @@ -278,11 +276,14 @@ Replace with: kebab-cased and capped at sixty characters, and the timestamp shape, having no ledger to draw an ordinal from; an `architect` dispatch on a bare question is one such, and the test is whether a document - exists rather than which agent ran. Where a name is already taken, - the new file appends the timestamp to the name it collided with — + exists rather than which agent ran. Where a round-ordinal name is + already taken, the new file appends the timestamp to it — `-round---.md` — which settles the superseded round two sessions can produce on one ordinal without - asking either to judge which report is stale. + asking either to judge which report is stale. A timestamp name + collides only when a second record lands in the same second, where + appending the same timestamp would reproduce the taken name, so it + takes the lowest free counter suffix instead: `-2`, then `-3`. - The relay opens with one header line, then one line per finding. The header carries the verdict, the model self-report, the finding count by severity, and how many decisions await the developer. Each @@ -1160,6 +1161,6 @@ an episode that precedes a document's first round. ### 2026-09-15 — plan-adversary, fable 5.1, concerns (round 2, diff-scoped) -- held — [Important] the collision rule appends the timestamp to the name it collided with, which settles the ordinal shape but not the timestamp shape: two dispatches of one agent on one subject landing in the same second collide on `--.md`, and appending the same timestamp reproduces a name already taken, so the second record overwrites the first; question: how does a timestamp-shape collision resolve?; options: (a) scope the timestamp append to the ordinal shape, which is the case spec decision 5 gives as its reason, and give the timestamp shape the lowest free counter suffix, `-2` then `-3` — recommended, deterministic and clock-free, with the spec's decision 5 sentence corrected by the cross-document clause this wave defines; (b) sub-second precision in every timestamp name, which changes the filename shape the developer settled and Task 1's `G` check; (c) state the collision as accepted, which loses a record rather than merely misordering one; counter: the finding traces to both documents — spec decision 5 states the append for every taken name under `ruling: 2026-09-14`, so its spec half is not the session's to edit -- held — [Minor] spec decision 3 says the resolution annotation "is defined for `concerns` alone" where the shipped rule defines it for an adjudicated `blocking` as well; the plan already follows the rule through deviation 5, so only the spec carries the error, and it would freeze into an `implemented` document amendable only in frontmatter; question: correct the spec's clause now, while the spec is still `approved`?; options: (a) correct it through the cross-document clause this wave defines, landing the line with the spec's existing resolution note and pointing at it from here, accepting that the spec's `integrity:` stamp goes stale as the rule says any body edit makes it — recommended, and one spec edit can carry the spec half of the finding above with it, so the stamp goes stale once rather than twice; (b) leave the spec as it stands, relying on deviation 5 to record the disagreement +- fixed 2026-09-15 — [Important] the collision rule appends the timestamp to the name it collided with, which settles the ordinal shape but not the timestamp shape: two dispatches of one agent on one subject landing in the same second collide on `--.md`, and appending the same timestamp reproduces a name already taken, so the second record overwrites the first; ruling: 2026-09-15; Task 1's bullet scopes the timestamp append to the round-ordinal name and gives a timestamp name the lowest free counter suffix, `-2` then `-3`; the spec half landed in `docs/specs/2026-09-14-review-loop-errata-wave-five.md` beside its round-2 resolution note +- fixed 2026-09-15 — [Minor] spec decision 3 says the resolution annotation "is defined for `concerns` alone" where the shipped rule defines it for an adjudicated `blocking` as well; the plan already follows the rule through deviation 5, so only the spec carries the error, and it would freeze into an `implemented` document amendable only in frontmatter; ruling: 2026-09-15; the line landed in `docs/specs/2026-09-14-review-loop-errata-wave-five.md` beside its round-2 resolution note, and deviation 5 now records the departure as resolved at the source - signal 2026-09-15 — a further round does not earn its cost: the Important is a one-or-two-sentence fix the propagation gate verifies mechanically, and the Minor is held for the developer whatever a round finds, being spec-origin; a diff-scoped round three would mostly re-confirm a small patch diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index d21a5a0..1df7088 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -191,9 +191,10 @@ for a hole. round heading, or as the resolution annotation the lifecycle rule already defines for a stamped document edited without a fresh round, and the plan's line points at it. Where the spec's loop closed at - `LGTM` it has neither — the annotation is defined for `concerns` - alone — so the fix opens a heading of its own: dated, naming the fix - rather than a round, and carrying the one line. Two consequences + `LGTM` it has neither — the annotation is defined for `concerns` and + for a `blocking` verdict closed by adjudication, never for `LGTM` — + so the fix opens a heading of its own: dated, naming the fix rather + than a round, and carrying the one line. Two consequences this decision states rather than invents: a spec's standing `integrity:` stamp goes stale on any body edit, this one included, by the rule that already says so; and the architect stamp stops @@ -335,11 +336,16 @@ for a hole. session can collide there, which the rule already accepts as undetectable and which the next clause resolves mechanically. - Where a name is already taken, the new file appends the timestamp. - That covers the superseded round the rules already describe, whose - stale result is relayed and never stamped: the two rounds share an - ordinal, and the rule resolves it mechanically instead of asking a - session to judge which report is the stale one. `ruling: 2026-09-14` + Where a round-ordinal name is already taken, the new file appends the + timestamp. That covers the superseded round the rules already + describe, whose stale result is relayed and never stamped: the two + rounds share an ordinal, and the rule resolves it mechanically + instead of asking a session to judge which report is the stale one. + A timestamp name cannot take that remedy, since it collides only + when a second record lands in the same second and appending the + same timestamp reproduces the taken name. It takes the lowest free + counter suffix instead — `-2`, then `-3` — which needs no clock and + gives every session the same answer. `ruling: 2026-09-14` ## Scope @@ -512,6 +518,15 @@ kept `compression is allowed, merging is not` and the rule copy did not. W2 repairs that drift, which makes it a defect fix rather than the addition this spec first took it for. +Two fixes reached this spec afterwards from the plan's adversary review, +round 2. Both trace to this document, so their lines land here, beside +the note that resolved the verdict, and the plan's lines point at them. +This spec's `integrity:` stamp stopped matching with these edits, as +the lifecycle rule says any body edit makes it. + +- fixed 2026-09-15 — [Important] decision 5 appended the timestamp to every taken name, which reproduces a taken timestamp name rather than freeing one, so a second record in the same second overwrote the first; ruling: 2026-09-15; the timestamp append now covers the round-ordinal name alone, and a timestamp name takes the lowest free counter suffix — pointed at from `docs/plans/2026-09-15-review-loop-errata-wave-five.md`, under its round-2 heading +- fixed 2026-09-15 — [Minor] decision 3 said the resolution annotation is defined for `concerns` alone, where the lifecycle rule defines it for a `blocking` verdict closed by adjudication too; ruling: 2026-09-15; the clause now names both and says the annotation never covers `LGTM`, which is the case the decision needs — pointed at from `docs/plans/2026-09-15-review-loop-errata-wave-five.md`, under its round-2 heading + ### 2026-09-15 — integrity audit, fable, at the consumption gate Coverage tell: 425 lines read, highest line cited 424 — a whole-document From 034f0c3592dbd2d16e08832b998ad2634edfabe3 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 15:42:29 +0200 Subject: [PATCH 101/120] fix: match the wave-five plan's annotation scope to the corrected spec --- .../2026-09-15-review-loop-errata-wave-five.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md index 4d81e82..63365a8 100644 --- a/docs/plans/2026-09-15-review-loop-errata-wave-five.md +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -97,9 +97,9 @@ the reason where the disagreement lives. Task 1 writes all three; Task 2 names "the timestamp shape of the two the verdict-agent dispatch subsection defines" instead. 5. **Resolved at the source.** Task 6 says the resolution annotation is - defined for `concerns` and `blocking` alone, following the - resolution-annotation bullet of `spec-plan-lifecycle.md`, which gives - a `blocking` verdict closed by adjudication the same form. The spec + defined for `concerns` and for a `blocking` verdict closed by + adjudication, following the resolution-annotation bullet of + `spec-plan-lifecycle.md`, which gives that verdict the same form. The spec said `concerns` alone when this entry was written; adversary round 2 corrected the spec to match, so no departure remains. The entry keeps its number because the round-1 ledger cites it. @@ -790,11 +790,11 @@ lines, and the invariant above holds on each. The pointer needs no clause of its own — `` already belongs to the `fixed` line's shape, so the clause table stays closed. In the changed document the line goes under its latest round heading, or with the -body note the resolution -annotation above already owes, where that document is stamped and no -fresh round ran. A document whose loop closed at `LGTM` has neither — -the annotation is defined for `concerns` and `blocking` alone — so the -fix opens a heading of its own: +body note the resolution annotation above already owes, where that +document is stamped and no fresh round ran. A document whose loop +closed at `LGTM` has neither — the annotation is defined for `concerns` +and for a `blocking` verdict closed by adjudication, never for `LGTM` — +so the fix opens a heading of its own: ### — fix from @@ -1161,6 +1161,7 @@ an episode that precedes a document's first round. ### 2026-09-15 — plan-adversary, fable 5.1, concerns (round 2, diff-scoped) +- hit fixed 2026-09-15 — Task 6's Replace block and deviation 5 said the annotation is defined for `concerns` and `blocking` alone, where the corrected spec names a `blocking` verdict closed by adjudication and a bare `blocking` carries no annotation; both now use the spec's wording, and the same edit rejoined a line the Replace block had broken mid-sentence - fixed 2026-09-15 — [Important] the collision rule appends the timestamp to the name it collided with, which settles the ordinal shape but not the timestamp shape: two dispatches of one agent on one subject landing in the same second collide on `--.md`, and appending the same timestamp reproduces a name already taken, so the second record overwrites the first; ruling: 2026-09-15; Task 1's bullet scopes the timestamp append to the round-ordinal name and gives a timestamp name the lowest free counter suffix, `-2` then `-3`; the spec half landed in `docs/specs/2026-09-14-review-loop-errata-wave-five.md` beside its round-2 resolution note - fixed 2026-09-15 — [Minor] spec decision 3 says the resolution annotation "is defined for `concerns` alone" where the shipped rule defines it for an adjudicated `blocking` as well; the plan already follows the rule through deviation 5, so only the spec carries the error, and it would freeze into an `implemented` document amendable only in frontmatter; ruling: 2026-09-15; the line landed in `docs/specs/2026-09-14-review-loop-errata-wave-five.md` beside its round-2 resolution note, and deviation 5 now records the departure as resolved at the source - signal 2026-09-15 — a further round does not earn its cost: the Important is a one-or-two-sentence fix the propagation gate verifies mechanically, and the Minor is held for the developer whatever a round finds, being spec-origin; a diff-scoped round three would mostly re-confirm a small patch From 7d97cd9875f747149a6208f85237b840456f0d90 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 15:54:12 +0200 Subject: [PATCH 102/120] docs: stamp the confirming adversary round on the wave-five plan and fix its finding --- .../2026-09-15-review-loop-errata-wave-five.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md index 63365a8..d0bbe3f 100644 --- a/docs/plans/2026-09-15-review-loop-errata-wave-five.md +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -184,10 +184,11 @@ echo "G $(grep -c '--\.md' $W)" echo "H $(n $W | grep -o 'git-ignored by a .\.gitignore. containing exactly' | wc -l)" echo "I $(n $W | grep -o 'write the dispatch record defined below' | wc -l)" echo "J $(n $W | grep -o 'Before writing a record the dispatcher ensures' | wc -l)" +echo "K $(n $W | grep -o "the relay names that record's path" | wc -l)" ``` Expected: `A 0`, `B 0`, `C 0`, `D 0`, `E 0`, `F 0`, `G 0`, `H 0`, `I 0`, -`J 0`. +`J 0`, `K 0`. `D` counts the store path; `E` counts a store path written without the `.claude/` prefix, which the spec forbids. The two are not the same @@ -297,7 +298,8 @@ Replace with: developer's standing veto, so a finding nobody printed is a finding nobody could overrule. Narrative is safe to condense because the dispatch record holds the full text, which makes an expansion a - quotation rather than a reconstruction. + quotation rather than a reconstruction — so the relay names that + record's path, and the developer can open what was condensed. - The header's count of decisions is the findings for which the session can cite no license, derived at relay time from the report and the decisions already written down — before any ledger line @@ -323,7 +325,11 @@ the rules already carry. Run the Step 1 command again. Expected: `A 1`, `B 1`, `C 1`, `D 3`, `E 0`, `F 1`, `G 1`, `H 1`, -`I 1`, `J 1`. +`I 1`, `J 1`, `K 1`. + +`K 1` is spec decision 1's pointer: the relay names the record's path, +which is what makes condensing narrative safe. Task 2 carries the same +duty for a consultation, and Task 7's **Relay** entry states it for both. `J 1` is the idempotent guard: the store's git-ignored state is ensured before every record write rather than at a first use nothing defines. @@ -1165,3 +1171,8 @@ an episode that precedes a document's first round. - fixed 2026-09-15 — [Important] the collision rule appends the timestamp to the name it collided with, which settles the ordinal shape but not the timestamp shape: two dispatches of one agent on one subject landing in the same second collide on `--.md`, and appending the same timestamp reproduces a name already taken, so the second record overwrites the first; ruling: 2026-09-15; Task 1's bullet scopes the timestamp append to the round-ordinal name and gives a timestamp name the lowest free counter suffix, `-2` then `-3`; the spec half landed in `docs/specs/2026-09-14-review-loop-errata-wave-five.md` beside its round-2 resolution note - fixed 2026-09-15 — [Minor] spec decision 3 says the resolution annotation "is defined for `concerns` alone" where the shipped rule defines it for an adjudicated `blocking` as well; the plan already follows the rule through deviation 5, so only the spec carries the error, and it would freeze into an `implemented` document amendable only in frontmatter; ruling: 2026-09-15; the line landed in `docs/specs/2026-09-14-review-loop-errata-wave-five.md` beside its round-2 resolution note, and deviation 5 now records the departure as resolved at the source - signal 2026-09-15 — a further round does not earn its cost: the Important is a one-or-two-sentence fix the propagation gate verifies mechanically, and the Minor is held for the developer whatever a round finds, being spec-origin; a diff-scoped round three would mostly re-confirm a small patch + +### 2026-09-15 — plan-adversary, fable 5.1, concerns (round 3, full-document) + +- fixed 2026-09-15 — [Important] Task 1 wrote the verdict relay's header, its uncondensable finding list and the condensation rule, but never told the relay to name the dispatch record, while spec decision 1 requires it and Task 7's **Relay** entry states it for both relay kinds; license: spec decision 1, "the relay names the dispatch record decision 5 defines"; the condensation bullet now ends by naming the record's path, and check `K` anchors it in Task 1's Step 1 and Step 4 +- signal 2026-09-15 — a further full-document round would not earn its cost: the one Important is a single-sentence fix with an obvious license, verifiable mechanically by one added check, so a propagation gate over Task 1 is sufficient before the loop closes From 68fb84be6cd4c4fad3a1fe1d659b6414485dca87 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 15:58:21 +0200 Subject: [PATCH 103/120] docs: resolve the adversary loop and approve the wave-five plan --- .../2026-09-15-review-loop-errata-wave-five.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md index d0bbe3f..9a0497e 100644 --- a/docs/plans/2026-09-15-review-loop-errata-wave-five.md +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -1,8 +1,8 @@ --- ticket: none date: 2026-09-15 -status: draft -adversary: concerns +status: approved +adversary: concerns (resolved 2026-09-15) spec: ../specs/2026-09-14-review-loop-errata-wave-five.md branch: feature/process-wave-five base: develop @@ -1176,3 +1176,12 @@ an episode that precedes a document's first round. - fixed 2026-09-15 — [Important] Task 1 wrote the verdict relay's header, its uncondensable finding list and the condensation rule, but never told the relay to name the dispatch record, while spec decision 1 requires it and Task 7's **Relay** entry states it for both relay kinds; license: spec decision 1, "the relay names the dispatch record decision 5 defines"; the condensation bullet now ends by naming the record's path, and check `K` anchors it in Task 1's Step 1 and Step 4 - signal 2026-09-15 — a further full-document round would not earn its cost: the one Important is a single-sentence fix with an obvious license, verifiable mechanically by one added check, so a propagation gate over Task 1 is sufficient before the loop closes + +The loop closed here on 2026-09-15, by the developer's decision and on +round 3's own stop signal, without a fourth round. What resolved the +concerns: round 3 was the confirming full-document round the developer's +earlier close made owed, and its one finding was licensed by spec +decision 1 and fixed in the same turn. A propagation gate over Task 1 +then passed clean, which is the verification that finding's stop signal +named as sufficient. Every finding of all three rounds reached a +terminal disposition, and the held set is empty. From 973902112f549b513a9f9be5222078dee5b94954 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 16:02:47 +0200 Subject: [PATCH 104/120] feat(working-process): give the verdict relay a header and a dispatch record --- plugins/working-process/rules/workflow.md | 89 ++++++++++++++++++++--- 1 file changed, 77 insertions(+), 12 deletions(-) diff --git a/plugins/working-process/rules/workflow.md b/plugins/working-process/rules/workflow.md index b65421b..65d10ed 100644 --- a/plugins/working-process/rules/workflow.md +++ b/plugins/working-process/rules/workflow.md @@ -160,18 +160,83 @@ available: progress visible in the session's task list. - On the completion notification, in one turn and in this order: verify the agent's model self-report (the comparison the lifecycle - rule defines), relay the report to the developer, check every - citation the report supplies against what it names, then stamp the - verdict (`LGTM` | `concerns` | `blocking`) into the reviewed - document's `architect:` / `adversary:` frontmatter field. The - citation check follows the relay rather than preceding it, because a - wrong citation in conversation costs a correction while a wrong one - in the record outlives the loop. The order - has one named exception, defined under *What a diff-scoped LGTM - certifies* below: a plan's diff-scoped LGTM. -- The relay carries the verdict, the model self-report, and every - finding in substance — condense narrative prose, never drop a - finding or its severity. + rule defines), write the dispatch record defined below, relay the + report to the developer, check every citation the report supplies + against what it names, then stamp the verdict (`LGTM` | `concerns` | + `blocking`) into the reviewed document's `architect:` / + `adversary:` frontmatter field. The citation check follows the relay + rather than preceding it, because a wrong citation in conversation + costs a correction while a wrong one in the record outlives the + loop. The order has one named exception, defined under *What a + diff-scoped LGTM certifies* below: a plan's diff-scoped LGTM. +- The dispatch record is written before the relay. The dispatcher + saves what the agent returned, verbatim, under a four-line header — + the date, the agent, the model self-report, and the subject — with + no frontmatter and no `ticket`, since the record is evidence rather + than a process artefact. It goes to `.claude/working-process/` at + the repo root (`git rev-parse --show-toplevel`). A record survives + compaction, the session's end and a branch switch, and no more; that + bound is what the relay's condensation below leans on. Audits write + none — a propagation gate's hits land in the ledger and an integrity + audit's dispositions land as the edits it causes plus the + `integrity:` stamp, so a record would give one fact a second home. +- The store is git-ignored by a `.gitignore` containing exactly `*`. + Before writing a record the dispatcher ensures that file holds + exactly that, rather than writing it once: the guard repairs a store + directory made by hand, a file someone truncated, and a `git clean` + that took the file and left the directory. A project already + ignoring `.claude/` still gets the file, since the store may not + depend on another file's contents. +- The record's name carries its agent and one discriminator: + + .claude/working-process//-round-.md + .claude/working-process//--.md + + `` is the reviewed document's basename without its extension, + one directory per subject. A verdict agent takes the round ordinal, + derived the way the heading will derive it — the highest ordinal the + document's rounds carry, plus one, ordinals continuing across + loops — because the record is written before that heading exists. A + dispatch that reviews no document takes its subject as the stem, + kebab-cased and capped at sixty characters, and the timestamp shape, + having no ledger to draw an ordinal from; an `architect` dispatch on + a bare question is one such, and the test is whether a document + exists rather than which agent ran. Where a round-ordinal name is + already taken, the new file appends the timestamp to it — + `-round---.md` — which settles the + superseded round two sessions can produce on one ordinal without + asking either to judge which report is stale. A timestamp name + collides only when a second record lands in the same second, where + appending the same timestamp would reproduce the taken name, so it + takes the lowest free counter suffix instead: `-2`, then `-3`. +- The relay opens with one header line, then one line per finding. The + header carries the verdict, the model self-report, the finding count + by severity, and how many decisions await the developer. Each + finding takes one line — severity, origin, claim — except where the + reviewer emits no origin: the `architect` reads one document and + every finding it returns originates there, so an architect relay's + line carries severity and claim alone. +- Condensation reaches narrative prose alone, whatever a session's + output style prescribes. The finding list is never condensed, and no + finding or its severity is ever dropped: the relay is the + developer's standing veto, so a finding nobody printed is a finding + nobody could overrule. Narrative is safe to condense because the + dispatch record holds the full text, which makes an expansion a + quotation rather than a reconstruction — so the relay names that + record's path, and the developer can open what was condensed. +- The header's count of decisions is the findings for which the + session can cite no license, derived at relay time from the report + and the decisions already written down — before any ledger line + exists, since `open` is written at stamp time and triage follows the + stamp. The `held` lines the same round later writes are that count's + check: they come to the same number, and a divergence means triage + found a license the relay missed, triage lost one the relay claimed, + or the oscillation tripwire held a finding that does carry a + license. The third is lawful, so the check reports the number rather + than asserting a fault. Verdicts that suspend autonomy — `blocking`, + the spent round cap, the all-Minor signal — stay out of the count: + they decide whether the loop continues rather than what a document + says, and the relay reports them in its own right. - A citation the report supplies — a file and line, an identifier, a count — is checked against what it names before it is written into the document or acted on by a fix. A precise citation reads like From 3c860ec6e60e6d9fd2092d04400898195823a7ca Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 16:05:55 +0200 Subject: [PATCH 105/120] feat(working-process): state the consultation floor and digest its relay --- plugins/working-process/rules/workflow.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/plugins/working-process/rules/workflow.md b/plugins/working-process/rules/workflow.md index 65d10ed..21ee54e 100644 --- a/plugins/working-process/rules/workflow.md +++ b/plugins/working-process/rules/workflow.md @@ -104,10 +104,24 @@ settled decisions separated from open questions — pointing at files rather than pasting them. A follow-up is a fresh cross-check dispatch, never a resumption. When both personas are consulted on one subject, give both the same canonical briefing, each with its own focusing -question appended, and tell neither what the other said. Relay each -contribution attributed and substantially verbatim, disagreements -presented as disagreements, and dispatch as a named background agent so -the transcript stays inspectable. +question appended, and tell neither what the other said. Dispatch as a +named background agent, so the transcript stays inspectable. + +A contribution is written to its dispatch record before it is relayed, +taking the timestamp shape of the two the verdict-agent dispatch +subsection defines: a consultation mints no round heading, so it has +no ordinal to derive, and several consultations of one persona on one +subject in one day are expected. The floor is met in that file — +attributed and substantially verbatim, compression allowed and merging +forbidden, every recommendation and every named risk surviving, and +text from two personas never landing in one bullet. The relay then +carries one paragraph per focusing question — or one per briefing, +where a single persona was consulted and none was appended — and the +path to the record. One thing stays in the relay whatever the floor +does: where the personas disagree, the disagreement is presented as a +disagreement with both positions, because a disagreement is the one +thing in a contribution the developer must decide and the digest +exists to raise decisions rather than bury them. When `docs/domain/glossary.md` exists in the project, its canonical terms and `_Avoid_` bans bind specs, plans, code identifiers, and From a8907ae6dc7b2e78635c879152cc6adf71af89fa Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 16:08:55 +0200 Subject: [PATCH 106/120] feat(working-process): hold a spec-origin finding at triage --- plugins/working-process/rules/workflow.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/working-process/rules/workflow.md b/plugins/working-process/rules/workflow.md index 21ee54e..90d2d77 100644 --- a/plugins/working-process/rules/workflow.md +++ b/plugins/working-process/rules/workflow.md @@ -330,6 +330,14 @@ decision. Consequences the loop states outright: - Only written decisions license fixes. A decision settled in conversation becomes citable by being written into the document, which the fix itself accomplishes. +- A finding whose `origin` names the spec is held unless a written + decision licenses the edit, since editing a spec from inside a plan + review is design work; `both` holds the same way, and its `held` + line names in `options:` which half is fixable at once. A licensed + spec-origin fix lands in the spec's own ledger and the plan's line + points at it, by the cross-document clause the spec-plan-lifecycle + rule defines — which also leaves the spec's `integrity:` stamp + stale, as any body edit does. A fix wave that deviates from a reviewer's suggestion records the deviation and its rationale beside the text they concern — not only in From 35903422370d2211da6b2f373f81599bfcc065dc Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 16:12:18 +0200 Subject: [PATCH 107/120] feat(working-process): meet the consultation floor in the dispatch record --- plugins/working-process/PERSONA_COMMON.md | 10 +++++++--- plugins/working-process/agents/architect-consult.md | 3 ++- .../working-process/agents/system-designer-consult.md | 3 ++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/plugins/working-process/PERSONA_COMMON.md b/plugins/working-process/PERSONA_COMMON.md index 7d29f90..c60e9a3 100644 --- a/plugins/working-process/PERSONA_COMMON.md +++ b/plugins/working-process/PERSONA_COMMON.md @@ -95,10 +95,14 @@ The briefing — the dispatcher's obligations: The reply — a contribution, never a finding, never a verdict; nothing from it is stamped into frontmatter: -- relayed to the developer attributed, in its own block, and - substantially verbatim — compression is allowed, merging is not; every +- written to the dispatch record and relayed from it. The record holds + the contribution attributed, in its own block, and substantially + verbatim — compression is allowed, merging is not; every recommendation and every named risk survives, and text from two - personas never lands in one bullet; + personas never lands in one bullet. The relay carries one paragraph + per focusing question — or one per briefing, where none was + appended — and the path to the record; the workflow rule defines + that record and names it; - disagreement between the personas is presented as a disagreement, with both positions — the choice is the developer's; the relaying thread may add its own opinion, marked as its own; diff --git a/plugins/working-process/agents/architect-consult.md b/plugins/working-process/agents/architect-consult.md index bbf6fb5..9a2f125 100644 --- a/plugins/working-process/agents/architect-consult.md +++ b/plugins/working-process/agents/architect-consult.md @@ -46,7 +46,8 @@ worth more to the developer than a confident answer built on it. - Never rewrite the developer's documents. Propose in your reply and let the dispatcher carry it. - Write the reply for the developer, not as a report to the dispatcher — - the contract has it relayed attributed and substantially verbatim. + the contract has it kept attributed and substantially verbatim in the + dispatch record, which the developer can open. - You may spawn helpers for search and documentation lookup — the domain-expertise duty often requires it. Never spawn another working-process persona: two personas reconciled inside one answer hand diff --git a/plugins/working-process/agents/system-designer-consult.md b/plugins/working-process/agents/system-designer-consult.md index e34e315..216bcdd 100644 --- a/plugins/working-process/agents/system-designer-consult.md +++ b/plugins/working-process/agents/system-designer-consult.md @@ -44,7 +44,8 @@ more to the developer than a confident proposal built on it. - Never rewrite the developer's documents. Propose in your reply and let the dispatcher carry it. - Write the reply for the developer, not as a report to the dispatcher — - the contract has it relayed attributed and substantially verbatim. + the contract has it kept attributed and substantially verbatim in the + dispatch record, which the developer can open. - You may spawn helpers for search and documentation lookup — the domain-expertise duty often requires it. Never spawn another working-process persona: two personas reconciled inside one answer hand From 7acf0a8c1b205fb6003ffa0b9529120de715645c Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 16:15:00 +0200 Subject: [PATCH 108/120] feat(working-process): emit the finding's origin from the plan adversary --- plugins/working-process/agents/plan-adversary.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/working-process/agents/plan-adversary.md b/plugins/working-process/agents/plan-adversary.md index e910c41..2525901 100644 --- a/plugins/working-process/agents/plan-adversary.md +++ b/plugins/working-process/agents/plan-adversary.md @@ -48,6 +48,12 @@ the review and point the dispatcher at the `architect` agent. Plan mechanics — named tests, per-phase commits, concrete paths — do not apply to a design document and would misfire as findings. +Naming a document is not reviewing it. A finding whose `origin` is +`spec` or `both` says where the defect traces to and proposes no change +to the spec, so this boundary holds: what to do about a spec-origin +finding is the dispatcher's, and its own rules hold one for the +developer unless a written decision licenses the edit. + ## Generic dimensions — walk every one; nothing passes by default ### 1. Scope against done-when @@ -93,6 +99,7 @@ to a design document and would misfire as findings. { "severity": "Critical" | "Important" | "Minor", "section": "", + "origin": "plan" | "spec" | "both", "claim": "", "evidence": "", "suggestion": "" From e444368f55a61b386445de0b447b04dee0e08571 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 16:17:29 +0200 Subject: [PATCH 109/120] feat(working-process): land a cross-document fix in its own ledger --- .../rules/spec-plan-lifecycle.md | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/plugins/working-process/rules/spec-plan-lifecycle.md b/plugins/working-process/rules/spec-plan-lifecycle.md index d5df639..fbd4987 100644 --- a/plugins/working-process/rules/spec-plan-lifecycle.md +++ b/plugins/working-process/rules/spec-plan-lifecycle.md @@ -226,6 +226,30 @@ their condition holds. Every terminal line carries exactly one authorizer. +A fix one review licenses can land in a document other than the +reviewed one — a plan review's finding carrying `origin: spec` is the +case the workflow rule names. The disposition line then lands in the +ledger of the document that changed, and the reviewed document's line +points at it in its `` clause, naming that document and +the heading the line sits under: one authorizer, two documents, two +lines, and the invariant above holds on each. The pointer needs no +clause of its own — `` already belongs to the `fixed` +line's shape, so the clause table stays closed. In the changed +document the line goes under its latest round heading, or with the +body note the resolution annotation above already owes, where that +document is stamped and no fresh round ran. A document whose loop +closed at `LGTM` has neither — the annotation is defined for `concerns` +and for a `blocking` verdict closed by adjudication, never for `LGTM` — +so the fix opens a heading of its own: + + ### — fix from + +It carries no ordinal and no verdict, which keeps it out of the round +cap's derivation and out of the Unfinished-work commands that anchor a +round heading: it records a fix, not a round. Two stamps go stale as on +any body edit — the `integrity:` hash stops matching, and the verdict +stops certifying the words that changed. + Payload costs nothing structurally. An Unfinished-work command under the list's default scope guard is held to the frontmatter block, so no body line reaches it at all; an entry publishing its own scope anchors a @@ -234,7 +258,8 @@ continuation matches neither. Payload under a line is therefore invisible to every published command, and where it runs long it belongs in indented sub-bullets rather than in a longer line. -One annotation extends those shapes, and nothing else does. A +Two additions extend those shapes, and nothing else does: the +annotation here, and the cross-document fix heading above. A diff-scoped `LGTM` heading gains `, debt discharged ` once the chain debt it carries is discharged, and its presence defeats the consumption gate's re-ask, as `, waived ` defeats the re-review @@ -316,7 +341,9 @@ token tells a gate line apart from that round's own findings; the date does not, since a gate episode and the round it precedes commonly share one. A gate still never mints a heading of its own, on the separate ground that the round heading's grammar is closed and a gate is not a -round. Writing at gate +round. The fix heading above is no counter-example: a gate's lines can +wait for the round that is coming, while a fix landing on a document +whose loop closed at `LGTM` waits for nothing. Writing at gate time is what the lines are for: a dismissal must exist while the episode is still re-dispatching, or the gate cannot terminate, and the next diff-scoped brief is composed before its own round is stamped. A gate From 20e33665aa0b82ac8f6a71235e2d3992443c034c Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 16:20:15 +0200 Subject: [PATCH 110/120] docs(glossary): mint the dispatch record and move the relay's floor --- docs/domain/glossary.md | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/domain/glossary.md b/docs/domain/glossary.md index 7aee38c..7ee9faf 100644 --- a/docs/domain/glossary.md +++ b/docs/domain/glossary.md @@ -420,12 +420,26 @@ _Avoid_: review agent (unqualified), reviewer agent **Relay**: The delivery of a background agent's result to the developer before any -further action on it — a verdict agent's report before its stamp (the -verdict, the model self-report, and every finding in substance), a -consultation's Contribution attributed and substantially verbatim. The -developer's standing veto point. +further action on it — a verdict agent's report before its stamp (a +header carrying the verdict, the model self-report and the counts, then +one line per finding, that list never condensed), a consultation's +Contribution digested one paragraph per focusing question. Either way it +names the Dispatch record holding the full text. The developer's +standing veto point. _Avoid_: report back, forward (as the term) +**Dispatch record**: +The file a dispatcher writes before relaying a background agent's +result — one dispatch, one file, holding what the agent returned +verbatim under a four-line header (date, agent, model self-report, +subject). Kept per subject under `.claude/working-process//`, +git-ignored, and NOT a Process directory — a per-checkout store under +the `.claude/` config namespace, as Private memory is a per-user one. It +survives compaction, the session's end and a branch switch, and no more: +that bound is what lets a Relay condense. Never a Review report, the +counted document a code-review run writes under `docs/code-review/`. +_Avoid_: dispatch log, transcript file + **Session skill**: A skill whose content is an open-ended conversation, named `*-session` — `architect-session`, `grilling-session`, `memory-review-session` among @@ -444,9 +458,10 @@ _Avoid_: informal review, advisory review **Contribution**: What a consultation returns: reasoning, options, and the questions the -persona would need answered next, relayed to the developer attributed and -substantially verbatim. Never graded and never counted — the graded, -counted unit is a Finding, which belongs to verdict-bearing reviews. +persona would need answered next, held attributed and substantially +verbatim in its Dispatch record and relayed from there as a digest. +Never graded and never counted — the graded, counted unit is a Finding, +which belongs to verdict-bearing reviews. _Avoid_: consultation finding, recommendation **Project memory**: From 8cc3c985757a8f6ccbceaacd59217bf1d09c0d1f Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 16:38:22 +0200 Subject: [PATCH 111/120] chore(working-process): mint the process-wave-five dogfood prerelease --- plugins/working-process/.claude-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/working-process/.claude-plugin/plugin.json b/plugins/working-process/.claude-plugin/plugin.json index a1169b5..ea50aa7 100644 --- a/plugins/working-process/.claude-plugin/plugin.json +++ b/plugins/working-process/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "working-process", "description": "Spec-driven working process on top of superpowers: grilling-session, architect-session, system-designer-session, process-status and sync-rules skills, architect and plan-adversary verdict agents, architect-consult and system-designer-consult consultation agents, propagation-auditor and integrity-auditor audit agents, and process rules distributed as a Rules payload; domain plugins hook in via *-plan-review checklist skills and their own rules/ payloads", - "version": "0.17.0-dev.process-wave-four", + "version": "0.17.0-dev.process-wave-five", "author": { "name": "Missing Bits (Jacek Nakonieczny)" }, "license": "MIT", "keywords": ["process", "workflow", "spec", "plan", "review", "glossary", "adr", "architecture"], From b388f14cd036ee0d28c8e2e33d0e8c9c17bfdd38 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 16:38:39 +0200 Subject: [PATCH 112/120] docs: mark the errata wave-five spec and plan implemented --- docs/plans/2026-09-15-review-loop-errata-wave-five.md | 2 +- docs/specs/2026-09-14-review-loop-errata-wave-five.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plans/2026-09-15-review-loop-errata-wave-five.md b/docs/plans/2026-09-15-review-loop-errata-wave-five.md index 9a0497e..d5edd97 100644 --- a/docs/plans/2026-09-15-review-loop-errata-wave-five.md +++ b/docs/plans/2026-09-15-review-loop-errata-wave-five.md @@ -1,7 +1,7 @@ --- ticket: none date: 2026-09-15 -status: approved +status: implemented adversary: concerns (resolved 2026-09-15) spec: ../specs/2026-09-14-review-loop-errata-wave-five.md branch: feature/process-wave-five diff --git a/docs/specs/2026-09-14-review-loop-errata-wave-five.md b/docs/specs/2026-09-14-review-loop-errata-wave-five.md index 1df7088..a2da3c9 100644 --- a/docs/specs/2026-09-14-review-loop-errata-wave-five.md +++ b/docs/specs/2026-09-14-review-loop-errata-wave-five.md @@ -1,7 +1,7 @@ --- ticket: none date: 2026-09-14 -status: approved +status: implemented grilled: 2026-09-14 architect: concerns (resolved 2026-09-15) integrity: 2026-09-15 (sha: 2673d32) From fec6cd300ece2268af3c07c9a228e1bc92858553 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 17:07:58 +0200 Subject: [PATCH 113/120] docs: mark the salesforce trigger-frameworks spec and plan implemented --- docs/plans/2026-09-08-salesforce-trigger-frameworks.md | 2 +- docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md index 84c868b..50daf8e 100644 --- a/docs/plans/2026-09-08-salesforce-trigger-frameworks.md +++ b/docs/plans/2026-09-08-salesforce-trigger-frameworks.md @@ -1,7 +1,7 @@ --- ticket: none date: 2026-09-08 -status: approved +status: implemented adversary: concerns (resolved 2026-09-08) spec: ../specs/2026-09-08-salesforce-trigger-frameworks-design.md branch: feature/trigger-frameworks diff --git a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md index d789e3a..0222234 100644 --- a/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md +++ b/docs/specs/2026-09-08-salesforce-trigger-frameworks-design.md @@ -1,7 +1,7 @@ --- ticket: none date: 2026-09-08 -status: draft +status: implemented grilled: 2026-09-08 architect: LGTM integrity: 2026-09-08 (sha: 715798b) From 7816426de759c8aef194e7a93f93b7a7c97f9425 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 20:25:11 +0200 Subject: [PATCH 114/120] docs: add a changelog to every plugin --- plugins/project-memory/CHANGELOG.md | 58 ++++++++ plugins/python-standards/CHANGELOG.md | 43 ++++++ plugins/salesforce-standards/CHANGELOG.md | 49 ++++++ plugins/working-process/CHANGELOG.md | 174 ++++++++++++++++++++++ 4 files changed, 324 insertions(+) create mode 100644 plugins/project-memory/CHANGELOG.md create mode 100644 plugins/python-standards/CHANGELOG.md create mode 100644 plugins/salesforce-standards/CHANGELOG.md create mode 100644 plugins/working-process/CHANGELOG.md diff --git a/plugins/project-memory/CHANGELOG.md b/plugins/project-memory/CHANGELOG.md new file mode 100644 index 0000000..1612839 --- /dev/null +++ b/plugins/project-memory/CHANGELOG.md @@ -0,0 +1,58 @@ +# Changelog — project-memory + +Released versions, newest first. The marketplace serves what `master` +holds; `## Unreleased` collects what has landed on `develop` since the +last release. + +## 0.5.1 — 2026-09-15 + +- The plugin ships this changelog. + +## 0.5.0 — 2026-08-29 + +- The grooming audit reads a relocated `status` field, and the + conventions rule states where the field is written alongside the + tolerant read. +- The `adr-candidate` sweep anchors its grep per the rule's own clause. + +## 0.4.0 — 2026-08-12 + +- Every entry opens with an H1 and carries a one-line `description:`. + The index line is a projection of the two, and the entry wins on + drift. The plugin's own frontmatter fields stay top-level, with a + tolerance clause for keys other tools wrote. +- The live index renames `INDEX.md` to `MEMORY.md` in both parts; + `ARCHIVE.md` keeps its name. The core rule reads a pre-rename store, + and the conventions rule offers a one-time rename that a decline + leaves readable. +- The Hybrid store arrives as an opt-in switch: the new + `redirect-memory` skill points Claude Code's Auto-memory at + `.claude/memory/` through `autoMemoryDirectory`, always as the current + environment's realpath. The core rule deduplicates index loading and + reports a divergent redirect. +- `memory-review-session` audits `MEMORY.md`, re-sections + harness-appended lines, and scopes a walk as a first-class choice + while the audit stays whole-part. +- `migrate-memory` path-qualifies store names and detects the hybrid + no-op. +- Entry prose routes through + `elements-of-style:writing-clearly-and-concisely` when the skill is + available. + +## 0.3.0 — 2026-08-05 + +- The `docs/memory` first-create question reads a declared instruction + as the decision already made. +- Trigger evals cover the memory skills, including the cross-project + pair. + +## 0.2.0 — 2026-07-22 + +- The `memory-review-session` grooming skill and the `migrate-memory` + home-dir-to-project skill. +- The index holds live entries only; closed entries move to an + `ARCHIVE.md` read on demand. + +## 0.1.0 — 2026-07-21 + +- The project-memory rules leave `working-process` for this plugin. diff --git a/plugins/python-standards/CHANGELOG.md b/plugins/python-standards/CHANGELOG.md new file mode 100644 index 0000000..21ec17f --- /dev/null +++ b/plugins/python-standards/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog — python-standards + +Released versions, newest first. The marketplace serves what `master` +holds; `## Unreleased` collects what has landed on `develop` since the +last release. + +## 0.3.2 — 2026-09-15 + +- The plugin ships this changelog. + +## 0.3.1 — 2026-08-12 + +- The store-probe restatement in the code-review skill tests the + Project-memory part directories, matching the updated review-reports + contract. Behavior-compatible for every store the plugin ever created. + +## 0.3.0 — 2026-08-05 + +- The review cascade grades from the tags: a plugin-level `rule-none`, + a justify clause, the finding unit, candidate-gap offers, and a + standalone fallback. +- `/python-review` dispatches the reviewer in the background, with the + first-create check gated by a store probe. +- A rerun can no longer inherit a prior report's counting policy. + +## 0.2.0 — 2026-07-23 + +- Six area skills: code-style, project-layout, typing, testing, cli, + and web-api. +- The `python-code-review` skill, the `python-code-reviewer` agent, the + `/python-review` command, and the `python-plan-review` checklist + skill. +- The `python-toolchain` Rules payload, with ruff's line length set to + 120. +- Every rule names its source — a PEP, an RFC, the official docs, or + this standard. The web-api skill adopts RFC 9457 problem details with + a `code` extension member, and the docstring norm is Google style with + a project override. +- The area skills adopt the severity-in-tag model. + +## 0.1.0 — 2026-07-16 + +- The plugin joins the marketplace. diff --git a/plugins/salesforce-standards/CHANGELOG.md b/plugins/salesforce-standards/CHANGELOG.md new file mode 100644 index 0000000..df14e5d --- /dev/null +++ b/plugins/salesforce-standards/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog — salesforce-standards + +Released versions, newest first. The marketplace serves what `master` +holds; `## Unreleased` collects what has landed on `develop` since the +last release. + +## 0.4.0 — 2026-09-15 + +- The new `salesforce-triggers` skill carries the rules that hold + whatever framework a project uses, and the trigger content leaves + `salesforce-apex`. +- A resolution protocol identifies the project's trigger framework + before Apex is graded, and three documents describe what each one + expects — frameworkless, base-class, and metadata-driven — with a + comparison for choosing among them. +- The toolchain rule routes triggers and documents the declaration keys, + naming `.claude/CLAUDE.md` among the homes a declaration may take. +- The plugin ships this changelog. + +## 0.3.1 — 2026-08-12 + +- The store-probe restatement in the code-review skill tests the + Project-memory part directories, matching the updated review-reports + contract. Behavior-compatible for every store the plugin ever created. + +## 0.3.0 — 2026-08-05 + +- The review cascade grades from the tags: a plugin-level `rule-none`, + a justify clause, the finding unit, candidate-gap offers, and a + standalone fallback. +- `/salesforce-review` dispatches the reviewer in the background, with + the first-create check gated by a store probe. +- The severity-in-tag model reaches the remaining area skills — lwc, + flow, data-model, security-model, aura, and visualforce. +- A rerun can no longer inherit a prior report's counting policy. + +## 0.2.0 — 2026-07-23 + +- Eight area skills: apex, apex-testing, lwc, flow, data-model, + security-model, aura, and visualforce. +- The `salesforce-code-review` skill, the `salesforce-code-reviewer` + agent, and the `/salesforce-review` command. +- The `salesforce-plan-review` checklist skill and the + `salesforce-toolchain` Rules payload. +- Apex and apex-testing adopt the severity-in-tag model. + +## 0.1.0 — 2026-07-20 + +- The plugin joins the marketplace. diff --git a/plugins/working-process/CHANGELOG.md b/plugins/working-process/CHANGELOG.md new file mode 100644 index 0000000..1ae072e --- /dev/null +++ b/plugins/working-process/CHANGELOG.md @@ -0,0 +1,174 @@ +# Changelog — working-process + +Released versions, newest first. The marketplace serves what `master` +holds; `## Unreleased` collects what has landed on `develop` since the +last release. + +## 0.17.0 — 2026-09-15 + +- A plan's review loop closes only on a full-document round: a + diff-scoped LGTM certifies a chain, so one confirming round follows it + and carries the stamp. The rules define which heading counts as the + latest, widen the plan exception to the relay-then-stamp order, and + bound what the round cap promises. +- The propagation duties ship as an author-facing rule, so an author + meets them while writing rather than through the auditor's hits. Two + hits on one document are reported as a shape, and the co-firing line + states a shape rather than a judgment. +- The verdict relay opens with a header line — verdict, model + self-report, findings by severity, decisions awaiting the developer — + and every dispatch writes a record to `.claude/working-process/` + before the relay. A consultation meets the same floor in its record, + and its relay digests one paragraph per focusing question. +- The plan adversary emits each finding's origin, triage holds a + spec-origin finding unless a written decision licenses the edit, and a + licensed cross-document fix lands in the spec's own ledger. +- Feature work follows the published branch-naming convention, + `feature/-`. +- The plugin ships this changelog. + +## 0.16.0 — 2026-09-07 + +- A reviewer's citation is verified against what it names before it + enters the record. A precise citation reads like verification and is + not one, so the dispatcher checks the file, the line, and the + identifier the report quotes. +- An audit report's body governs, never its closing token: located hits + beside a `CLEAN` line mean the gate has not passed. +- The propagation audit parts a prescribed block from a reported one, + and both identity surfaces name the partition. +- The integrity auditor's card states its measurements as facts rather + than rates, and drops the threshold magnitudes. + +## 0.15.0 — 2026-09-07 + +- Chain debt: a diff-scoped LGTM certifies a chain rather than a fresh + whole-document read. Three paths discharge the debt — an integrity + audit, a full-document round, or the developer's recorded decline — + and all three write one `debt discharged` annotation. +- Errata wave one sharpens the loop: gate lines get their own shape, a + `revises` field records what a document supersedes, and the + terminators state what each one suspends. +- The disposition ledger gains four states and an authorizer clause, a + clause table, and prose tolerance for the shapes earlier rounds wrote. + Relitigation branches on the authorizer clause, and the ledger stands + as context for a diff-scoped round. +- A reviewer's stop signal gets a ledger line, so a later session can + cite what a round judged its leftovers to be worth. +- Per-round commits on a document branch, offered as the second clause + of the loop's autonomy question. +- Developer contact is defined, the round cap is stated as best-effort, + and a plan's confirming round counts against it. + +## 0.14.0 — 2026-08-29 + +- Verdict agents run in the background: `architect` and `plan-adversary` + return a self-describing report to a subject-keyed output, and the + dispatcher relays before it stamps. +- Two audit agents join the family. `propagation-auditor` checks a spec + or plan for mechanical consistency before an expensive dispatch; + `integrity-auditor` reads a churned spec on a fresh context at the + consumption gate. Neither grades nor stamps — a passing audit is a + precondition for the work that follows. +- The autonomous review loop: triage by license rather than by grade, a + disposition ledger carrying each finding's state, the `integrity:` + stamp, and the terminators that end an unattended run. +- The new `process-status` skill reports what the process left + unfinished in a repo, and publishes the unfinished-work list as a + named section. +- A code review is offered after implementation, before a plan's + `status` moves to `implemented`. +- The ticket sweep tolerates a relocated field, quotes hash-leading + values, and bounds itself to one line. + +## 0.13.0 — 2026-08-12 + +- The commit suggestion fires only at the implementation-ready gate. +- A Project-memory review is offered when a document reaches + `implemented`. +- Prose under `docs/` routes through + `elements-of-style:writing-clearly-and-concisely` when the skill is + available. +- Store probes test the Project-memory part directories instead of the + index filename, and the registry-file list delegates to the + project-memory conventions rule. + +## 0.12.0 — 2026-08-05 + +- The design personas ship as a pair: `architect-consult` and + `system-designer-consult` return one contribution from a fresh + context, and the `system-designer-session` skill runs the live + counterpart. A shared persona file carries the consultation contract, + and the workflow rule carries the dispatcher's duties. +- The workflow rule asks once per conversation whether the personas may + be consulted as a design forms, and re-asks when compaction obscures + the answer. +- Every persona surface resolves domain artifacts from the repo root. +- The review-reports contract names the finding unit, the citation + rules, and background dispatch. +- The first-create question reads a declared instruction as the + decision, and names how each mode is materialized. +- Trigger phrases across all surfaces are English. + +## 0.11.0 — 2026-07-22 + +- The review contract owns its `rule-none` citations, labels findings by + kind, and offers candidate gaps. + +## 0.10.0 — 2026-07-22 + +- The project-memory rules leave this plugin for the `project-memory` + plugin. + +## 0.9.0 — 2026-07-20 + +- Line-less review findings sort by a stable key the domain states. + +## 0.8.0 — 2026-07-20 + +- Project memory arrives as two rules: an always-on core rule and a + `paths:`-scoped conventions rule. Notes are exempt from the ticket + requirement. + +## 0.7.0 — 2026-07-17 + +- Model selection: the workflow rule carries the tier heuristic and the + cap protocol, both verdict agents carry the dispatch-tier directive + and report their own model, and the lifecycle rule gains the + `fallback` field with its re-review offer. + +## 0.6.0 — 2026-07-17 + +- Report filenames drop the prompt text, and the process-artifacts and + ticket-frontmatter rules cover review reports. + +## 0.5.0 — 2026-07-16 + +- The review-reports rule. + +## 0.4.0 — 2026-07-16 + +- Reviews record their findings, and a resolved concern carries an + annotation. + +## 0.3.0 — 2026-07-13 + +- The rules engine: the `sync-rules` skill installs, updates, and + removes distributed rules, a SessionStart hook reports drift, and a + single-writer manifest script guards the installed set. +- Four rules ship — workflow, spec-plan-lifecycle, process-artifacts, + and ticket-frontmatter. +- The persona file becomes `ARCHITECT_PERSONA.md`. + +## 0.2.0 — 2026-07-13 + +- The ruleset hashing script. + +## 0.1.1 — 2026-07-13 + +- The `architect` and `plan-adversary` agents, the `architect-session` + and `grilling-session` skills, and the plugin README. + +## 0.1.0 — 2026-07-13 + +- The plugin manifest and the architect persona. From d7dab0abda61529d74ba0c2c04d1b4b1ddd951b6 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 20:25:11 +0200 Subject: [PATCH 115/120] chore: mint release versions - working-process 0.17.0, salesforce-standards 0.4.0, project-memory 0.5.1, python-standards 0.3.2 --- plugins/project-memory/.claude-plugin/plugin.json | 2 +- plugins/python-standards/.claude-plugin/plugin.json | 2 +- plugins/salesforce-standards/.claude-plugin/plugin.json | 2 +- plugins/working-process/.claude-plugin/plugin.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/project-memory/.claude-plugin/plugin.json b/plugins/project-memory/.claude-plugin/plugin.json index afafaf4..4d0c369 100644 --- a/plugins/project-memory/.claude-plugin/plugin.json +++ b/plugins/project-memory/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "project-memory", "description": "In-repo project memory for Claude Code sessions — committed Team memory (docs/memory/) and git-ignored per-user Private memory (.claude/memory/); a Rules payload plus memory-review-session, migrate-memory, and redirect-memory skills", - "version": "0.5.0", + "version": "0.5.1", "author": { "name": "Missing Bits (Jacek Nakonieczny)" }, "license": "MIT", "keywords": ["memory", "notes", "ideas", "knowledge", "rules"] diff --git a/plugins/python-standards/.claude-plugin/plugin.json b/plugins/python-standards/.claude-plugin/plugin.json index 915f0eb..0e416e4 100644 --- a/plugins/python-standards/.claude-plugin/plugin.json +++ b/plugins/python-standards/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "python-standards", "description": "Python coding standards for the uv + ruff + pytest + pyright toolchain: six area skills, a code-review stack (python-code-review skill, python-code-reviewer agent, /python-review command) writing review reports to docs/code-review/, a python-plan-review checklist for plan reviews, and a python-toolchain rule shipped as a Rules payload", - "version": "0.3.1", + "version": "0.3.2", "author": { "name": "Missing Bits (Jacek Nakonieczny)" }, "license": "MIT", "keywords": ["python", "standards", "uv", "ruff", "pytest", "pyright", "typer", "fastapi", "review"] diff --git a/plugins/salesforce-standards/.claude-plugin/plugin.json b/plugins/salesforce-standards/.claude-plugin/plugin.json index 5c6741c..54f59b0 100644 --- a/plugins/salesforce-standards/.claude-plugin/plugin.json +++ b/plugins/salesforce-standards/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "salesforce-standards", "description": "Salesforce coding standards for the sf CLI + Code Analyzer + Prettier-Apex + sfdx-lwc-jest toolchain: nine area skills (Apex, Apex triggers, Apex testing, LWC, Flow, data model, security model, maintenance-first Aura and Visualforce), a code-review stack (salesforce-code-review skill, salesforce-code-reviewer agent, /salesforce-review command) writing review reports to docs/code-review/, a salesforce-plan-review checklist for plan reviews, and a salesforce-toolchain rule shipped as a Rules payload", - "version": "0.4.0-dev.trigger-frameworks", + "version": "0.4.0", "author": { "name": "Missing Bits (Jacek Nakonieczny)" }, "license": "MIT", "keywords": ["salesforce", "standards", "apex", "triggers", "lwc", "flow", "aura", "visualforce", "sf-cli", "review"] diff --git a/plugins/working-process/.claude-plugin/plugin.json b/plugins/working-process/.claude-plugin/plugin.json index ea50aa7..20e1255 100644 --- a/plugins/working-process/.claude-plugin/plugin.json +++ b/plugins/working-process/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "working-process", "description": "Spec-driven working process on top of superpowers: grilling-session, architect-session, system-designer-session, process-status and sync-rules skills, architect and plan-adversary verdict agents, architect-consult and system-designer-consult consultation agents, propagation-auditor and integrity-auditor audit agents, and process rules distributed as a Rules payload; domain plugins hook in via *-plan-review checklist skills and their own rules/ payloads", - "version": "0.17.0-dev.process-wave-five", + "version": "0.17.0", "author": { "name": "Missing Bits (Jacek Nakonieczny)" }, "license": "MIT", "keywords": ["process", "workflow", "spec", "plan", "review", "glossary", "adr", "architecture"], From f116570d719ae8e7d35d936d268655538d7a0a03 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 21:01:29 +0200 Subject: [PATCH 116/120] docs: state where a plugin's changelog lives and when its entry is written --- .claude/rules/plugin-versioning.md | 38 ++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/.claude/rules/plugin-versioning.md b/.claude/rules/plugin-versioning.md index 3263b59..a0ac2c0 100644 --- a/.claude/rules/plugin-versioning.md +++ b/.claude/rules/plugin-versioning.md @@ -18,6 +18,18 @@ paths: merging to master without changing that string delivers nothing to existing users, so a release PR with no bumps for its changed plugins is invalid. +- Every plugin keeps a `CHANGELOG.md` at its root: released versions + newest first, each heading carrying the version and its date. The file + installs with the plugin, so a copy on disk carries the history of + exactly that copy. +- A topic branch writes its entry under an `## Unreleased` heading + before it merges into `develop`, while the author still knows what the + change means. The release PR renames that heading to + `## ` in the commit that mints the version, and the + PR body is assembled from the entries that changed. +- A plugin whose only change is its own changelog takes a patch bump, + for the reason the bump bullet gives: the file reaches an installed + copy the way any other content does. - Dogfooding unreleased content needs a changed version string — the plugin cache keys content by version. A topic branch that dogfoods a plugin sets `X.Y.Z-dev.` on it — the issue number the @@ -25,20 +37,20 @@ paths: issue (e.g. `-dev.design-personas`). Widening the discriminator instead of minting another channel keeps one channel for one purpose; the discriminator only needs to be unique among parallel topics. The - suffix flows into develop as-is. A topic that - does not dogfood never touches the version. On a version-line merge - conflict between parallel topics, the merging topic's own - `-dev.` wins — both strings are provisional. The release PR - strips every `-dev` suffix while minting the final numbers; the - `release-guard` workflow fails any PR to master that carries a - prerelease version or a changed plugin without a bump. + suffix flows into develop as-is. A topic that does not dogfood never + touches the version. On a version-line merge conflict between parallel + topics, the merging topic's own `-dev.` wins — both + strings are provisional. The release PR strips every `-dev` suffix + while minting the final numbers; the `release-guard` workflow fails + any PR to master that carries a prerelease version or a changed plugin + without a bump. - Prerelease grammar: `-.`. Defined channels: - `dev.` (topic-branch dogfooding, above) and `rc.` (release - candidate — a freeze of develop dogfooded as one bundle when a - release warrants whole-unit validation; minted by a release-prep - commit and stripped by the release PR like any prerelease). Future - channels extend this list by editing this rule only — the - release-guard workflow rejects every prerelease on master + `dev.` (topic-branch dogfooding, above) and + `rc.` (release candidate — a freeze of develop dogfooded as one + bundle when a release warrants whole-unit validation; minted by a + release-prep commit and stripped by the release PR like any + prerelease). Future channels extend this list by editing this rule + only — the release-guard workflow rejects every prerelease on master (`*-*`), so new channels never need a CI change. - **patch** — wording or docs fixes, no behavior change; - **minor** — a new component or section, backward-compatible behavior From db7e9a5bcc9c2442e5dfe1d357c82231ae15fbfd Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 21:02:43 +0200 Subject: [PATCH 117/120] ci: check a bumped plugin's changelog heading and ban an Unreleased section on master --- .github/workflows/release-guard.yml | 41 ++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-guard.yml b/.github/workflows/release-guard.yml index 423fbfb..c88c804 100644 --- a/.github/workflows/release-guard.yml +++ b/.github/workflows/release-guard.yml @@ -1,7 +1,9 @@ # Release guard — runs on PRs targeting master (release PRs from develop). # Enforces the plugin-versioning rule's release-time invariants: # 1. no prerelease (-dev.*) version string reaches master; -# 2. every plugin changed since master carries a version bump. +# 2. every plugin changed since master carries a version bump; +# 3. a bumped plugin's changelog names that version at its top; +# 4. no Unreleased section survives the release. name: release-guard on: @@ -30,6 +32,43 @@ jobs: done exit $fail + - name: A bumped plugin's changelog names the new version + run: | + base="origin/${{ github.base_ref }}" + fail=0 + for d in plugins/*/; do + name=$(basename "$d") + manifest="${d}.claude-plugin/plugin.json" + head_v=$(jq -r .version "$manifest") + base_v=$(git show "$base:$manifest" 2>/dev/null | jq -r .version || echo "") + if [ "$base_v" = "$head_v" ]; then continue; fi + changelog="${d}CHANGELOG.md" + if [ ! -f "$changelog" ]; then + echo "::error file=$manifest::plugin '$name' moved to '$head_v' but ships no CHANGELOG.md" + fail=1 + continue + fi + top=$(grep -m1 '^## ' "$changelog" || true) + escaped=$(printf '%s' "$head_v" | sed 's/[.]/\\./g') + if ! printf '%s\n' "$top" | grep -qE "^## $escaped — [0-9]{4}-[0-9]{2}-[0-9]{2}$"; then + echo "::error file=$changelog::plugin '$name' moved to '$head_v' but its top heading reads '$top' — expected '## $head_v — '" + fail=1 + fi + done + exit $fail + + - name: No Unreleased section reaches master + run: | + fail=0 + for f in plugins/*/CHANGELOG.md; do + [ -f "$f" ] || continue + if grep -q '^## Unreleased' "$f"; then + echo "::error file=$f::rename the Unreleased heading to the minted version before releasing" + fail=1 + fi + done + exit $fail + - name: Changed plugins carry a bump run: | base="origin/${{ github.base_ref }}" From 00927f52d5d20f08a63d77c6eebe9f2852f0ede8 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 21:02:43 +0200 Subject: [PATCH 118/120] docs: cut the authoring mechanics from the plugin changelog preambles --- plugins/project-memory/CHANGELOG.md | 4 +--- plugins/python-standards/CHANGELOG.md | 4 +--- plugins/salesforce-standards/CHANGELOG.md | 4 +--- plugins/working-process/CHANGELOG.md | 4 +--- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/plugins/project-memory/CHANGELOG.md b/plugins/project-memory/CHANGELOG.md index 1612839..9d3119a 100644 --- a/plugins/project-memory/CHANGELOG.md +++ b/plugins/project-memory/CHANGELOG.md @@ -1,8 +1,6 @@ # Changelog — project-memory -Released versions, newest first. The marketplace serves what `master` -holds; `## Unreleased` collects what has landed on `develop` since the -last release. +Released versions of this plugin, newest first. ## 0.5.1 — 2026-09-15 diff --git a/plugins/python-standards/CHANGELOG.md b/plugins/python-standards/CHANGELOG.md index 21ec17f..04e14c0 100644 --- a/plugins/python-standards/CHANGELOG.md +++ b/plugins/python-standards/CHANGELOG.md @@ -1,8 +1,6 @@ # Changelog — python-standards -Released versions, newest first. The marketplace serves what `master` -holds; `## Unreleased` collects what has landed on `develop` since the -last release. +Released versions of this plugin, newest first. ## 0.3.2 — 2026-09-15 diff --git a/plugins/salesforce-standards/CHANGELOG.md b/plugins/salesforce-standards/CHANGELOG.md index df14e5d..046754e 100644 --- a/plugins/salesforce-standards/CHANGELOG.md +++ b/plugins/salesforce-standards/CHANGELOG.md @@ -1,8 +1,6 @@ # Changelog — salesforce-standards -Released versions, newest first. The marketplace serves what `master` -holds; `## Unreleased` collects what has landed on `develop` since the -last release. +Released versions of this plugin, newest first. ## 0.4.0 — 2026-09-15 diff --git a/plugins/working-process/CHANGELOG.md b/plugins/working-process/CHANGELOG.md index 1ae072e..2a44099 100644 --- a/plugins/working-process/CHANGELOG.md +++ b/plugins/working-process/CHANGELOG.md @@ -1,8 +1,6 @@ # Changelog — working-process -Released versions, newest first. The marketplace serves what `master` -holds; `## Unreleased` collects what has landed on `develop` since the -last release. +Released versions of this plugin, newest first. ## 0.17.0 — 2026-09-15 From 2a793f1a5fe6612619a7da52c5b37c23e9b9a67b Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 21:19:12 +0200 Subject: [PATCH 119/120] docs: place the Unreleased heading and fold the changelog bump clause --- .claude/rules/plugin-versioning.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.claude/rules/plugin-versioning.md b/.claude/rules/plugin-versioning.md index a0ac2c0..c1fbcd3 100644 --- a/.claude/rules/plugin-versioning.md +++ b/.claude/rules/plugin-versioning.md @@ -21,15 +21,13 @@ paths: - Every plugin keeps a `CHANGELOG.md` at its root: released versions newest first, each heading carrying the version and its date. The file installs with the plugin, so a copy on disk carries the history of - exactly that copy. -- A topic branch writes its entry under an `## Unreleased` heading - before it merges into `develop`, while the author still knows what the - change means. The release PR renames that heading to - `## ` in the commit that mints the version, and the - PR body is assembled from the entries that changed. -- A plugin whose only change is its own changelog takes a patch bump, - for the reason the bump bullet gives: the file reaches an installed - copy the way any other content does. + exactly that copy, and a plugin whose only change is that file still + takes a patch bump. +- A topic branch writes its entry under an `## Unreleased` heading at + the top of the file, before it merges into `develop`, while the author + still knows what the change means. The release PR renames that heading + to `## ` in the commit that mints the version, and + the PR body is assembled from the entries that changed. - Dogfooding unreleased content needs a changed version string — the plugin cache keys content by version. A topic branch that dogfoods a plugin sets `X.Y.Z-dev.` on it — the issue number the From cf7d4c2564579c7413578dd41b2696f212314694 Mon Sep 17 00:00:00 2001 From: Jacek Nakonieczny Date: Tue, 15 Sep 2026 21:22:59 +0200 Subject: [PATCH 120/120] docs: a dev prerelease hangs off a version above what develop carries --- .claude/rules/plugin-versioning.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.claude/rules/plugin-versioning.md b/.claude/rules/plugin-versioning.md index c1fbcd3..8f248cb 100644 --- a/.claude/rules/plugin-versioning.md +++ b/.claude/rules/plugin-versioning.md @@ -32,10 +32,12 @@ paths: plugin cache keys content by version. A topic branch that dogfoods a plugin sets `X.Y.Z-dev.` on it — the issue number the branch name carries, or the branch short-name when the topic has no - issue (e.g. `-dev.design-personas`). Widening the discriminator - instead of minting another channel keeps one channel for one purpose; - the discriminator only needs to be unique among parallel topics. The - suffix flows into develop as-is. A topic that does not dogfood never + issue (e.g. `-dev.design-personas`). The suffix hangs off a version + above the one `develop` currently carries — never a re-suffix of a + version already minted. Widening the discriminator instead of minting + another channel keeps one channel for one purpose; the discriminator + only needs to be unique among parallel topics. The suffix flows into + develop as-is. A topic that does not dogfood never touches the version. On a version-line merge conflict between parallel topics, the merging topic's own `-dev.` wins — both strings are provisional. The release PR strips every `-dev` suffix