Skip to content

release: align main — the CLI and MCP resolve the SDK that carries the security wave, 1.2.2 (GT-634) - #284

Merged
beyondnetPeru merged 32 commits into
mainfrom
develop
Jul 30, 2026
Merged

release: align main — the CLI and MCP resolve the SDK that carries the security wave, 1.2.2 (GT-634)#284
beyondnetPeru merged 32 commits into
mainfrom
develop

Conversation

@beyondnetPeru

Copy link
Copy Markdown
Contributor

Pull Request Summary

Aligns main with develop so 1.2.2 can be published from main, with provenance pointing at the default branch rather than a feature branch.

Two independent pieces of work:

1. The release (#282) — @beyondnet/evolith-cli and @beyondnet/evolith-mcp 1.2.2 (GT-634). Both declared @beyondnet/evolith-sdk: ^1.1.0, which resolves to exactly 1.1.0 — the SDK publishes 1.0.0, 1.1.0, 2.0.0 and nothing in between, and a caret cannot cross a major. sdk@1.1.0 is from 2026-07-18; sdk@2.0.0 shipped the security wave on 2026-07-27; both consumers were published on 2026-07-28, after 2.0.0 existed, still on the 1.x line. The lockfile made it concrete: it pinned the registry tarball of sdk@1.1.0 nested inside each consumer, where it shadows the workspace link, so npm ci resolved the pre-wave SDK while every local run read the symlink to 2.0.0.

Also in that PR, three version-pinned artifacts the bump moves (maturity-reconciliation.json, evolith-machine-contracts.json producer version, product-inventory.{md,es.md}) and a real fix to the release gate:

check-install-smoke could not run under the rehearsal, and reported a good artifact as broken. npm publish --dry-run exports npm_config_dry_run=true into prepublishOnly; the guard's nested npm pack/npm install inherited it, so pack printed a filename and wrote nothing. The dry-run mode had therefore never exercised this gate — the 2026-07-28 publish passed it only because no dry-run flag was in the environment.

2. #279 — the evaluability capture (GT-633), from a parallel session. Already merged and validated on develop; it rides along under the normal develop → main flow. Its recapture (native-handler 139 → 151, documentation-only 129 → 136, backlog 60 → 48, corpus 379 → 386) was independently reproduced in the other branch of this work and the numbers agree.

Verified

All 7 required checks green on 87ebd609 before #282 merged. On the release branch: plan-npm-release → 2 of 8 WILL PUBLISH; a clean install of both 1.2.2 tarballs, in a directory that has never seen this workspace, resolves sdk@2.0.0; check:install-smoke passes both plainly and with npm_config_dry_run=true (the runner condition, reproduced locally); CLI jest 1436/1436, mcp-server 433/433.

After this merges

npm-release.yml on main: dry_run=true first, then dry_run=false to publish @beyondnet/evolith-mcp@1.2.2 and @beyondnet/evolith-cli@1.2.2 with provenance.

Evolith Core Quality Gates

Linked ADRs / Issues

Conventional Commits

  • Commits follow Conventional Commits.

🤖 Generated with Claude Code

beyondnetPeru and others added 17 commits July 29, 2026 14:03
The GT-595 config-shaped slice gave MTN-05 and GIT-08 real handlers, and the
first thing they reported was that `evolith init` produces a repository that
fails its own first `evolith validate` with two REAL verdicts — not skips.
Both were pinned by name in rule-applicability.integration.spec.ts so that
closing them would be visible. They are closed here, by different means,
because they are different kinds of defect.

GIT-08 was a SCAFFOLD gap. Conventional Commits bind from the first commit and
the corpus already states the convention and its type list verbatim, so there
is nothing to decide and nothing to invent. `init` now emits all three legs the
handler reads: commitlint.config.mjs, the commitlint packages in
devDependencies, and — with `--features hooks` — a .husky/commit-msg that runs
`npx --no-install commitlint`, so a missing tool is an ERROR rather than the
silent skip GT-623 found. When hooks are not selected the config is still
written and the result carries a warning saying nothing runs it, rather than
leaving a config that looks like enforcement and is not.

It is runtime-independent on purpose: commitlint is a Node tool, so a .NET or
Python satellite carries a tooling-only package.json — which is what such
repositories do in practice and what GIT-08's handler actually reads. Emitting
the config alone would have left the rule failing for every non-Node runtime.

MTN-05 was NOT a scaffold gap, and emitting a `spec.boundedContexts` stanza
would have been the wrong fix. The rule's own description says the strategy
MUST be defined "before Phase 2 Design", and a phase-0 scaffold has no bounded
contexts to declare; satisfying it at init would have meant writing an invented
persistence decision into every new repository, after which MTN-05 would pass
for a reason nobody chose. Worse, per the handler's own note the two
vocabularies are irreconcilable — evolith-yaml.schema.json restricts
`persistence` to a database-engine enum under additionalProperties:false — so
any value written either breaks the contract schema or cannot match the rule.
It is annotated `appliesFromSdlcPhase: 2` instead, which is the applicability
fact the rule always stated in prose. The rule is DEFERRED, not weakened: a
repository that has reached Design is still failed by it for the same reason,
and that is asserted rather than assumed.

The pinned assertion now reads [] — every remaining blocking finding on a fresh
satellite is the GT-595 blocking-and-skipped invariant, not a verdict against
anything the user did.

Verified: core-domain 1442 passed, infra-providers 129 passed, sdk/cli 1436
passed, ruleset schema gate 168/168, native-evaluator parity 451 assertions,
doc validation 1491 files. Confirmed per-rule rather than by the suite alone:
GIT-08 now executes and PASSES (typescript and python), and MTN-05 is reported
not-applicable rather than silently passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The init-scaffold defect that GT-595 exposed now has a row of its own rather
than living only in a pinned test assertion. Registered as GT-633 (P1/S,
`Core Domain`) and closed on 05c3f62.

The row records the part that is not obvious from the diff: the two halves were
closed by OPPOSITE means. GIT-08 was a scaffold gap and nothing else, so `init`
now emits the config, the package and the hook. MTN-05 was not — its own text
defers it to Phase 2 Design, and satisfying it at init would have written an
invented persistence decision into every new repository, so it is annotated
`appliesFromSdlcPhase: 2` instead. A board that recorded only "both fixed"
would lose exactly the distinction worth keeping.

Also recorded, because it nearly produced a false CLOSURE: the premise did not
reproduce on first contact. The suite resolves core-domain through the
workspace symlink to a `dist/` that was nine days stale and predated the GT-595
handlers, so both rules reported `skipped` and the pinned assertion already read
`[]`. Same untracked-build-state class as GT-625 and GT-632, but inverted — here
the stale artifact would have "proved" the gap closed before a line was written.

Counters recomputed rather than incremented by hand: 633 gaps EN/ES, 609/609
catalog sections, 575 closure records. The executive summary and maturity
reconciliation were regenerated, and their diff is exactly the three +1 deltas,
which is what shows nothing else drifted.

Verified: 08-validate-tracking, 05-validate-executive-summary,
09-reconcile-maturity --check, 01-validate-docs (1491 files) and
04-check-bilingual-parity all exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in the follow-on GT-595 left pinned: a freshly initialized satellite
failed MTN-05 and GIT-08 with REAL verdicts, and the assertion in
rule-applicability.integration.spec.ts named them so that closing them would be
visible. It now reads [].

Closed by opposite means on purpose. GIT-08 was a scaffold gap — `init` emits
commitlint.config.mjs, the commitlint packages, and a commit-msg hook that
fails rather than skips when the tool is absent. MTN-05 was not: its own text
defers it to Phase 2 Design, so it carries `appliesFromSdlcPhase: 2` rather
than having the scaffold invent a persistence decision no team made.

Registered and closed on the board as GT-633 (633 gaps, 575 closure records).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…otation

The exploration tester's anti-drift check went red on `construction` and `qa`.
Regenerated from the live capture rather than hand-edited, which is the whole
point of those documents.

The drift is exactly `rulesTotal: 178 -> 177`, in six captured responses and
nothing else. That single decrement IS the MTN-05 annotation: on the phase-0
satellite the tester scaffolds, the rule moved out of the evaluated denominator
and into `rulesNotApplicable`. One rule left, the one annotated, and no other
captured surface behaviour moved — so the regeneration doubles as evidence that
the change does what it claims and no more.

`findings = 0` throughout: no CLI/MCP/REST divergence was introduced.

Verified: test:exploration 6/6, 01-validate-docs 1491 files,
04-check-bilingual-parity 1492 scanned. Reproducing this locally first needed
@nestjs/cache-manager, which is declared in core-api and mcp-server but absent
from the shared node_modules — an install gap in that tree, not a code defect;
CI installs it from the lockfile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tself

Investigating why GT-633's PR had `Governance guards (GT-578)` in the fail
column surfaced an uncommitted, unfinished refactor already sitting in this
worktree: `resolveCommand` called `npmScriptOperand(...)`, a function that was
never defined, so the guard crashed with a ReferenceError on every invocation
-- even bare `--report` mode, before `--strict`/`--max-dead` were ever reached.
309 was never a real measurement of the evidence corpus; it was whatever count
the guard produced before it hit the crash path on a given run.

Restoring that function (equivalent to the pre-refactor inline behaviour)
dropped the count to 78 and exposed a second, real bug the first one had been
masking: the corpus uses `npm run --workspace <path> <script>` throughout --
the `--workspace` flag BEFORE the script name, a form `npm` itself accepts --
and the resolver's script-name search had no notion that `--workspace`
consumes the next token, so it took the workspace path itself as the script
name and reported it "not declared". Fixed, dropping the count to 52.

The rest is the data migration the guard's own docstring already described:
~290 pre-`src/`-refactor and pre-`product/`-move paths recorded when they were
correct. 18 renames applied here, each verified against a real successor file
present on disk before being written -- `product/infra/docker-compose.yml`,
`product/operations/**`, `.harness/scripts/ci/agentic/review-*.test.mjs`,
`src/tests/contract/roundtrip-gate-evaluate.mjs`,
`reference/core/control-center/gaps/gap-reference-catalog{,.es}.md` -- plus one
bare-filename fix (`chat.command.ts` -> its real path) once confirmed to exist.
That took 52 -> 21.

A third, unrelated defect found on the way: `.harness/bin/opa` (15 of the
original 309, the single largest bucket) is gitignored and downloaded by a
separate CI step this guard's own job never runs. Same finding as GT-632 on
guard 47 -- a gitignored artifact absent on a clean checkout mistaken for a
wrong path -- but narrower here: that guard could confirm a path's ancestor was
real; this one cannot confirm the leaf exists at all, so the added verdict is
`unchecked`, not `resolved`.

`--max-dead` moves from 305 to 21, proven exact (not padded) by observing the
guard fail at 20. The residual 21 is retired-artifact or prose-misparse debt,
each individually confirmed rather than force-mapped to a plausible target: a
consolidated `.bmad-core/AGENTS.md`, the retired `evolith-bff` gateway, the
retired `packages/mcp-tools`, two k8s manifests with no path-equivalent
successor, a retired sandbox app, three root-level scripts predating
workspaces, and three records where `vision/` / `tools/list` are prose, not
paths.

5 new self-tests cover both bugs and their negative twins, so the fix cannot
blind the guard: 39/39 pass. Verified: `08-validate-tracking`, guards 42-45 in
the same CI job, and a full `--execute --verbose` run -- 6 pre-existing
executed failures, none among the paths touched here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dead-reference ratchet fix gets a row of its own rather than living only
in a workflow comment and a code diff.

Registered as GT-634 (P1/S, `.harness`) and closed on 5acb29e. The row records
what the diff alone would not: the guard was never really measuring 305 of
evidence-corpus debt — a crashing ReferenceError and a `--workspace`-before-
script-name misparse were BUGS IN THE GUARD, and only after fixing both did the
true count (309, unchanged) become visible for the first time. Separately, it
records that the remaining 21 were deliberately NOT force-mapped to a
plausible-looking target — a wrong rename would corrupt the record it claims
to verify, which is worse than an honest residual — and names each retired
artifact by id so the reasoning survives past this session.

Counters recomputed, not hand-incremented: 634 gaps EN/ES, 610/610 catalog
sections, 576 closure records. Maturity reconciliation and the executive
summary regenerated in the correct order (reconcile first, summary second —
running them the other way crashes with a drift error) and their diff is
exactly the expected deltas.

Verified: 08-validate-tracking, 05-validate-executive-summary,
09-reconcile-maturity --check, 01-validate-docs (1491 files) and
04-check-bilingual-parity all exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…read Core (#279)

`native-evaluability-snapshot.json` was 12 rules behind Core and nothing said
so. Its own `validation` field promised it reproduced the counts pinned by
`rule-corpus-triage.spec.ts`, but the only guard over it — the test named "the
evaluability snapshot still matches the class counts pinned by Core" — asserted
`evaluability.counts` against six literals typed into the test file. It compared
the snapshot with a second copy of the snapshot and never with Core, so it
passed while the pin moved 139 -> 151 and the published handler backlog
overstated the work by twelve rules.

Recapture, from the live triage rather than by hand:
  corpus              379 -> 386   (the 7 ADR rulesets 0118..0124 were absent)
  native-handler      139 -> 151
  documentation-only  129 -> 136
  unimplemented-native 60 ->  48

The per-rule diff is closures, not a renumbering: HXA-01/02/04/05 (GT-632) and
MTN-05, GIT-08, SEC-RL-01/02, ED-R04..06, DAM-R05 (GT-595) are `native-handler`
now. `handlerBacklog` re-derives with them — realBacklogSize 60 -> 48,
remainderToAuthor -> 38, `not-in-snapshot` back to 2 from 9.

The guard is rebuilt at both ends, and neither end is a copy:

 - `rule-corpus-triage.spec.ts` RENDERS the snapshot from the same `CLASSIFIED`
   array its assertions measure, and pins it byte-for-byte — the `--check`
   shape `build-iso-5055-mapping.mjs` already uses for its own outputs.
   Recapture with UPDATE_EVALUABILITY_SNAPSHOT=1. A hand-edit now fails jest.
 - `iso-5055-mapping.test.mjs` parses the pinned `expect(SUMMARY.byClass)`
   object out of Core's suite instead of restating it. A pin it cannot find is
   a failure, never a skip, so the test cannot go green by losing its authority.

Both were verified to go red by injecting drift from each side.

Also corrects the two READMEs, which published the same stale figures in prose
(60-rule backlog, "17 of 60", 381 rules), and states why the backlog moved so it
does not read as a re-cut denominator. Note that `build-iso-5055-mapping.mjs
--check` was already failing before this change: the corpus had grown by seven
rulesets and the table had not been regenerated.

Verified: node --test src/rulesets/standards/iso-5055-mapping.test.mjs (7/7),
npx jest src/application/validators/rule-corpus-triage.spec.ts (22/22),
build-iso-5055-mapping.mjs --check.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…275

Investigation only, at the user's explicit request -- no fix applied. Each of
the three CI checks still red on PR #275 (Security Audit, E2E Tests, Evolith
Core Validation) is root-caused with evidence, not assumed pre-existing on the
strength of "it was probably already broken":

  - Security Audit: 31 npm audit findings (29 high, 2 moderate) in transitive
    deps of jest-snapshot/fast-uri. No package.json or lockfile touched here.
  - E2E Tests: one test of 132, "should validate architecture with --arch
    flag", exits 1 where it expects 0 or 2. #273's own CI run -- the same
    branch's prior merged PR, before any commit in this session existed --
    shows the identical failure by name and by count.
  - Evolith Core Validation: fails at Bilingual Terminology Lint, 5
    inconsistencies across four .es.md files, none touched by any commit in
    this PR (checked with git log per file, not assumed).

Left PENDING with unchecked acceptance criteria: fixing the npm audit findings
risks a breaking dependency bump, the E2E exit-code mismatch needs a decision
about which side (CLI or test) is wrong, and the terminology fixes need a
human call on which suggestions are correct versus a deliberate exception --
none of that is this closure's call to make unilaterally.

Counters recomputed: 635 gaps EN/ES (594 done, 23 pending), 611/611 catalog
sections, 576 closure records (unchanged -- GT-636 is open, so it carries none).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Continuing GT-636's investigation into action, at the user's explicit request.

Bilingual Terminology Lint (4 files, 5 flagged terms): two were genuine
untranslated prose, corrected directly -- "database" -> "base de datos" in
0124-credential-secret-management-standard.es.md, "ports and adapters" ->
"puertos y adaptadores" in level-3-components/README.es.md. The other three
were NOT prose gaps and translating them blindly would have made the
documents worse, not better: gap-tracking.es.md's "Portability" cites
ISO/IEC 25010:2023's own English characteristic name inside a clause renaming
it to "Flexibility" -- translating only that one word while its three
neighbors (Usability, Interaction capability, Flexibility) stayed English
would read as broken, not fixed. maturity-assessment.es.md's "Modular
Monolith" / "Microservices" are two cells in an 8-row topology-name column
where the other six rows (Distributed Modules, Serverless, Edge Computing,
Event-Driven, Data Mesh) are correctly left in their canonical English form --
translating only two would have broken that table's own consistency. All
three are marked with backticks instead, which the linter already excludes
from prose scanning by design (it strips inline code before matching, per its
own header comment) -- an honest "this is a cited identifier, not narrative"
markup, not a workaround.

E2E Tests (1 test of 132): `should validate architecture with --arch flag`
called `validate --architecture --arch-level F1`. Neither flag ever existed --
`grep -rn "arch-level" src/sdk/cli/src/` returns nothing, and `validate --help`
confirms the real flag is `-a, --arch` (a boolean, no level argument).
Commander rejected `--architecture` as unknown and exited 1, indistinguishable
from a real crash under this test's own `[0, 2]` contract. Reproducing this
locally required `npm run build` (not `tsc -b` directly) so `prebuild` fires
`copy-rulesets` -- my first repro attempt skipped that lifecycle hook and hit
an unrelated RULESET_NOT_FOUND that made the actual defect harder to see.
Fixed to `validate --satellite <path> --arch`; the full suite now passes
132/132, confirmed with the exact CI command (`npm run test:e2e`).

Security Audit: investigated, not fixed here. The isolated repro (a scratch
worktree with a REAL node_modules copy, not the per-package symlink farm this
session built earlier for workspace testing -- that farm was confirmed to
corrupt `npm ls`'s own graph display, though not `npm audit`'s severity
count, which matched at 31 in both environments) traced the two real chains:
`babel-plugin-istanbul` (via `ts-jest`/`@jest/transform`) and `eslint`'s own
bundled `minimatch` both carry a nested, un-hoistable `brace-expansion@1.1.16`
that the repo's existing root `overrides.brace-expansion: "2.1.2"` does not
reach, plus `fast-uri@3.1.2` under `ajv` (safely bumpable to 3.1.4, in-range
of ajv's own `^3.0.1`). The plain `npm audit fix` cannot even attempt these:
it fails immediately on an unrelated `@typescript-eslint` peer conflict.
Every one of the 31 is a devDependency (jest/eslint/@nestjs-cli/babel
toolchain) with no production runtime exposure, and the real fix is either a
new targeted nested override (unverified against every workspace's lint/test
run) or a major bump of core tooling across ~10 workspaces -- exactly the
class of hard-to-reverse, broad-blast-radius change this session should not
push unilaterally at the end of a long run without dedicated regression
testing. Left as accepted, tracked debt in GT-636 rather than forced through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Terminology and E2E were real bugs, not accepted debt -- corrected in the
prior commit. Security Audit stays open, deliberately, with the deeper
forensic detail this session's investigation actually found: two independent
un-hoistable brace-expansion@1.1.16 chains the repo's own existing override
doesn't reach, plus a safely-bumpable fast-uri, all devDependency-only with
no production runtime exposure -- and why forcing either fix (an unverified
nested override or a multi-workspace major bump) isn't something to push
through solo at this hour without real regression testing.

Status PENDING -> IN-PROGRESS; 3 of 4 acceptance criteria ticked. Counters:
15 in progress (was 14), 22 pending (was 23), total unchanged at 635.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…urity wave, 1.2.2 (GT-634)

Separated from PR #277 on purpose: that branch also carries the GT-633 standards
work, which now needs reconciling against #279 (the same fix landed on develop
independently). None of that touches these six files, and an irreversible publish
should not wait on a design merge.

THE DEFECT. `@beyondnet/evolith-cli@1.2.1` and `@beyondnet/evolith-mcp@1.2.1` both
declared `@beyondnet/evolith-sdk: ^1.1.0`, which resolves to EXACTLY 1.1.0: the
SDK publishes 1.0.0, 1.1.0 and 2.0.0 with nothing in between, and a caret cannot
cross a major. sdk@1.1.0 is from 2026-07-18; sdk@2.0.0 shipped the security wave
on 2026-07-27; both consumers were published on 2026-07-28, AFTER 2.0.0 existed,
still on the 1.x line. So `npm install @beyondnet/evolith-cli@latest` installed a
pre-wave SDK, and the exposure GT-570 declared closed for "anyone installing
latest" was not closed for this dependency.

The lockfile made it concrete rather than cosmetic: it pinned the registry tarball
of sdk@1.1.0, with integrity hashes, at src/sdk/cli/node_modules and
src/packages/mcp-server/node_modules. A clean `npm ci` fetched the pre-wave SDK and
nested it inside each consumer, where it SHADOWS the workspace link — the
top-level symlink to the local 2.0.0 is what a developer reads, the nested 1.1.0
is what a fresh install resolves. Same class as GT-625. Both entries disappear
once the range is ^2.0.0.

Patch, not minor, and the reason is checked rather than assumed: neither package
re-exports the SDK and both use only `EvolithRestClient`, so 2.0.0's breaking
change (payload types re-exported from core-domain, `.passed` -> `.verdict`,
`'info'` severity retired) reaches no public API of either.

ALSO HERE, because it blocked the rehearsal: check-install-smoke was reporting the
npm version rather than the artifact. `--pack-destination` is not honoured
identically across npm majors when the cwd is a workspace package — npm 11 writes
to the destination, npm 10 (the Node 20 runner) writes to the package directory,
and both print the same filename. The first 1.2.2 dry run (30499155684) failed on
that with a perfectly good tarball. It now looks in both and moves a stray tarball
out of the source tree.

VERIFIED ON THIS BRANCH, with an install rather than a range:
  - plan-npm-release -> 2 of 8 WILL PUBLISH, in dependency order
  - clean install of beyondnet-evolith-cli-1.2.2.tgz in a directory that has never
    seen this workspace resolves @beyondnet/evolith-sdk@2.0.0; same for
    beyondnet-evolith-mcp-1.2.2.tgz
  - check:install-smoke -> 36 specifiers resolve, binary boots printing 1.2.2
  - check:release-drift -> passed
  - CLI jest 1436/1436 (99 suites), mcp-server 433/433 (53 suites)
  - lockfile diff is two versions, two ranges, two nested removals; produced by
    `npm install --package-lock-only` on develop's lockfile, not hand-merged

The `!` marks the transitive major for consumers who reach the SDK through these
packages; neither package's own API changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both were caught by required checks on PR #282, and both are the gates working.

`maturity-reconciliation.json` records `cliPackage: @beyondnet/evolith-cli@1.2.1`,
so the version bump made it stale and `Validate documentation` went red at
"Reconcile maturity evidence". Regenerated, then the executive summary re-checked
in that order — it was already current, since the field it reads did not move.

WORTH RECORDING, because it is a hole in a declaration this repository added
yesterday: `09-reconcile-maturity.mjs` reads `src/sdk/cli/package.json`, and the
GT-630 chain declares that link's `consumes` as the gap board plus the maturity
assessment ONLY. So the CLI manifest is an UNDECLARED INPUT to a derived artifact
— exactly the "generator whose output does not depend only on its declared
consumes" case the fixed-point pass exists to catch, and here it was caught by
staleness on a runner instead. The chain declaration lives on the branch of
PR #277; the `consumes` entry belongs there rather than in a release commit.

`evolith-machine-contracts.json` pins `producer.version` to the CLI's version, and
`10-validate-contract-conformance.mjs` fails when they diverge (GT-563 records that
this check was silently crashing until recently, so it is newly effective). Bumped
to 1.2.2. The contract itself is unchanged: `contractVersion` is untouched and all
four schema hashes still resolve.

Verified: 10-validate-contract-conformance -> 4 schemas hashed and resolved;
contracts jest 104/104; 01-validate-docs 1491 files; 08-validate-tracking;
46-validate-derived-artifact-order -> 3 links current and at a fixed point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rsion (GT-634)

Third and last version-pinned artifact the 1.2.2 bump moves.
`generate-product-inventory.mjs` stamps `@beyondnet/evolith-cli@<version>` into
`product/products/smart-cli/product-inventory.{md,es.md}`, and
`11-validate-product-docs.mjs` runs its `--check`, so `Validate documentation`
went red at "Validate product documentation synchronization". Regenerated: the
diff is the version and the counts the generator recomputes.

The staleness was reported with an EMPTY detail — `Product inventory is stale: `
— because the guard interpolates the child's stdout and the child writes its
reason to stderr. Not fixed here (it is not a release change) but worth naming:
a guard that says something is stale without saying how is a guard you have to
re-derive by hand.

WHAT I DELIBERATELY DID NOT COMMIT. `coverage-dashboard.mjs --check` also reports
drift locally, and regenerating it replaces MEASURED coverage — "88.09% statements
· 88.38% lines" — with "pending a coverage run", because this machine has no
coverage data. That is a regression dressed as a fix. The docs job on the runner
runs a build and the exploration suite before that check and passed green on
develop at 20:56Z, so the report is current where it is measured. Same for
`inventory-summary.{md,es.md}`, whose 167 -> 174 ruleset count belongs to whoever
landed #279, not to this release.

Verified with only this staged: 11-validate-product-docs OK (CLI 1.2.2, no
placeholders), 10-validate-contract-conformance OK (4 schemas), 09-reconcile
--check OK.

Honest note on method: I ran the docs job's fifteen validators in one local loop
to find every pin at once instead of one CI round each. Three of them reported
failure inside that loop and pass individually, including under the same output
redirection — so the loop, not the checks, is what was wrong. I have not explained
it, and the runner is the oracle for this job, not my loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sh, and said so wrongly

ROOT CAUSE, measured rather than guessed — and my two earlier explanations were
both WRONG, recorded here so they are not re-attempted.

`npm-release.yml`'s rehearsal mode runs `npm publish --dry-run`, which exports
`npm_config_dry_run=true` into the environment of `prepublishOnly`. This guard runs
there, and its nested `npm pack` and `npm install` INHERIT it: pack prints the
tarball name and writes no file, install exits 0 and installs nothing. So the guard
declared a perfectly good artifact "NOT installable", twice, in runs 30499155684
and 30499540347.

  npm_config_dry_run=true npm pack --pack-destination <dir>
    -> prints "beyondnet-evolith-cli-1.2.2.tgz", leaves <dir> EMPTY

Verified on npm 11 and npm 10. What I claimed before and was wrong about:
`--pack-destination` is NOT ignored on the runner, and npm majors do NOT disagree
about it — npm 10 honours it exactly like npm 11. I reproduced that with
`npx npm@10` instead of continuing to reason about it.

THE CONSEQUENCE IS BIGGER THAN THE BUG. The rehearsal mode has never exercised
this gate: under dry_run it always failed here, and the real publish of 2026-07-28
passed only because no dry-run flag was in the environment. The workflow's safety
model says "the default invocation exercises the whole path — resolve, build, pack,
verify — and publishes nothing", and for this step that was not true.

Packing and installing into a throwaway directory is the guard's MEASUREMENT, not
part of the publish being rehearsed, so `npm_config_dry_run` is stripped for every
child in the `run` helper rather than at one call site, and `--dry-run=false` is
passed explicitly to the pack.

Verified by reproducing the runner condition locally, which is what the two failed
attempts lacked: `npm_config_dry_run=true node scripts/check-install-smoke.mjs`
passes — 36 specifiers resolve, the installed binary boots printing 1.2.2 — and so
does the plain invocation. The tarball location is also discovered rather than
predicted now, and a failure prints what each candidate directory holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s supposed to fix

npm audit --audit-level=high reported 31 findings (29 high, 2 moderate). 29 of
the 29 high traced to ONE root cause, not 29 independent problems:
GHSA-mh99-v99m-4gvg is a brace-expansion DoS advisory affecting every
published version <=5.0.7, patched only in 5.0.8 -- released after this
repo's existing `overrides.brace-expansion: "2.1.2"` was set, so that override
was already insufficient against the CURRENT advisory before this
investigation began. The "high" severity propagates from that one leaf up
through the entire jest/eslint/@nestjs/cli devDependency toolchain via `via:`
chains, which is why the finding list reads as jest-snapshot/babel-jest/
ts-jest/eslint/etc. all being independently vulnerable when they are not.

Root `overrides.brace-expansion` bumped 2.1.2 -> 5.0.8. Separately,
`overrides.fast-uri: "3.1.4"` added -- in-range of ajv's own declared
`^3.0.1`, unrelated to the brace-expansion chain.

Getting the override to actually apply took more than editing the number.
A plain `npm install` left 4 of the tree's 10 brace-expansion positions
(eslint, @eslint/config-array, @eslint/eslintrc, fork-ts-checker-webpack-
plugin, each via their own un-hoisted minimatch) pinned to whatever the
lockfile already recorded -- the specific `packages` entries had to be
deleted from package-lock.json before a fresh install would re-resolve that
position at all, and it took two rounds (the first re-resolution landed on
an intermediate 1.1.17, itself still vulnerable). A more targeted attempt --
scoping the override under each affected parent, matching this repo's own
existing `eslint.ajv` pattern -- made it WORSE: once a package has ANY nested
override object, npm stops the plain top-level rule from cascading into that
package's subtree at all, silently blocking the fix instead of applying it.
The working fix is the flat top-level override alone, no nested entry
competing for the same package name.

Verified in an isolated clean environment (a scratch worktree with a REAL,
non-symlinked node_modules -- this session's own earlier per-package symlink
farm, built for testing @beyondnet/* packages against local sources, was
independently confirmed to corrupt `npm ls`'s graph display, though not
`npm audit`'s severity count): `npm audit --audit-level=high` exits 0 (checked
as a real exit code, not just the printed summary, which was once observed
stale). 2 moderate findings remain, deliberately not chased: a Windows-only
path-traversal in @hono/node-server's serve-static, irrelevant to this repo's
Linux CI/deployment, whose fix requires bumping an EXACT-PINNED production
dependency (@modelcontextprotocol/sdk) rather than a devDependency override.

Re-verified beyond the audit, after rebuilding every workspace dependency
exactly as the CI job's own "Build workspace dependencies" step does:
core-domain 1442/1442, CLI unit 1436/1436, CLI E2E 132/132, mcp-server
433/437 (4 pre-existing skips) -- all green against the fixed tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(deps)!: the CLI and MCP must resolve the SDK that carries the security wave, 1.2.2 (GT-634)
The security audit fix this closure originally left open turned out to trace
to one root cause (a brace-expansion advisory the repo's own override
predates), not 29 independent problems. Recorded here in enough detail that
the shape of the fix -- why a plain override edit didn't work, why a scoped
override made it worse, why two rounds of lockfile-entry deletion were needed
-- survives past this session, since the failure modes were the non-obvious
part, not the final two-line diff.

Status IN-PROGRESS -> DONE, all 4 acceptance criteria ticked, closure record
added referencing d85c621. Counters: 595 done (was 594), 14 in progress
(was 15), total unchanged at 635, 577 closure records (was 576).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@beyondnetPeru
beyondnetPeru requested a review from a team as a code owner July 29, 2026 23:59
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

beyondnetPeru and others added 5 commits July 29, 2026 20:09
…rifying

CI never triggered on the previous push for over an hour (a genuine webhook
gap, not just a slow queue -- confirmed by comparing against other branches'
run timestamps in the same window). Manually dispatching sdk-cli-ci.yml and
ci-cd.yml surfaced a real regression: `Governance guards (GT-578)` now failed
at 23 dead references against the --max-dead 21 this session pinned only two
commits earlier.

Both new ones were self-inflicted, from GT-636's own validationCommands:
`npm run test:e2e (src/sdk/cli, exact CI command)` -- the workspace name was
prose in a parenthetical, not a real --workspace argument, so the extractor
saw a bare `npm run test:e2e` with no such script at <root>/package.json.
`sdk/cli unit: npm run test:unit` -- the "sdk/cli unit:" label prefix parsed
as a binary invocation (`binRaw: "sdk/cli"`), not as descriptive text.

Both commands are real and already verified to pass; only their phrasing was
wrong. Corrected to `npm run test:e2e --workspace src/sdk/cli (exact CI
command)` and `npm run test:unit --workspace src/sdk/cli`, both of which the
extractor resolves correctly. Ratchet back to exactly 21 -- passes at 21,
fails at 20, same as before this session's own record introduced the drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… with develop

`origin/develop` registered its own GT-634 while this branch was in flight --
`fix(deps)!: the CLI and MCP must resolve the SDK that carries the security
wave, 1.2.2 (GT-634)` -- an unrelated fix (a caret range that pinned a
pre-security-wave SDK). Same number, two different gaps, discovered while
preparing this branch's merge to develop: `git log --all` and every board file
across every branch were scanned for the true maximum in use anywhere (636,
this branch's own GT-636), landing on 637 as the free number.

Pure rename, no content change: every "GT-634"/"gt-634" referring to the
dead-reference ratchet fix, across the board rows, catalog entries, the
closure evidence record, the ci-cd.yml ratchet-step comment, and the guard's
own docstring, becomes "GT-637". Re-verified after the rename: the ratchet
still measures exactly 21 (unchanged -- only text moved) and the guard's own
39 self-tests still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…arson-721318

# Conflicts:
#	reference/core/control-center/maturity-reports/maturity-reconciliation.json
…op merge

The merge conflict on this generated file was resolved with --ours as a
placeholder; this replaces it with the real regeneration, which correctly
picks up develop's CLI 1.2.1 -> 1.2.2 version bump.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(init): a freshly scaffolded satellite passes its own governance (GT-633)
beyondnetPeru and others added 3 commits July 30, 2026 08:40
…ectory

`git push` crashed inside the pre-push hook's repo-optimization step, in this
worktree specifically: `fs.mkdirSync ENOTDIR '.../recursing-carson-721318/.git/
evolith-quarantine/69e8ec9d'`. `.git` at a worktree's root is a plain text
redirect file (`gitdir: <real-gitdir>`), not a directory, and
02-optimize-repo.mjs assumed otherwise in two places:

1. `path.join(root, ".git", "evolith-quarantine")` assumed `.git` was always a
   writable directory to build a subtree under. Fixed by resolving the real
   git-dir via `git rev-parse --git-dir` -- which is `.git` in a normal
   checkout and the per-worktree directory under `.git/worktrees/<name>` here
   -- with `path.resolve`, not `path.join`: `--git-dir` prints a RELATIVE path
   in a normal checkout but an ABSOLUTE one in a worktree, and `path.join`
   would have mangled the absolute case by concatenating it onto `root`.

2. Fixing (1) surfaced a worse defect, verified live: once the crash stopped,
   the quarantine loop saw `.git` itself as an unrecognised untracked FILE
   (only `allowedDirectories` lists it, not `allowedFiles`) and moved it into
   quarantine -- severing this worktree from git entirely. Recoverable, since
   quarantine moves rather than deletes, but confirmed by reproducing it and
   then restoring `.git` by hand before writing the real fix: exclude `.git`
   unconditionally, by name, before the directory/file allowlist split.

The identical root cause was independently present in
03-validate-root-cleanliness.mjs, a separate script with its own duplicated
allowlist -- confirmed failing in this same worktree ("Unauthorized file
found in root: .git") before the same by-name exclusion fixed it too. Lower
blast radius than the pre-push script (CI always checks out fresh, never a
worktree, so this specific guard never hits it there), but the same class of
bug and already fully understood, so fixed alongside it.

Also added two legitimately-tracked root files the first script's allowlist
was missing (evolith.yaml, commitlint.config.mjs), clearing an unrelated
warning this same run surfaced.

Re-verified: both scripts pass clean in this worktree (0 quarantined, 32 root
entries inspected), 01-validate-docs (1491 files) and 08-validate-tracking
(635 gaps, 577 closure records) unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…1318

fix(ci): root-cleanliness guards assumed .git is always a directory
…implementations

GT-633 was fixed twice, in parallel, by sessions that could not see each other.
One shipped a standalone capture script reachable by the derived-artifact chain
guard and the documentation job (#276, #287, on main); the other rendered the
document inside `rule-corpus-triage.spec.ts`, where jest can pin it byte-for-byte
(#279, on develop). Both were right about their half and both recaptured the same
numbers — which is mutual corroboration of the measurement.

But TWO GENERATORS FOR ONE ARTIFACT IS GT-633's OWN DEFECT ONE LEVEL UP: whichever
ran last would win and the other's `--check` would go red for no reason a reader
could see. That is what blocked develop -> main.

The renderer now lives ONCE, in `test/rule-corpus-triage.ts`, with two callers:

  - `rule-corpus-triage.spec.ts` PINS it byte-for-byte (it can recompute);
  - `capture-native-evaluability-snapshot.mjs` WRITES it and `--check`s it, which
    is what the GT-630 chain and docs.yml invoke.

The spec no longer writes. `UPDATE_EVALUABILITY_SNAPSHOT=1` is gone: a second
writer is how this artifact ended up with two generators, and the surviving one is
the command the chain guard already runs.

RESOLVING IT SURFACED THE SAME DEFECT IN MINIATURE, twice, and both are fixed:

  1. The spec asserted `SUMMARY.byClass` against an INLINE literal sitting beside
     the `PINNED_CLASS_COUNTS` constant that the dependency-free guard in
     src/rulesets/standards reads out of this file. Two copies of six numbers,
     and editing either left the other silently disagreeing. Now one declaration,
     asserted against and read by both.
  2. My own first pass DELETED `PINNED_CLASS_COUNTS` while lifting the loader out.
     The documentation-job guard THREW rather than passing — "Could not find the
     PINNED_CLASS_COUNTS literal ... update this parser rather than deleting the
     check" — which is exactly the behaviour that fix was built for. Restored.

The sticky `capturedOn` survives the port: the script renders once with the
previous date and replaces that single field only when the CLASSIFICATION moved,
so the chain's fixed-point replay stays byte-identical without a second ts-node run.

Also converged onto main's shape so the branch merge is trivial rather than
conflicted: `iso-5055-mapping.test.mjs`, both standards READMEs, the 5-link chain
guard with its fixtures, and the two GT-598 steps in docs.yml.

NOTE ON THE NUMBERS: this branch computes native-handler 151, and main pins 154.
Not a measurement bug — main has more handler closures landed. The pin and the
byte-for-byte snapshot assertion are what will catch it when the branches meet,
which is the point of having one renderer.

Verified: capture --check faithful (386 rules), build --check up to date (388
rules), replay byte-identical, iso-5055-mapping guard 9/9, chain guard 5 links
current and at a fixed point with its own suite 8/8, core-domain jest 1444 passed
/ 128 suites, tsc --noEmit clean, 01/04/08/09 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…derer

fix(standards): one renderer, two callers — reconcile the two GT-633 implementations
@beyondnetPeru

Copy link
Copy Markdown
Contributor Author

Conflict map for this merge — standards resolved, the rest attributed

The GT-633 design collision is gone: two independent implementations (a standalone capture script on main, an in-spec renderer on develop) are now one renderer with two callers, landed on develop in #294 and on main in #295. Once #295 merges, test/rule-corpus-triage.ts, capture-native-evaluability-snapshot.mjs, rule-corpus-triage.spec.ts, iso-5055-mapping.test.mjs and both standards READMEs stop conflicting, and the two derived JSONs are regenerated rather than merged.

#295 also carries a bug that reconciliation exposed: the sticky capturedOn comparison was order-sensitive, so the date was rewritten on every run and the GT-630 fixed-point replay would have failed on any day after a capture. It passed on develop only because both runs stamped the same date. develop still has the buggy version and needs the same one-function fix.

The remaining conflicts are not GT-633 and are not mine to resolve. Attribution and the resolution I would recommend:

file develop side main side recommended
gap-closure-evidence.json 8449af3d, 6f7cb3c0 (GT-636/GT-637 records) ff03a223, c3221276 (GT-633/634/635 records) Union of records. Both sides only append. Merge the arrays, keep every id once, then run 08-validate-tracking — it will catch a dropped record whose row survived.
gap-tracking.md / .es.md, gap-reference-catalog.md / .es.md 8449af3d, 2f32aaad ff03a223, c3221276 Union of rows, then recompute the counters from the table. Note the ID collision below.
ci-cd.yml 5acb29ed, 8449af3d 6f0ee004 (GT-624 publish-lag gate) Both: develop's ratchet-budget change and main's new 48-validate-security-publish-lag step are independent additions to the same job.
41-validate-evidence-commands.mjs 5acb29ed — "the ratchet was stuck by two bugs in itself" d1ea72a3 — "stop the evidence guard blaming the evidence for two parser defects" Read both before choosing. Two sessions independently diagnosed the same guard's parser defects. Likely the same two bugs fixed twice; take one and verify the other's fixtures still pass.
cli-e2e.test.ts 3dd6f7cd (GT-636) 74fd2f9f (real --arch flag) Both touch the same e2e; small, take the union.
maturity-reconciliation.json, executive-summary.md / .es.md derived derived Do not merge. Regenerate in GT-630 order after the board is settled: 09-reconcile-maturity.mjs, then generate-executive-summary.mjs.

One thing worth flagging beyond the merge

8449af3d renumbered a row GT-634 → GT-637 because of an ID collision with develop — two sessions allocated the same GT id on different branches. The board has no allocator, so the next parallel wave will do it again. Worth a row of its own.

Also, the same convergent-duplication pattern shows up three times in this merge alone: GT-633 fixed twice, the evidence-guard parser fixed twice, and a GT id allocated twice. That is a process signal, not three coincidences.

beyondnetPeru and others added 3 commits July 30, 2026 09:40
Ports to develop the bug found while bringing the same design to main (#295).

`classificationChanged` compared `JSON.stringify(previous.counts)` against a
counts object rebuilt in KNOWN_CLASSES order, while the renderer emits them in
CLASS_ORDER — a different order for the same six numbers. So it always reported
"changed", `capturedOn` was refreshed on every run, and the GT-630 fixed-point
replay would have failed on any day after a capture.

WHY IT WAS INVISIBLE HERE. The reconciliation (#294) landed and was verified on
the same day the snapshot was captured, so both runs stamped 2026-07-30 and the
bytes matched. A green that depends on the date being unchanged is exactly the
kind this chain exists to distrust. It surfaced on main only because that branch's
committed snapshot carried 2026-07-29, so the two runs disagreed out loud.

OBSERVED BOTH WAYS rather than declared, by forcing the committed date to
2026-07-28 and running the capture with the classification unchanged:

  without the fix -> capturedOn becomes 2026-07-30   (drift)
  with the fix    -> capturedOn stays   2026-07-28   (sticky, as designed)

Verified after restoring the committed snapshot: `--check` passes, the replay is
byte-identical, the derived-artifact chain reports 5 links current and at a fixed
point, and the ISO/IEC 5055 mapping guard is 9/9. Only the script changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(standards): the sticky capture date was rewritten on every run
…for byte

COMMENTS ONLY. No logic changes, and the diff is 3 insertions / 7 deletions in a
single comment block.

The sticky-date fix was written twice — once on main (#295) and once here (#296) —
with the same logic and DIFFERENT prose, because the develop copy added where the
bug had been found. That left `capture-native-evaluability-snapshot.mjs`
conflicting between the branches over ten lines of comment, after the whole point
of the exercise had been to stop this file needing a decision at merge time.

Taking main's text verbatim, so the file is byte-identical on both branches and
drops out of the develop -> main conflict set entirely. Where the bug was found is
recorded in #296's commit message and PR, which is the right place for it: a
comment that differs per branch is a merge conflict pretending to be context.

Verified that behaviour is untouched: `--check` passes, the replay is
byte-identical, and the derived-artifact chain reports 5 links current and at a
fixed point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chore(standards): align the capture script's comment with main, byte for byte
Unblocks #284. main was 45 commits ahead, develop 30, and 17 files conflicted.
Every resolution below was MEASURED or derived, not chosen by preference.

STANDARDS (GT-640, was GT-633) — took main's files wholesale: they carry the
one-renderer reconciliation, the GT-640 renumber and the sticky-date fix that
develop's copy still lacked. The two derived JSONs were REGENERATED on the merged
corpus rather than merged; the capture reports 389 rules / native-handler 154,
which is main's number because main is where those handler closures landed.

CLOSURE REGISTRY — 91 records differed, because BOTH branches independently
migrated the recorded commands onto real paths (the duplication GT-639 records,
again). Chosen by measurement rather than by argument: built both candidate
unions and ran the guard against each with the same parser.

    prefer develop's migration -> 19 dead on a clean checkout
    prefer main's migration    -> 22

develop's is kept. Union first, so all 582 records survive — the 3 only develop
had (GT-633, GT-636, GT-637) and the 5 only main had (GT-624, GT-634, GT-635,
GT-638, GT-640), verified present by id after the merge rather than assumed.

GUARD 41 — both sides fixed the same two parser defects independently, with
different helpers. Took main's `firstOperand`: it handles `--` and the VALUE of
every value-taking flag, while develop's `npmScriptOperand` knew only
`--workspace`, so `npm run --prefix ./x build` would have read `./x` as the script
name. develop's now-unused helper is deleted rather than left as a second parser
for one job.

RATCHET BUDGET — 19, the number this tree actually counts, not either side's (21
on develop, 38 on main). Both were measured against their own corpus and their
own parser; neither describes the merged one.

BOARD + CATALOG — union of rows and entries, eight new ids across both sides, and
the counters RECOMPUTED from the merged table: 600 / 640 done, 14 in progress, 22
pending, 4 deferred. Never merged as text.

WORKFLOWS — union of steps. docs.yml took main's telling of the GT-598 comment
(both sides added the same two steps); ci-cd.yml took develop's ratchet history,
which owns the GT-637 fixes.

MATURITY REPORTS — regenerated in GT-630 order, not merged.

CLI E2E — kept develop's GT-636 comment over identical code.

Verified on the merged tree: core-domain jest 1455 passed / 129 suites, tsc
--noEmit clean, mcp-server suite green, guard 41 self-tests 45/45, guard 49 13/13,
iso-5055-mapping 9/9, 43-validate-guard-negative-fixtures 38/38 observed red,
46 chain current and at a fixed point, 01/04/08/49 green, 41 within its new budget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n-2026-07-30

Merge main into develop — resolve the 17-file divergence, unblocking #284
@github-actions

Copy link
Copy Markdown

📊 Bilingual Coverage Impact

PR Changes

  • Paired EN/ES files modified: 3
  • New EN files needing ES translation: 2

Repository Coverage

Metric Value
Total EN files 524
Total ES files 498
Paired files 0
Coverage 0%

⚠️ Action required: 2 new EN file(s) added without ES counterparts.

To create skeletons:

node .harness/scripts/generate-es-skeleton.mjs <file.md>

Generated by GitHub Actions

@beyondnetPeru
beyondnetPeru merged commit 1a04b9b into main Jul 30, 2026
69 checks passed
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