Skip to content

[Plugin Generator] Split core into composable plugin sets (#315) - #340

Open
isolomatov-gd wants to merge 40 commits into
mainfrom
feat/315-plugin-sets
Open

[Plugin Generator] Split core into composable plugin sets (#315)#340
isolomatov-gd wants to merge 40 commits into
mainfrom
feat/315-plugin-sets

Conversation

@isolomatov-gd

@isolomatov-gd isolomatov-gd commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Implements #315. Splits instructions/r3/core/ into five sibling domain setscore, workflows, qe, search, modernization — and builds every set × IDE combination from a declarative catalog in one generator invocation.

Output is 49 folders: rosetta-<ide> (the full combo), rosetta-<ide>-light, and the five split sets. core-<ide> is the core set alone; the full plugin is rosetta-<ide>. scripts/pre_commit.py holds exactly one generator call. Per-set content is tabulated in docs/ARCHITECTURE.md.

Generator

  • src/rosettify-plugins/plugins.json declares sets, folders, variants, and per-set bootstrap and hook lists. Validated at pre-flight; an unknown field aborts before any write.
  • spec.name is the bare IDE id; the set rides spec.destination. Directive tokens namespaced target-/ide-/set-/profile-.
  • Preserved config lives in 5 template-<ide> folders, shared by every set. Profile descriptors carry { modelOverrides }; suffixes live on the set variant.
  • --config selects a catalog; --domain filters sets, naming the folder it cannot resolve and exiting non-zero before any write.
  • A missing hook-bundle directory is a hard error, and bundles a set does not declare are swept.
  • An org folder is a domain set — there is no overlay layering. The 8 IDE guides live in the harness skill, verbatim-protected. Split sets are lightweight-only, so their union equals rosetta-light, not rosetta.

Hook configuration

Each emitted hooks.json comes from its own literal hooks.json.tmpl, seven in all, the file's path in the preserved tree being its identity. Copilot and Cursor each carry two — a plugin form and a standalone form — so the nine emitted documents keep their distinct addressing: the plugin form probes the install location, the standalone form uses repository-relative paths and an empty session-start array.

Only two values are parameterised: the output folder name and the probe guard. Rendering is Handlebars strict: true, followed by JSON.parse validation that hard-fails naming the target, covering both the template text and the raw bootstrap injection spliced into it.

The Copilot plugin-root probe guards on .github/plugin/plugin.json, which every Copilot plugin carries regardless of which set built it.

Bootstrap reaches Copilot and Cursor through their auto-loaded rules, not through hooks; their templates register no session-start payload. tests/hook-schema/ asserts the per-IDE document envelope and entry shapes at unit level, including the registrations that stay off, so enabling one later is a template edit against a known-good shape.

Verification

Four gates in plans/issue-315-plugin-sets/verify/, all green against a snapshot of the output taken before anything moved:

  • ac_structure.sh, ac_equivalence.py, ac_crossrefs.py — the 12 acceptance criteria.
  • ac_hooks_content.py (NFR-0012) — asserts the relationships between the hooks.json documents a target emits: path set, required identity, required distinctness, form markers, JSON validity, plus a content digest per document. Relationships rather than frozen bytes, so it stays valid as the bootstrap payload moves with the instruction source. Green at both --deterministic-hooks postures, and validated in both directions — passing on the pre-change snapshot, failing when a document's form is altered.

Tests: 940 / 60 files (rosettify-plugins), 1441 (hooks), 432 (curiocity). Requirements under docs/requirements/plugin-generator/ match the code; only the backlogged order-token pair is ToBeModified.

Before merge

scripts/pre_commit.py calls npx -y rosettify-plugins@latest. The published generator predates plugins.json and would regenerate plugins/ with the old build, destroying the 49 folders. Publish the generator first, then rely on that call.

Splits `instructions/r3/core/` into five sibling domain sets — core,
advanced, qe, search, modernization — and makes the generator build every
set x IDE combination from a declarative catalog in ONE invocation.

Output goes from 14 folders to 49: rosetta-<ide> (the full combo, what
core-<ide> used to be), rosetta-<ide>-light, and the five split sets.
`scripts/pre_commit.py` now holds exactly one generator call.

Generator
- New `src/rosettify-plugins/plugins.json` declares sets, folders,
  variants, per-set bootstrap and hook lists, and per-release availability.
  Validated at pre-flight; an unknown field aborts before any write.
- `spec.name` becomes the bare IDE id; the set rides `spec.destination`.
  Directive tokens namespaced: target-, ide-, set-, profile-.
- Hook registration moves from template literals into `HOOK_LAYOUTS` data;
  hooks.json is assembled via JSON.stringify, so malformed output is
  structurally impossible. Handlebars runs strict.
- Preserved config folders collapse from 5 core-<ide> to 5 template-<ide>.
- Profile descriptors shrink to `{ modelOverrides }`; suffixes move to the
  set variant. `--config` added; `--domain` is now a set filter.
- Silent-failure paths closed: a missing bundle dir is a hard error, and
  undeclared bundles are swept.

Content
- configure/ retired; the 8 IDE guides live in the harness skill, now
  verbatim-protected (they were being reference-rewritten while the
  original was not).
- self-help-flow.md deleted; its 13 citations repointed to help-flow.
- Cross-set command advertisements marked with their required plugin.

Org-overlay layering is retired — an org folder is now simply a domain set.

Fixes an Antigravity bug: templates/ -> rules/ rewriting corrupted
skill-relative paths and prose, so the security skill pointed at files that
did not exist.

All 12 acceptance criteria verified, including content-equivalence of
rosetta-<ide> against the pre-change output and union-equality of the split
sets with rosetta-<ide>-light.

Tests: rosettify-plugins 927, hooks 1441, curiocity 432.

Note: pre_commit.py calls rosettify-plugins@latest, which cannot know about
plugins.json until the generator is published. Sequence the publish before
relying on that call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
isolomatov-gd and others added 28 commits September 2, 2026 12:38
…t unit globs (#315)

Standalone plugins have never told the agent where they were extracted, so
every relative unit path in them resolved to nothing.

Root cause: the generator had one notion of "root" — plugin-root-relative,
in pluginRewriteReferences — applied to two distributions that do not share
one. A marketplace plugin is installed, so the install dir IS the root. A
standalone is extracted into a user repo, so the root is the repo and the
.cursor/.github prefix must survive. An anchor-based injection existed to
paper over that for one file, and never fired: it scanned for a
`# PREP STEP 1:` marker that lives in the test fixture but not in the real
rule, and skipped silently when absent.

Replaced with pluginEmitDistributionRoot — a factory composed only into the
two standalone specs, appending the declaration at a deterministic position
with no anchor and no string matching. Deliberately not a PluginSpec field:
a field five of seven specs leave unset, read by a processor that no-ops for
most, is identity branching wearing a data costume (FR-ARCH-0005).

Mass path rewriting was rejected: `agents/` names both plugin content that
moves under the root and target-repo files (agents/IMPLEMENTATION.md,
agents/*-state.md) that stay at the repo root, so a folder-level pair would
corrupt them — the FR-ARCH-0049 hazard. One declared root, and the agent
disambiguates by context as it already does.

Also corrects six documented unit globs that named files which do not exist:
cursor RULE (.mdc), copilot AGENT/SUBAGENT (.agent.md), copilot-standalone
WORKFLOW/COMMAND (.prompt.md), and all four codex globs, now uniformly
root-relative since codex alone spans two roots (.codex/agents/*.toml for
subagents, .agents/ for everything else) per its own configure guide.
A mutation-tested e2e sweep now asserts all 28 globs resolve to real files.

Requirements: bootstrap-delivery ownership consolidated into FR-VAR-0070
(the template-placeholder rule it stated was falsified when every template
became {{{hooks_json}}}); six duplicate contracts reconciled; dependency
direction corrected to match the corpus convention; DATA-CFG-0008 authored
for the hook-layout table the code already cited in six places.

Gates: structure, equivalence against the pre-change golden, and
cross-references all pass. Tests 939 / 1441 / 432.

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

A second review found the previous pass corrected <statement> text and left
the superseded rule standing in titles, rationales, criteria and
implementation notes. Three of its six blocking findings lived entirely
outside <statement>, including DATA-CFG-0002 — an Approved unit in this
delta — still asserting "delivery is a property of the preserved
templates/rules" and citing as its authority the unit that now refutes it.

Swept by phrase rather than by unit. One occurrence is legitimate and
survives: preventing DOUBLE-delivery genuinely is a template/rule concern.
The other five are corrected.

Also fixed:
- FR-GEN-0010 / FR-SET-0070 duplicated two shall clauses verbatim — a
  duplicate the previous pass created while claiming its citation check
  clean, because that check covered statements only.
- FR-ARCH-0051 contradicted itself twice (a leftover unqualified hard-error
  from the pluginInjectSections version, and configured-vs-derived workflow
  folder) and claimed the emission is "never located by matching an anchor
  string" when the code uses lastIndexOf on the block-end tag. Reworded to
  the guarantee that actually holds: the insertion point always exists, so
  the emission cannot be silently skipped.
- FR-SET-0030 delegated "every variant is built" then re-claimed it.
- FR-SET-0070's "sole determinant of the set's hook footprint" narrowed: the
  set decides which modules it requests, the target layout decides what is
  bound and whether a bootstrap block is emitted. Footprint is not uniform
  across a set's targets.
- FR-PROF-0011 made the normative owner of the profiled model-drop rule,
  which three FR-COPY units had been restating.
- Revision narration stripped from implementation notes; CHANGES.md owns it.

A mechanical clause-overlap scan over all 438 citation edges — covering
titles, rationales and notes, not just statements — now runs as the check.
It went from 12 pairs above 0.68 to 7, each adjudicated legitimate, and it
caught two duplicates neither review had listed. Two inspection passes
missed what it finds in seconds; it belongs in the gate.

158 units, 290 edges, zero cycles, zero dangling, no DATA->FR edges.
Gates and suites unchanged: 939 / 1441 / 432.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: isolomatov-gd <isolomatov@griddynamics.com>
Signed-off-by: isolomatov-gd <isolomatov@griddynamics.com>
NFR-0001's parity gate compares output paths only, and ac_equivalence.py
blanket-permits every content change to a file named hooks.json (permitted
difference #2). Between them, a document that changed shape while keeping its
path passed every gate in PR #340: <set>-copilot/hooks/hooks.json went from the
60-byte standalone form to a 24443-byte copy of the plugin form unnoticed.

The gate asserts the relationships between the documents rather than hashing
them alone: the exact path set per target, which documents must be
byte-identical (codex mirror pair, copilot root <-> .github/plugin), which must
differ (copilot plugin form vs standalone-form staging; cursor's two forms at
posture=true), that each carries its own form's markers, that every document
parses, and a per-document hash so future content changes are visible instead
of silently permitted.

Validated in both directions: ALL PASS against the pre-#315 golden tree
(28 assertions), 12 failures against the current tree, each naming the
collapse. Cursor's twins are correctly not flagged at posture=false, where
both legitimately reduce to {"version":1,"hooks":{}}.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
D23 hook modules are all-or-nothing per set: plugins.json holds exactly two
configurations, so the "commas between optional siblings" problem that
justified collapsing the templates does not arise.
D24 the two work tracks run on disjoint filesets.
D25 the 7 literal per-IDE templates are kept and HOOK_LAYOUTS is retired; the
table's only real justification (JSON validity) is served better by a
post-render JSON.parse validation, which also guards a malformed raw injection.

B2 resolved by verify/ac_hooks_content.py. OQ-1 closed as not blocking: the
verified Copilot spec is silent on marketplace-plugin mode, that gap is
pre-existing, and the pre-#315 design already committed to shipping the
standalone form there regardless. The old "suppress the third file" sidestep
is wrong -- FR-VAR-0030.AC4 requires it, and golden proves it was 60 B.

Two out-of-scope findings recorded in F7: the never-verified Copilot
marketplace probe loops, and the Copilot bootstrap hook never firing in VS Code
(camelCase-only registration, identical before and after #315).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…plit empty-selection exit code

FR-CLI-0030: an unknown --domain token used to fall through to the generic
"No plugin sets to build" message with the missing instruction folder never
named. generate() now checks each token against
<instructionsSource>/<release>/<token> before selection and reports the
resolved path(s) it looked for, aborting with exit 1.

FR-CLI-0031: an empty selection used to exit 1 unconditionally, conflating
two different causes. A domain filter that matches no declared set is now a
legitimate outcome (exit 0, nothing written); an unconfigured release with
no --domain stays exit 1 with its existing wording, since that is
misconfiguration, not a filter outcome. Ship together: 0031 alone would
make `--domain typo` exit 0 silently.

New exported parseDomainTokens (spec/plugin-sets.ts) is the single place
that splits/trims/filters the raw --domain value, shared by selectSets and
generate()'s new folder check.

Also re-tags generate.test.ts's mislabeled "missing instruction directory"
test: it exercises buildVfs throwing, not the domain-filter rule.

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

RULES_EXCLUDES (src/rosettify-plugins/src/spec/targets.ts) ships three
entries — rules/bootstrap.md, rules/mcp-files-mode.md, rules/local-files-mode.md
— but the requirement statement named only two, so it flagged a real,
already-shipped exclusion (instructions/r3/core/rules/mcp-files-mode.md) as
missing. Updates the statement, AC1, and implementationNotes to match the
code and flips implementation to Implemented; status stays Draft
(unrelated approval gate).

Adds a unit test asserting all three rules are excluded from generated
frames, plus a read-only check of the already-shipped plugins/ tree
confirming none of the three leaked into any output folder.

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

NFR-0001 compares output file paths only, deliberately, and the equivalence
check listed hooks.json among its permitted-difference classes. Between them a
hook-configuration document that changed shape while keeping its path was
invisible to every gate -- how <set>-copilot/hooks/hooks.json went from the
60-byte standalone form to a copy of the 24443-byte plugin form with all gates
green.

NFR-0012 is the content-shape complement, covering the case a path oracle
cannot see: a target emitting two hook-configuration documents at fixed,
different paths that differ only in content. It asserts the relationships
between them -- declared path set, required identity (codex mirror pair,
copilot alternate-name copy), required distinctness (copilot's two forms;
cursor's two forms where the deterministic-hooks value makes them
distinguishable), form markers, JSON validity -- plus a recorded digest per
document. Relationships rather than frozen bytes, so the gate stays valid as
the bootstrap payload legitimately moves with the instruction source.

NFR-0001's notes and the INDEX summary now name where content is covered rather
than only disclaiming it; TRACE maps NFR-0012 to G7 alongside NFR-0001;
CHANGES records RECONCILIATION-14.

Status Draft: the capability was owner-approved (follow.md OWNER'S ASKS item
12), this unit's text is new and awaits review.

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

initLogger (src/logging.ts) already sends all progress to stderr via pino's
destination:2 transport; the requirement wanted per-plugin counts on
stdout, which would corrupt `--dry-run > file` redirection since
buildPipeline threads `out: Writable = process.stdout` into pluginCopy and
pluginWrite (FR-ARCH-0045). Measured: a normal `--domain qe` run produced
0 bytes on stdout, 2,079 bytes of progress on stderr; `--dry-run --domain
qe` produced 2,707,684 bytes of payload on stdout, 131,276 bytes of
progress on stderr. The code was right; the requirement was wrong about
where progress belongs.

Rewords the statement/ACs to require progress on stderr and an empty
stdout on a normal run, with `--dry-run` payload on stdout as the explicit
exception, and flips to Implemented. Drops the per-plugin
copied/renamed/generated breakdown claim (the logger reports a single
`frames` total) to backlog rather than blocking this unit.

Adds tests/unit/cli-progress.test.ts, spawning the CLI as a subprocess
(cli.ts runs main() at import) to verify stdout is empty on a normal run
and carries the dry-run payload otherwise.

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

--help documented the plugin-set mechanism but never listed the descriptor
field names, so a user had to read plugins.json or the source to learn
what a set/variant/manifest carries. Exports SET_FIELDS, VARIANT_FIELDS
and MANIFEST_FIELDS from spec/plugin-sets.ts and interpolates them into
the "Plugin sets" help block in cli.ts, rather than hand-typing a second
list that would drift from the loader's own allow-list — the same trap
FR-SET-0050 closes for the manifest description.

Also drops implementationNotes' stale claim that AC4 names the field `id`
instead of `name`: AC4 already reads `name`, and its nine names are
exactly SET_FIELDS.

Adds tests/unit/cli-help.test.ts, spawning `--help` as a subprocess
(cli.ts runs main() at import) to assert every SET_FIELDS/VARIANT_FIELDS/
MANIFEST_FIELDS name appears in the output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, never derive it (FR-SET-0050)

`readSets` already checked that a `requires` entry names a declared set
and does not name itself; it never checked that the requiring set's own
manifest.description actually mentions the required set, so a `requires`
edit that forgot to update the authored "Requires X and Y" prose shipped
silently wrong manifests.

Adds the check inside the existing per-`requires` loop: the BASE
description (never the variant-suffixed composite) must mention the
required set's name, matched case-insensitively with a lookaround
(`(?<![a-z0-9-])name(?![a-z0-9-])`) rather than `\b`, which treats `-` as
a word boundary and would let a set named e.g. `read-once` match inside
"read-once-shared".

Validating rather than deriving keeps the authored sentence — which reads
better than anything generated — as the single source of the fact, and
catches a forgotten description at the exact moment the `requires` edit
is made, rather than composing a second, worse sentence from the same
data. Verified the shipped plugins.json still loads under the new check.

Tests: a fixture requires:['core'] with a description omitting "core"
throws PluginCatalogError naming both the set and the entry; a
hyphenated-name substring case proving \b would have been wrong; the
shipped catalog still loads; and an output-shape assertion (the class of
bug the prior audit missed) that a built qe-* manifest description
actually contains "Core" and "Workflows".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Architecture doc: all four open questions decided. OQ-1 probe guard fixed as
its own commit; OQ-2 destination plumbed unconditionally; OQ-3 golden retained
through this PR; OQ-4 -- F1/F2/F3 are BY DESIGN, not defects.

Copilot and Cursor deliver bootstrap through rules, deliberately (FR-VAR-0070,
Approved). Enabling those hook registrations would double-deliver instructions
the auto-loaded rules already carry. §6 amended so the three rows read as
records of intentional design rather than a backlog. docs/hooks-verify.md does
not gate generator work -- it governs how the per-IDE spec documents were
authored and sealed. What is required instead: unit-level schema tests, in
their own location, that neither run the generator nor write a hooks.json, and
that cover the switched-off shapes so a future re-enable is safe. Copilot is
one plugin serving every Copilot variant from a single document.

MEMORY.md, two appended rules (append-only, 6 insertions, 0 deletions):
shell cwd persists so relative-path existence checks lie -- this overwrote
MEMORY.md itself, recovered from HEAD; and a finding contradicting an Approved
requirement is probably a misreading, never offer an option whose own
description states why it is harmful.

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

Restores the 7 per-IDE hooks.json.tmpl files (and their 7 test fixtures)
verbatim from 492b6a7~1, replacing the single {{{hooks_json}}} placeholder
each carried under the #315 HOOK_LAYOUTS refactor. A document's shape now
lives in the file that produces it again — file path is identity.

The Copilot plugin-form template (.github/plugin/hooks.json.tmpl) additionally
substitutes {{destination}} for the hardcoded core-copilot literal at all 14
probe sites (7 bash + 7 powershell), so rosetta-copilot and
rosetta-copilot-light probe their own install folder instead of core-copilot's.
generate.ts does not yet plumb `destination` (step 2) — this commit is a pure
template restore per plans/issue-315-plugin-sets/hooks-architecture.md §3
step 1.

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

FR-HOOK-0022 -- the audit was wrong, no code change needed. sweepUndeclaredBundles
is called unconditionally by pluginSyncBundles, outside the deterministic-hooks
copy branch, so AC2 holds on both branches; the four sweep tests in
plugin-sync-bundles.test.ts already cover it. implementationNotes now cite the
helper and the named tests, and record the nuance that pluginCleanup wipes the
destination folder first, which makes the sweep defence-in-depth for direct
library callers rather than a CLI-pipeline property. Flipped to Implemented;
status stays Approved because no normative text changed.

FR-SET-0050 -- its validation landed in 6675e99. The statement's duty moves from
composing the manifest description to enforcing an authored one: readSets now
refuses a catalog whose manifest.description omits a set its requires list names,
matching on a word boundary so a hyphenated set name cannot match inside a longer
one. Added AC5 for the abort, added the rationale line for why enforcing an
invariant beats generating the artifact that satisfies it, and pointed
implementationNotes at readSets plus the two test files.

Status moved Approved -> Draft on FR-SET-0050 only: refusing to load a catalog is
a new obligation and awaits re-approval. AC2's observable outcome is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
generate.ts:206 now supplies `destination: spec.destination` to every
spec's templateContext alongside release/deterministic_hooks/bootstrap_hooks,
per plans/issue-315-plugin-sets/hooks-architecture.md §1.3/§3 step 2. Plumbed
unconditionally per OQ-2: strict rendering only throws on a MISSING variable,
never an extra one, so a uniform context has no cost and avoids a
per-target-only field. `hooks_json` is dropped from baseTemplateContext (no
template references it after step 1).

DEVIATION from §1.4 (flagging per task instructions): the design assumed
`{{destination}}` at the 14 Copilot powershell/bash probe sites would
substitute byte-identical because "no escapable character can occur." That
holds for the 7 bash occurrences (preceded by `/`), but the 7 powershell
occurrences are preceded by `\\` (JSON-escaped single backslash) directly
against the mustache open-brace, and Handlebars treats `\\{{expr}}` as
"literal backslash + evaluated expression," CONSUMING one of the two
backslashes (verified directly against Handlebars 4.7: `\\{{x}}` -> one `\`
+ evaluated x, not two). This silently dropped a path separator
(`...\plugins\core-copilot` instead of `...\plugins\\core-copilot`,
i.e. `\plugins\core-copilot` as the decoded literal instead of
`\plugins\core-copilot` -- concretely, `ac_hooks_content.py --posture true`
caught it as "not valid JSON: Invalid \escape"). Fixed by using a third
backslash (`\\\{{destination}}`) on the 7 powershell lines only, which
Handlebars renders as one literal backslash + the evaluated value -- the
same net two-backslash output the original literal had. Verified: 0
structural diffs across all 18 documents (both postures) against
492b6a7~1's original templates, and destination values do not cross-leak
between core-copilot/rosetta-copilot/rosetta-copilot-light.

Verified: `ac_hooks_content.py` ALL PASS at --posture true and --posture
false (scratch builds); full unit suite 952/952 unaffected.

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

The table keyed one layout per bare IDE target identity -- seven entries, which
its own AC1 fixed as the whole inventory -- while the generator emits NINE
distinct hook documents: Copilot and Cursor each own both a plugin form and a
standalone form. A structure that cannot address the document it describes can
only produce one document per target, so the two forms collapsed into
byte-identical copies. Measured: the Copilot standalone form went from 60 bytes
to 24443, an exact copy of the plugin form; the Cursor root document took
plugin-form addressing where standalone-form addressing was required.

Retired under the FR-HOOK-0003 precedent -- record kept, status Deprecated,
reason in implementationNotes -- because FR-VAR-0070, FR-HOOK-0005, FR-GEN-0010,
FR-GEN-0011 and FR-SET-0070 all referenced it. Those references are redirected in
the same pass to the per-IDE templates and FR-GEN-0011.

implementation is ToBeRemoved rather than Removed: hook-layouts.ts,
PluginSpec.hookLayout and plugin-assemble-hooks-json.ts are still present at the
time this record was written. It flips to Removed when that deletion lands.

FR-VAR-0070 keeps Approved -- the conjunction it states is unchanged, only the
second conjunct's home moves from a table entry to a template placeholder. Its
new notes record the three by-design consequences of that conjunction (Copilot's
camelCase sessionStart and preCompact registration, Cursor's absent placeholder)
so they are not re-raised as defects: each is the deliberate no-double-delivery
posture, with the same bootstrap bodies arriving through auto-loaded rules.

Also swept pluginAssembleHooksJson out of the DATA-CFG-0002 processor list and
the FR-ARCH-0039 cross-file processor list, and dropped hookLayout from the
PluginSpec field list.

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

Deletes plugin-assemble-hooks-json.ts and its test module, and removes
pluginAssembleHooksJson from all 7 target pipelines (spec/targets.ts) and
the CLI processor catalog (cli.ts), per hooks-architecture.md §1.8/§3 step 3.

emitsHooksJson moves into plugin-copy.ts and is resignatured to
`(spec) => spec.hookModules.length > 0 || spec.bootstrap` — it no longer
takes a layout, because document SHAPE lives entirely in the 7 templates now
(steps 1-2). Its only remaining job is deciding WHETHER a hooks.json.tmpl
frame is created at all for a hookless set (workflows, qe, search,
modernization all declare bootstrap:false with an empty hook list).

The frame-drop moves from a post-hoc filter (the deleted processor dropped
frames pluginCopy had just created) to declining to create the frame in the
first place: `collectTmplFrames` now takes an `emitsHooks` flag and skips any
`hooks.json.tmpl` basename when it's false, and the `standaloneTemplates`
loop applies the identical skip. Raw disk copy is unaffected --
copyDirRecursive already excludes .tmpl for every target.

Test changes: two plugin-copy.test.ts cases that used `hooks.json.tmpl` as a
stand-in filename for GENERIC .tmpl-frame-registration behavior (unrelated
to hook emission) are renamed to `readme.md.tmpl` so they aren't coupled to
the new skip. Two cases that genuinely exercise the standaloneTemplates
hooks.json.tmpl path are given `bootstrap: true` so their spec actually
emits hooks (previously frame creation was unconditional; now it depends on
emitsHooksJson). plugin-assemble-hooks-json.test.ts (69 tests: 56 from an
it.each over 7 layouts x 2x2x2 postures/bootstrap/modules, 13 direct) is
deleted with its module -- all subtracted tests belong to the deleted unit.

Verified: tsc clean; scratch builds at both postures exit 0 with no errors;
`workflows-*`, `qe-*`, `search-*`, `modernization-*` outputs contain no
hooks.json and no hooks/ folder; ac_hooks_content.py ALL PASS at both
postures; unit suite 882/882 (952 - 70 removed with the deleted module).

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

FR-GEN-0011 is rewritten from assemble-then-serialize to render-then-validate.
Its old text mandated the defect: "exactly one raw-injection placeholder and no
control flow ... no literal hook entry" is what collapsed the seven templates to
a shared one-liner, after which nothing distinguished Copilot's marketplace form
from its standalone form. The new statement puts each emitted document's complete
structure back in its own template file, whose path in the preserved tree is its
identity, and forbids giving a template its content by matching its filename.

The JSON-validity property the old text was adopted for now comes from parsing
what was rendered. That is strictly stronger, not merely equivalent: it also
catches a malformed raw bootstrap injection, which serializing a built object
cannot see, because that fragment is spliced in as text after the object would
have been serialized. Hard error, unlike FR-GEN-0010's warn-and-continue on
render failures -- a document that rendered but will not parse silently disables
every hook in that plugin.

Also in this pass:
- FR-GEN-0010 loses the assembled-value clause; its render context becomes
  release variables, the bootstrap payload and the spec's output folder name.
- FR-ARCH-0048 gains the output folder name and the post-render parse obligation.
- FR-VAR-0071's rationale asserted the defect as design ("the two template frames
  are now byte-identical single-placeholder files"). Rewritten: the per-form
  difference lives in the template text, and a new criterion asserts it.
- FR-VAR-0030 AC4 is CORRECT and currently failing -- left untouched, with the
  notes saying so, so it is not closed by editing the criterion. FR-VAR-0030 and
  FR-VAR-0031 now name the mechanism that exists (a declarative post-render
  mirror pair) rather than the SpecEntry/fileRename one they described.
- FR-HOOK-0007's Copilot probe guard moves off commands/coding-flow.md, which the
  set split made absent from every core-based Copilot plugin, to
  .github/plugin/plugin.json -- the marketplace manifest every Copilot plugin of
  every set carries, and the file whose presence is exactly the proposition the
  probe tests. New criterion generalizes the rule.
- FR-SET-0070 AC1/AC2 named per-set hook subsets no declared set has ever
  carried; restated over the two configurations plugins.json actually declares.
  AC3 kept as the named instance of the rule AC2 states generally.

Status Approved -> Draft on FR-GEN-0011, FR-GEN-0010, FR-VAR-0071, FR-HOOK-0007
and FR-SET-0070: each changed an obligation or a criterion and awaits
re-approval. FR-VAR-0030/0031 were already Draft.

implementation stays ToBeModified where the code has not landed yet
(FR-GEN-0011, FR-VAR-0071, FR-VAR-0030, FR-HOOK-0007); FR-VAR-0031 moves to
Implemented because the mirror mechanism it now describes exists today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…RGET_HOOK_MODULES (step 4/9)

spec/targets.ts: modulesForTarget no longer takes a HookLayout — it takes a
plain module-name list, narrowed and support-expanded exactly as before.
The list comes from a new TARGET_HOOK_MODULES constant keyed by IDE FAMILY
(hooks-architecture.md §1.9): 7 modules for claude/codex/copilot/cursor, 2
(dangerous-actions, read-once) for antigravity. This holds no event names,
matchers, entry shapes or envelopes — only "which .js files this family's
templates invoke by name" — so it is not HOOK_LAYOUTS coming back in
disguise; it is a bundle-shipping fact consumed only by pluginSyncBundles.

Verified by raw-text scan (not rendered — at deterministic_hooks=false the
templates name no modules at all, yet bundles must still ship): every
restored template under plugins/template-<family>/**/hooks.json.tmpl names
exactly TARGET_HOOK_MODULES[family]'s tokens via a `[a-z][a-z0-9-]*\.js`
scan across all 7 files. This same scan becomes the T5 consistency test in
the schema-test area (step 10).

Verified: tsc clean; scratch build ships core-antigravity/hooks/ with
exactly 4 .js (dangerous-actions, read-once, read-once-reset,
read-once-shared — the latter two via plugins.json's hookSupportModules
expansion, never named literally in any template) and core-claude/hooks/
with exactly 8; ac_hooks_content.py ALL PASS both postures; unit suite
882/882 unaffected.

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

Swept the phrases the retired layout table left behind across the units that
reference it only in passing -- titles, rationales, notes and implementationNotes
all read as normative in this corpus, so each needed editing rather than only the
statements.

- GLOSSARY PluginProcessor term and NFR-0007's two-tier enumeration drop
  pluginAssembleHooksJson.
- INT-IDE-0002, FR-VAR-0020, FR-VAR-0082 and FR-VAR-0083 restate "the layout
  declares a null bootstrap slot" as "the template carries no bootstrap
  placeholder", which is the same observable fact named at its real source.
  FR-VAR-0083's advisory-hook exclusion is restated against what the Antigravity
  template invokes; the behaviour asserted is unchanged and still holds.
- ASSUMPTIONS OQ-4 claimed "Hook JSON is covered by this general structural rule
  like any other output." It is not: a paths-only oracle cannot see a document
  whose shape changed at a fixed path, which is exactly what the two hook forms
  are. Corrected and pointed at NFR-0012.
- STRUCTURES.md needed almost nothing -- it was never updated by the refactor and
  already describes this architecture, which is independent corroboration from
  the committed structure spec rather than from the git baseline. Two edits: the
  Cursor root template is also rendered into the main Cursor tree, and the
  Copilot root hooks.json is a declarative mirror pair, not a SpecEntry copy.
- docs/ARCHITECTURE.md called Copilot's hooks/hooks.json.tmpl the plugin form,
  contradicting its own hook-forms table nine paragraphs later. Corrected.
- TRACE gains rows for FR-VAR-0071 (G11) and FR-VAR-0031 (G12), which owned no
  row despite being referenced; FR-SET-0070's row gains its .AC7; the coverage
  assertion moves to 83 units / 117 links and records why the retired
  DATA-CFG-0008 carries no row.
- CHANGES records RECONCILIATION-15 (the architecture change) and
  RECONCILIATION-16 (the two remediation units closed in 9d6c340).

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

The concurrent implementation advanced past migration step 4 while this sweep was
being written, so the ToBeModified notes needed re-grounding against what is
actually in the tree rather than what was there an hour ago.

Verified landed: the seven literal hooks.json.tmpl files carry their own
document's structure again (template-copilot/hooks/hooks.json.tmpl is back to a
literal "sessionStart": [] with .github/hooks/ addressing;
template-cursor/hooks.json.tmpl is back to .cursor/hooks/); {{destination}} at
exactly 14 sites in the Copilot plugin-form template, plumbed from
spec.destination into the render context in generate.ts;
plugin-assemble-hooks-json.ts deleted with the frame-drop folded into pluginCopy;
PluginSpec.hookLayout gone; HOOKS_PSEUDO_FOLDER relocated to spec/hooks.ts.

Verified outstanding: the post-render JSON.parse hard error, and deletion of the
residual hook-layouts.ts, which no longer has an importer.

Status changes that follow from the facts, not from the plan:
- FR-VAR-0071 -> Implemented. Its subject is the two template forms, and they now
  demonstrably differ in the template text; two files cannot collapse into one
  document.
- FR-GEN-0011 stays ToBeModified -- half of it (post-render validation) has not
  landed -- with the notes now separating landed from outstanding.
- FR-VAR-0030 stays ToBeModified deliberately, even though the template that
  caused its AC4 failure is fixed: its criteria assert properties of generated
  OUTPUT, and that cannot be claimed until the tree is rebuilt and
  ac_hooks_content.py passes over it. Noted rather than quietly flipped.
- DATA-CFG-0008 stays ToBeRemoved, with the note narrowed to the one file left.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eld (steps 5+6/9)

Deletes spec/hook-layouts.ts entirely: HOOK_LAYOUTS, HookLayout, HookBinding,
BootstrapBinding, COPILOT_PLUGIN_PATH, copilotProbeBash/Powershell, and the
entry/envelope helpers (command, cursorEntry, copilotPluginEntry,
plainHooks, versionedHooks). Every document shape they held now lives in the
7 restored literal templates (steps 1-2); nothing in src/ references
HOOK_LAYOUTS except two now-historical comments in the new file's own header
explaining what was retired.

Rewrites the FR-ARCH-0005 rationale comment in plugin-emit-distribution-root.ts
(§3 step 5's explicit "also") to drop its "(Contrast HOOK_LAYOUTS, correctly a
table...)" aside, now a dangling reference to a deleted symbol; the point it
was illustrating (compose behaviour only where it applies) stands on its own.

DEVIATION from the step table (flagging per task instructions): step 5 and
step 6 (drop PluginSpec.hookLayout, types.ts:173) are combined into this one
commit rather than landing separately. They are not independently
verifiable in isolation: types.ts's `hookLayout: HookLayout | null` field
imports HookLayout from hook-layouts.ts, so deleting that module while the
field declaration (and targets.ts's `hookLayout: layout` assignment) still
reference the type cannot typecheck. Landing step 5 alone would require
either leaving a dangling type import (breaks tsc immediately) or
introducing a throwaway placeholder type -- both worse than a single
combined, clean commit. Rollback is unaffected: reverting this one commit
restores both the module and the field together, which is exactly how they
were coupled before it landed.

HOOKS_PSEUDO_FOLDER is relocated to a new spec/hooks.ts (not spec/targets.ts,
though the plan floated either) rather than merged into spec/targets.ts,
because plugin-copy.ts's buildManifestOverlay also needs it and
spec/targets.ts already imports plugin-copy.ts (to compose the per-target
processor pipeline) -- importing HOOKS_PSEUDO_FOLDER back from targets.ts
into plugin-copy.ts would recreate exactly the plugin-copy<->targets.ts
import cycle the original hook-layouts.ts placement comment warned about.
A standalone one-export module breaks the tie without reviving the cycle.

Verified: tsc clean; `grep -r HOOK_LAYOUTS src/` matches only two comments
inside the new spec/hooks.ts header explaining the historical name (no
functional reference); scratch builds exit 0 both postures with no errors;
ac_hooks_content.py ALL PASS both postures; unit suite 882/882 unaffected
(plugin-copy.test.ts's 19 now-meaningless `hookLayout: null,` literals
removed -- PluginSpec no longer declares the field, so TypeScript's excess-
property check on the `Partial<PluginSpec>` object literals would otherwise
fail the build; no assertions changed).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
plugin-render-templates.ts: immediately after a `.json`-targeted template
renders successfully, JSON.parse the rendered text. On failure, record a
HARD error naming the target, the output file and the parser message, and
do not emit the rendered sibling — distinct from the warn-and-continue
handling FR-GEN-0010 already applies to render failures (missing template,
unplumbed variable), per hooks-architecture.md §1.6/§3 step 7.

This is strictly stronger than the JSON-validity guarantee the deleted
buildHooksDocument+JSON.stringify pipeline provided: it also catches a
malformed raw `{{{bootstrap_hooks}}}` injection, which serializing a built
object can never detect (parsePayloadEntries only guarded the bootstrap
fragment, and only while that assembler ran — both gone since step 3).

Mechanism: plain `JSON.parse` inline in the render path, NOT the shared
schema validator introduced for the hook-schema test area (step 10). The
owner's design permits, but does not require, sharing a validator between
the production check and the tests; wiring the per-IDE shape validator into
this path would mean it must accept every document the 7 templates render
at both postures or it would hard-fail real builds, which is a much larger
compatibility surface than "is this valid JSON." Kept separate deliberately.

Added 4 unit tests (plugin-render-templates.test.ts): a stray-comma .json
template hard-errors with no emitted sibling; a malformed
{{{bootstrap_hooks}}} injection hard-errors the same way; a non-.json target
(e.g. a rendered .md) is not JSON-validated at all; a valid .json render is
unaffected. 18/18 in that file (14 pre-existing + 4 new).

Verified: tsc clean; scratch builds at both postures exit 0 with no render
errors on any of the 7 real templates; ac_hooks_content.py ALL PASS both
postures; unit suite 886/886 (882 + 4 new).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…on (step 8/9)

OWN COMMIT, separately revertible, per the task's explicit instruction and
hooks-architecture.md OQ-1's recommendation.

spec/bootstrap-manifest.ts: COPILOT_PLUGIN_ROOT_BASH / _POWERSHELL guarded on
`commands/coding-flow.md` to decide whether to emit the "Rosetta Plugin
Path" additionalContext at all. That file moved to the `workflows` set under
#315's set split and is absent from every `core`-based Copilot plugin,
making the guard a permanent no-op there (it still worked for `rosetta`,
which ships both `core` and `workflows`). Fixed per hooks-architecture.md
§1.5: guard on `.github/plugin/plugin.json` instead — Copilot's marketplace
manifest, NOT at the plugin root (verified: `<set>-copilot/` contains
`.github/ commands/ hooks/ hooks.json rules/ skills/`, no root plugin.json).
It is emitted unconditionally by pluginCopy's manifest overlay for every
Copilot spec regardless of set contents, content-independent, and is
literally the file this probe lives alongside in the output tree — the
strongest available guard, per §1.5's rejected-alternatives analysis
(bare `-d` directory test passes on a stale/partial dir; guarding on
`rules/bootstrap-alwayson.md` couples an install-location probe to
instruction content and would silently re-break under a future bootstrap
set lacking that rule).

This is a FIX, not a restore: 492b6a7~1 carried the same
`commands/coding-flow.md` literal, correct then because only one set
existed. It is a defect the #315 set split created, and is inside #315's
blast radius, which is why the task places it in scope as its own commit
rather than deferring it (§7 OQ-1).

Also (§3 step 8, same commit): spec/plugin-sets.ts's variant reader now
pattern-validates destinationSuffix against
`/^(?:-[a-z0-9]+(?:-[a-z0-9]+)*)?$/` — admits "" and "-light" (plugins.json's
only two current values), rejects anything with an escapable-by-Handlebars
character. destinationSuffix feeds `spec.destination`, which is now a
template variable ({{destination}}, steps 1-2); it was previously read
unvalidated while the sibling set-name field already was (plugin-sets.ts's
existing check), a latent input-validation gap this closes.

Verified: tsc clean; scratch builds at both postures exit 0, plugins.json's
real "" and "-light" suffixes pass validation; core-copilot/.github/plugin/
plugin.json exists at the exact path the new probe guards on;
ac_hooks_content.py ALL PASS both postures; unit suite 886/886 unaffected
(no test asserted the old commands/coding-flow.md literal).

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

Three defects in the preceding commits, plus a status reconciliation.

1. Dependency cycles. hooks-architecture.md §5.2 proposed
   `FR-GEN-0011 depends: FR-GEN-0010, FR-ARCH-0048, FR-VAR-0071, FR-VAR-0031,
   NFR-0012`. Three of those five point at units that specialize or verify
   FR-GEN-0011 and therefore already depend on it, producing seven cycles through
   FR-GEN-0011 -- including the direct FR-ARCH-0048 <-> FR-GEN-0011 pair. The
   governing rule depends on nothing that implements it: FR-GEN-0011 now depends
   on FR-GEN-0010 alone, and FR-VAR-0071 gains the edge to FR-GEN-0011 in the
   direction that specialization runs. Verified zero cycles across every depends
   edge in the area.

2. FR-ARCH-0048 gained a post-render validation obligation and a criterion but
   kept status Approved. Flipped to Draft with a note, and added to
   RECONCILIATION-15's Status line, which listed it in neither column.

3. FR-HOOK-0007's notes read as though the guard literal had changed on
   2026-09-03. It has not -- bootstrap-manifest.ts still carries two
   commands/coding-flow.md occurrences. It is the CRITERION that changed;
   reworded, and the implementation node corrected to ToBeModified to match its
   own notes.

Implementation statuses re-grounded after the concurrent implementation landed
migration steps 5, 6 and 7: hook-layouts.ts, plugin-assemble-hooks-json.ts and
PluginSpec.hookLayout are deleted with no HOOK_LAYOUTS reference left in the
tree, and plugin-render-templates.ts JSON.parse's every rendered JSON document as
a hard error. DATA-CFG-0008 -> Removed; FR-GEN-0011 -> Implemented. FR-VAR-0030
stays ToBeModified on purpose: its criteria assert generated OUTPUT, and the tree
has not been rebuilt against ac_hooks_content.py.

Also corrected two overreaches in FR-VAR-0070's new notes: the marketplace plugin
ships rule/instruction files generally, not the .instructions.md documents the
standalone transform produces; and preCompact is a hook-registration consequence,
not a bootstrap-delivery one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The architecture's C1 refuted it with evidence: plugin-copy.ts reads standalone
templates from the shared preserved-template directory, never from the main
plugin's output, so the two builds are independent and merely render the same
source file. The document is an incidental emission of the preserved-tree
template walk, read by nothing in marketplace mode, retained because
FR-VAR-0030 mandates pre-existing shipped output -- not a staging source.

I wrote the wrong word when authoring this unit; the requirements sweep flagged
it and correctly did not overstep its brief to change it. Assertions unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
isolomatov-gd and others added 11 commits September 3, 2026 20:47
…osture (step 9/9)

The ONLY commit in this migration that writes plugins/, per
hooks-architecture.md §3 step 9: steps 1-8 verified only against scratch
builds, so the previously-committed 49-folder tree still carried the
Copilot/Cursor form-collapse regression this migration fixes.

Regenerated with `npm --prefix src/rosettify-plugins start -- --release r3
--deterministic-hooks false --source <repo-root> --output <scratch>`, the
shipped default posture (FR-CLI-0012: omitting --deterministic-hooks
defaults to false; docs/ARCHITECTURE.md:343 and scripts/pre_commit.py:86
confirm this is the posture `plugins/` ships at). Never ran
`npx rosettify-plugins@latest` (would run the published, pre-plugins.json
build and destroy the 49 output folders).

Procedure, to avoid a repeat of an earlier destructive overwrite in this
session: built to a scratch directory first; diffed its folder-name set
against `plugins/`'s existing 49 names (identical, confirmed by diff before
touching anything); replaced each of the 49 named folders in `plugins/`
with its scratch counterpart one at a time (rm -rf + cp -R by name, never a
wildcard or `git add -A`); confirmed the folder-name set was still exactly
the committed 49 afterward; staged only those 49 paths by name.

Result: 34 files changed under 25 of the 49 folders. All changes are either
(a) hooks.json content -- the Cursor and Copilot standalone forms now
correctly diverge from their plugin-form siblings (the regression) -- or
(b) unrelated instruction-source drift the regeneration incidentally picked
up (skills/backlog/SKILL.md's orchestration wording, present on the
instructions source already and untouched by this migration) since the
committed tree was last regenerated. No file outside these 49 folders was
touched; no folder was added or removed.

Verified: `ac_hooks_content.py` (no --tree argument, i.e. against this
committed tree) ALL PASS, 84/84 assertions, golden cross-check PASS; full
unit suite 886/886 (unaffected -- this step touches only generated output,
no source).

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

New tests/hook-schema/ — unit-test level, distinct from both the generator-
pipeline tests (tests/unit/plugin-processors/**) and the output-tree content
gate (plans/issue-315-plugin-sets/verify/ac_hooks_content.py, which this
does not duplicate). No file here runs the generator or writes a hooks.json
to disk: every check either hand-builds an in-memory document or renders one
real .tmpl file in memory with Handlebars (the same library
plugin-render-templates.ts uses) and asserts on the parsed result — never
writing it anywhere.

Mechanism (tests/hook-schema/validator.ts): a per-IDE shape validator table,
each function citing its source doc line range in a comment —
validateClaudeOrCodexDocument (claude-code.md:87-107, codex.md:86-106),
validateCopilotDocument + validateCopilotMergedContext (copilot.md:16,
:66-87, :301-315; FR-HOOK-0005/FR-HOOK.md:111-112), validateCursorDocument
(cursor.md:105-122), validateAntigravityDocument (antigravity.md:44,
:60-68), plus scanModuleTokens for the T5 raw-text module scan. This is a
test-only module, deliberately NOT wired into step 7's production
JSON.parse check (see that commit's rationale: the full per-IDE shape
validator is a much larger compatibility surface than "is this valid
JSON", and hard-failing real builds against it was not the owner's ask).

Coverage, per file:
- claude-codex.schema.test.ts (10 tests): shared {hooks:{PascalEvent:
  [{matcher,hooks:[{type,command}]}]}} shape, both postures, against the
  real restored templates.
- copilot.schema.test.ts (15 tests): the CURRENTLY-ENABLED mixed-casing
  shape (camelCase sessionStart/preCompact flat, PascalCase PreToolUse/
  PostToolUse grouped with bash+powershell entries) against the real
  template; the merged top-level+nested additionalContext requirement
  exercised directly against the production buildCopilotHookPayloadJson
  builder (not a hand-reimplementation); and the RESERVED-but-OFF shapes —
  PascalCase SessionStart and PascalCase PreCompact — hand-built and
  validated, so their shapes are already known correct if ever enabled.
  Encodes owner constraint 1: no template registers PascalCase
  SessionStart/PreCompact, and PreToolUse/PostToolUse are registered
  PascalCase-only (never also camelCase, which would double-fire the CLI).
- cursor.schema.test.ts (12 tests): the flat, no-hookSpecificOutput-wrapper
  shape against both real templates (plugin form and standalone form) at
  both postures; the RESERVED sessionStart shape, hand-built, since no
  Cursor template wires it today (owner constraint 1).
- antigravity.schema.test.ts (10 tests): the two-shapes-in-one-document
  registration (wrapped PreToolUse/PostToolUse vs flat PreInvocation/
  PostInvocation/Stop) against the real template at both postures, and the
  antigravity.md:44 rule that SessionStart is not a valid event for this
  IDE at all.
- module-consistency.schema.test.ts (7 tests): T5 — raw-text `<module>.js`
  token scan of every real template, set-equality against
  spec/targets.ts's TARGET_HOOK_MODULES (7 modules for claude/codex/
  copilot/cursor, 2 for antigravity), plus an explicit assertion that
  read-once-shared never appears literally in any template (it reaches
  every target only through plugins.json's hookSupportModules expansion).

Verified: ad hoc `tsc --noEmit` over src/**+tests/hook-schema/** together
(neither is covered by the project's tsconfig.json, which excludes
`tests/` — same as every pre-existing test file) is clean; full suite
940/940 (886 + 54 new), 60 files (55 + 5 new); ac_hooks_content.py (default
tree) still ALL PASS, unaffected by test-only additions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The equivalence gate flagged skills/backlog/SKILL.md as an unaccounted diff in
every plugin. Not a regression: commit b5388f7 on main changed one line in
instructions/r3/core/skills/backlog/SKILL.md ("One story, one context" ->
"Bounded stories, one context ... Disjoint areas in parallel"), the merge at
Step 0 brought it in, and the generator correctly propagated it everywhere.
The golden snapshot simply predates it.

Added as permitted class 23, scoped to that one file with the originating
commit named. A blanket "anything that changed on main" class would blind the
gate to real regressions, which is the property that makes it worth having.

All four gates now green: STRUCTURE, EQUIVALENCE, CROSSREFS, and HOOKS CONTENT
at both --deterministic-hooks postures.

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

Both units held ToBeModified because their criteria assert generated output and
the tree had not been rebuilt. It has been, and every criterion was re-checked
against that output rather than inferred from source:

FR-VAR-0030 — all four hold. Three hooks.json at the declared paths, root and
.github/plugin byte-identical, and hooks/hooks.json the standalone form with an
empty session-start array, matching the pre-#315 snapshot, across all three
hook-emitting copilot plugins.

FR-HOOK-0007 — all nine hold, including the guard on .github/plugin/plugin.json.
That guard holds structurally rather than by luck: the copilot spec emits that
manifest for every set, so unlike commands/coding-flow.md it cannot rot when set
content moves between folders.

Both implementation -> Implemented; status stays Draft, approval independent.

Records are target-state; CHANGES.md is the only change log. Removed progress
and correction narrative from five units -- dated CLOSED/UPDATE entries,
"CORRECTION: the earlier note ...", "at the time this record was written", and a
paragraph explaining a prior draft had since been fixed. The two closed units'
implementationNotes were rewritten to the concise files-affected form (2397->679
and 2500->718 chars). Retirement reasons on Deprecated units are retained: those
state why a unit is gone, which is current content, not history.

RECONCILIATION-17 and -18.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Root cause: closing two units against a rebuilt tree, I wrote dated
verification narrative into implementationNotes; a corpus sweep then found the
same defect in four more units. Test that generalizes: if a sentence would not
survive in a spec that was never revised, it does not belong in the spec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both described defects that commit 843d238 fixed this session; only the
records lagged. Verified by running the CLI rather than by inspection: an
unresolvable --domain token exits 1 naming the folder and the absolute path
searched and creates no output directory, and generate.ts:86 carries the branch
that lets a legitimate empty match exit zero.

RECONCILIATION-19.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both statements already described the current code; only the tags lagged.

NFR-0007's statement lists the per-vocabulary model-normalization processors,
and its criterion holds: file-processors/, plugin-processors/, escaping/,
spec/model-maps.ts and generate.ts are separate units.

FR-CLI-0021's two criteria hold -- cli.ts resolves outputDir to the --output
value when given and <source>/plugins otherwise, verified in code and by use.
Its ToBeModified tag dated from RECONCILIATION-9, when the --source model was
written into the spec ahead of the code; the code has since adopted it.

RECONCILIATION-20.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The statement already read "the target's model-normalization processor" -- the
per-vocabulary wording. The note describing drift against a single
fileNormalizeModels was stale; that dispatcher was deleted under FR-ARCH-0005
and replaced by four per-vocabulary processors, which are what a SpecEntry
pipeline composes today (targets.ts:409).

The unit defines HOW a copy is expressed, not that any target currently needs
one, so "nothing declares it" was the wrong test. Both halves hold: folder
duplication is available only as an additional SpecEntry, and no pre-copy pass
exists anywhere in src/.

Title prefixed "Policy:" so the conditional shape is legible at a glance -- a
"Where X is needed, do it this way" rule reads as an unimplemented feature
otherwise, which is exactly how this unit was misread.

RECONCILIATION-21.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five FR-COPY units prescribe a mechanism rather than assert a feature -- each
opens "Where ..." and names the processor that shall do the work, often
forbidding an alternative. Without a marker such a unit scans as an unbuilt
feature, which is exactly how FR-COPY-0033 was misread. Prefixed "Policy:".

Left unmarked: FR-ARCH-0024, FR-GEN-0001/0003/0010, FR-HOOK-0020, FR-VAR-0072
and NFR-0012. They open with "Where" but assert features, and marking them
would cost the prefix its meaning.

FR-VAR-0030 and FR-VAR-0031 both declared depends on FR-COPY-0033, the
SpecEntry duplication policy, while the Copilot root hooks.json is a
post-render mirror pair applied by a generic mirror processor -- a different
mechanism, as both statements now say. Repointed to DATA-CFG-0002, whose
statement declares that mechanism. Verified no dependency cycles.

RECONCILIATION-22.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Owner approval of every unit standing Draft with implementation=Implemented.
Handles both schema forms in the corpus; status, approved_by and changed move
together as the template intends.

FR-HOOK-0005's note existed only to explain why the unit was unapproved, so it
was removed along with the state it described.

RECONCILIATION-23.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Told twice in one session -- a PR body opened by explaining a reversal, and
requirement notes carried dated CLOSED/UPDATE entries. Generalizes the existing
requirements-specific rule to every reader-facing artifact, and to the shape of
a sentence rather than a word list: a comparative clause implies a before-state
even when no past-tense verb appears.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant