@event4u/agent-config 14.16.0 → 14.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +177 -213
  5. package/README.md +2 -2
  6. package/dist/agent-src/commands/analyze/inbox.md +172 -6
  7. package/dist/agent-src/commands/fix/commit-messages.md +336 -0
  8. package/dist/agent-src/commands/fix.md +5 -3
  9. package/dist/agent-src/commands/pr/merge.md +35 -44
  10. package/dist/agent-src/rules/commit-conventions.md +1 -1
  11. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
  12. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
  13. package/dist/agent-src/skills/aws-infrastructure/SKILL.md +41 -0
  14. package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
  15. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
  16. package/dist/agent-src/skills/conventional-commits-writing/SKILL.md +234 -4
  17. package/dist/agent-src/skills/design-review/SKILL.md +2 -0
  18. package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
  19. package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
  20. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
  21. package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
  22. package/dist/agent-src/skills/terraform/SKILL.md +29 -0
  23. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
  24. package/dist/agent-src/skills/threat-modeling/data/reasoning.csv +1 -0
  25. package/dist/agent-src/skills/threat-modeling/data/surfaces.csv +1 -0
  26. package/dist/agent-src/skills/threat-modeling/data/threats.csv +5 -0
  27. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +12 -0
  28. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
  29. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
  30. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  31. package/dist/cli-delegate/cmd_conformance.js +335 -659
  32. package/dist/cli-delegate/cmd_explain.js +24 -112
  33. package/dist/discovery/deprecation-report.md +1 -1
  34. package/dist/discovery/discovery-manifest.json +55 -29
  35. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  36. package/dist/discovery/discovery-manifest.summary.md +3 -3
  37. package/dist/discovery/dormancy-report.md +1 -1
  38. package/dist/discovery/orphan-report.md +1 -1
  39. package/dist/discovery/packs.json +6 -5
  40. package/dist/discovery/trust-report.md +2 -2
  41. package/dist/discovery/workspaces.json +5 -4
  42. package/dist/hooks/dispatch.js +959 -1304
  43. package/dist/mcp/registry-manifest.json +3 -3
  44. package/dist/mcp/server.json +2 -2
  45. package/dist/mcp/server.mjs +671 -1002
  46. package/dist/shared/settingsConsent.js +1 -1
  47. package/package.json +1 -1
  48. package/src/config/conformance-claim-baseline.json +9 -0
  49. package/src/config/gate-violation-baselines.json +3 -3
  50. package/src/config/release-gate-locality.yml +129 -0
  51. package/src/config/web-launch-readiness.json +1 -1
  52. package/src/scripts/_cli/explain_last/index.ts +1 -1
  53. package/src/scripts/_cli/explain_last/memory.ts +8 -104
  54. package/src/scripts/_lib/release_highlights.ts +172 -1
  55. package/src/scripts/_lib/release_material.ts +66 -0
  56. package/src/scripts/_lib/repeated_run.ts +77 -0
  57. package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
  58. package/src/scripts/_lib/unified_diff.ts +121 -0
  59. package/src/scripts/check_artefact_count_messaging.ts +9 -0
  60. package/src/scripts/check_finding_dispositions.ts +179 -3
  61. package/src/scripts/check_installed_hooks_fresh.ts +381 -0
  62. package/src/scripts/check_release_highlights.ts +89 -4
  63. package/src/scripts/check_suppression_hygiene.ts +11 -0
  64. package/src/scripts/check_web_launch_readiness.ts +295 -15
  65. package/src/scripts/code_graph/build.ts +230 -33
  66. package/src/scripts/code_graph/extract.ts +168 -33
  67. package/src/scripts/code_graph/types.ts +37 -2
  68. package/src/scripts/code_graph/validate.ts +12 -1
  69. package/src/scripts/conformance_scan.ts +2 -2
  70. package/src/scripts/detect_silent_catch.ts +403 -0
  71. package/src/scripts/detect_verification_tampering.ts +538 -0
  72. package/src/scripts/git_authorization_hook.ts +153 -17
  73. package/src/scripts/hook_manifest.json +1 -1
  74. package/src/scripts/hook_manifest.yaml +33 -19
  75. package/src/scripts/hooks/block_no_verify.ts +3 -3
  76. package/src/scripts/hooks/concern_registry.ts +2 -2
  77. package/src/scripts/hooks/evidence_independence.ts +21 -2
  78. package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +25 -369
  79. package/src/scripts/hooks/push_settle_hook.ts +250 -0
  80. package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
  81. package/src/scripts/install-hooks.sh +134 -1
  82. package/src/scripts/language_mirror_hook.ts +27 -5
  83. package/src/scripts/measure_release_mix.ts +384 -0
  84. package/src/scripts/release.ts +31 -143
  85. package/src/scripts/release_drill.ts +71 -1
  86. package/src/scripts/release_mix_taxonomy.json +172 -0
  87. package/src/scripts/release_publication.ts +243 -3
  88. package/src/scripts/release_verify.ts +220 -0
  89. package/src/scripts/schemas/review-findings.schema.json +1 -1
  90. package/src/scripts/schemas/skill.schema.json +11 -1
  91. package/src/scripts/self_review_gate.ts +10 -1
  92. package/src/shared/settingsConsent.ts +1 -1
  93. package/src/scripts/attest_artifact.ts +0 -193
@@ -15,7 +15,7 @@
15
15
  * would mean the agent could arrive at its own permission by observing the
16
16
  * world, which is the confused-deputy shape in miniature.
17
17
  *
18
- * Modelled on `src/scripts/hooks/block_unauthorized_git.ts`: answer from a
18
+ * Modelled on `src/scripts/hooks/git_command_classifier.ts`: answer from a
19
19
  * recorded fact rather than from the model's recollection, and treat a missing
20
20
  * record as NOT granted rather than as probably-fine.
21
21
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
3
  "mcpName": "io.github.event4u-app/agent-config",
4
- "version": "14.16.0",
4
+ "version": "14.18.0",
5
5
  "description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including the counts in these badges.",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "Pre-existing standard-conformance claims in src/skills/**/SKILL.md and src/rules/*.md that carry NO `enforced_by:` declaration. The list may only SHRINK — an entry whose artefact gained a declaration must leave it in the same change, and a new undeclared claim is never added. Written by road-to-defect-population-sweeps 3.2; enforced by tests/contracts/conformance_claim_declaration.test.ts.",
3
+ "_why_empty": "The measured population was TWO artefacts, not the large legacy set 3.2 anticipated, and both were declarable honestly in the same change: accessibility-auditor names its real resolver, design-review declares the gap with its reason. An empty baseline is the strongest state this ratchet can be in, not a missing one — the file exists so the mechanism is wired and testable, and so a future undeclared claim has somewhere it is conspicuously NOT allowed to land.",
4
+ "_known_out_of_scope": [
5
+ "src/skills/design-intelligence/references/design-rules-checklist.md — carries `WCAG 2.2 AA` in a dedup NOTE that points at accessibility-auditor rather than claiming conformance itself. Out of scope because roadmap step 3.1 scopes the bar to `a skill or rule`, and the scanned set is the artefacts that carry frontmatter: `SKILL.md` and `src/rules/*.md`. Recorded here rather than silently excluded."
6
+ ],
7
+ "count": 0,
8
+ "undeclared": []
9
+ }
@@ -34,10 +34,10 @@
34
34
  "note": "The nine active non-draft roadmaps that predate the `relates:` requirement (road-to-agentic-engineering-assurance, road-to-component-library-lifecycle, road-to-council-evidence-integrity, road-to-frontend-fidelity-calibration, road-to-monorepo-scope-and-detection, road-to-observability-plate, road-to-role-scoped-spawn-profiles, road-to-skill-delivery-over-mcp, road-to-standing-payload-diet). This is a NEW obligation rather than a repaired scan root, and the ratchet is the point: `depends:` shipped in the template with nothing requiring it and measured 0 adoption, so requiring `relates:` retroactively across the estate would be an owner-reserved write with no analysis behind the edges it would have to invent (held as step 4.3, deferred). Clearing an entry here is one roadmap at a time: run `agent-config roadmap:context` on it, write the rows the probe surfaces, and lower the count."
35
35
  },
36
36
  "check_source_size_budget": {
37
- "count": 18180,
38
- "landed": "2026-09-03",
37
+ "count": 18068,
38
+ "landed": "2026-09-05",
39
39
  "note": "Lowered 18,245 -> 18,230 on 2026-09-02 by drain/publication-integrity-option-a. Phase 2 Option A moved the authoring instruction out of the generator: release.ts, which sits ~530 lines PAST the 1500 cap so every line there costs one excess, loses the comment emission, a 16-line HEAD_DEFAULT doc block and its alias const, and gains a shorter writer docstring. The doc block moved to _lib/release_material.ts, which is 289 lines and therefore UNDER the cap, so the same prose now costs nothing. Net -15. The branch first came in at +8 and paid its own way down rather than raising the baseline.",
40
- "reason": "Lowered 18,230 -> 18,209 on 2026-09-03 by drain/artifact-location. `doctor --strict` needed 16 lines of integration wiring in cmd_doctor.ts, which sits ~2,100 lines PAST the 1500 cap so every line there costs one excess. The branch first came in at +137 and paid its own way down rather than raising the baseline: the strict semantics, threshold, kill switch, payload helper and argv fragments moved to _cli/_doctor_strict.ts (~160 lines, under the cap, costs nothing), taking it to +15; then the 37-line drift renderer moved to _cli/_doctor_emit.ts. Net -21. The renderer was chosen deliberately: pure rendering, two dependencies, no argparse involvement and therefore none of the byte-parity risk the parser surface carries against the retired Python implementation; nothing outside cmd_doctor.ts imported it. An AI council on the residual split (a: raise by 15 / d: descope the feature); neither was taken, because a parallel branch demonstrated the same day that paying down is achievable in this codebase, which is the evidence the split lacked. This gate's own test comment prescribes it: a commit that splits a god-file lowers the excess and must carry the lowered baseline. MERGE RECONCILIATION 2026-09-03, 18,209 -> 18,205: drain/wired-instruments and drain/artifact-location each lowered from the SAME 18,230 base in parallel -- the branch to 18,226 (net -4, run_continuation_hook.ts's termination ladder moved to _lib/continuation_ladder.ts and runtime_journal.ts's prose to _lib/outcome_vocabularies.ts, both under the cap) and main to 18,209 (net -21, the doctor --strict split above). Taking either side alone is wrong in a different direction: main's value silently discards the branch's earned -4, and the branch's value RAISES the baseline against main and weakens the gate. The committed number is neither -- it is the gate's own output on the merged tree, where both reductions apply. THIS LOCAL READING IS ADMISSIBLE HERE, and the discriminator matters because this file's ci-parity:local-only entry records a 165 -> 164 lowering that was reverted for exactly the opposite case: that count depends on how the environment resolves the projection surface, whereas this gate walks only SOURCE_ROOTS = ['src'] (check_source_size_budget.ts:88) counting excess lines over the 1500 ceiling, and `git status --porcelain -uall -- src/` reported ZERO untracked files, so a clean CI checkout of this commit measures the same 18,205. If CI reads anything else, CI's number is the correct one and this note is the record of why. LOWERED 18,205 -> 18,193 on 2026-09-03 by fix/release-curation-ask. The change adds a fifth publication guard, and its own additions landed at +10 excess: release.ts 2,008 -> 2,018, every line of which is past the 1500 cap. Paid down rather than baselined, in the two ways this gate's doctrine prescribes. The guard's rationale (~13 lines of docstring, including why prMerged is a parameter rather than a call-site if) lives in release_publication.ts, 915 lines and therefore UNDER the cap, so the prose costs nothing; and the two pure text helpers _splitlines and _rstrip moved out of release.ts into release_env.ts (333 lines, also under the cap), taking release.ts to 1,996 -- 12 BELOW where it started. The branch came in at +10 and leaves at -12. Recorded while moving them: _splitlines is duplicated a third and fourth time at adoption_report.ts:106 and readme_linter.ts:418; collapsing all three onto one definition touches three modules and was left out of this change rather than done silently. LOWERED 18,193 -> 18,180 on 2026-09-03 by fix/release-branch-reuse. The step-1 branch decision -- twenty lines of if/else in release.ts -- became one call to checkout_release_branch in release_publication.ts, which is 1,019 lines and therefore UNDER the 1500 cap, so the twenty lines plus a ~35-line rationale now cost nothing. release.ts 1,996 -> 1,983, every one of those lines being past the cap. The drill grew 765 -> 918 for two new scenarios and the snapshot/restore that keeps them from writing the real CHANGELOG, and it is under the cap too. A change that adds a feature and lowers the excess is the shape this gate's doctrine asks for; the alternative was a +13 raise for logic that had no business in the god-file."
40
+ "reason": "Lowered 18,230 -> 18,209 on 2026-09-03 by drain/artifact-location. `doctor --strict` needed 16 lines of integration wiring in cmd_doctor.ts, which sits ~2,100 lines PAST the 1500 cap so every line there costs one excess. The branch first came in at +137 and paid its own way down rather than raising the baseline: the strict semantics, threshold, kill switch, payload helper and argv fragments moved to _cli/_doctor_strict.ts (~160 lines, under the cap, costs nothing), taking it to +15; then the 37-line drift renderer moved to _cli/_doctor_emit.ts. Net -21. The renderer was chosen deliberately: pure rendering, two dependencies, no argparse involvement and therefore none of the byte-parity risk the parser surface carries against the retired Python implementation; nothing outside cmd_doctor.ts imported it. An AI council on the residual split (a: raise by 15 / d: descope the feature); neither was taken, because a parallel branch demonstrated the same day that paying down is achievable in this codebase, which is the evidence the split lacked. This gate's own test comment prescribes it: a commit that splits a god-file lowers the excess and must carry the lowered baseline. MERGE RECONCILIATION 2026-09-03, 18,209 -> 18,205: drain/wired-instruments and drain/artifact-location each lowered from the SAME 18,230 base in parallel -- the branch to 18,226 (net -4, run_continuation_hook.ts's termination ladder moved to _lib/continuation_ladder.ts and runtime_journal.ts's prose to _lib/outcome_vocabularies.ts, both under the cap) and main to 18,209 (net -21, the doctor --strict split above). Taking either side alone is wrong in a different direction: main's value silently discards the branch's earned -4, and the branch's value RAISES the baseline against main and weakens the gate. The committed number is neither -- it is the gate's own output on the merged tree, where both reductions apply. THIS LOCAL READING IS ADMISSIBLE HERE, and the discriminator matters because this file's ci-parity:local-only entry records a 165 -> 164 lowering that was reverted for exactly the opposite case: that count depends on how the environment resolves the projection surface, whereas this gate walks only SOURCE_ROOTS = ['src'] (check_source_size_budget.ts:88) counting excess lines over the 1500 ceiling, and `git status --porcelain -uall -- src/` reported ZERO untracked files, so a clean CI checkout of this commit measures the same 18,205. If CI reads anything else, CI's number is the correct one and this note is the record of why. LOWERED 18,205 -> 18,193 on 2026-09-03 by fix/release-curation-ask. The change adds a fifth publication guard, and its own additions landed at +10 excess: release.ts 2,008 -> 2,018, every line of which is past the 1500 cap. Paid down rather than baselined, in the two ways this gate's doctrine prescribes. The guard's rationale (~13 lines of docstring, including why prMerged is a parameter rather than a call-site if) lives in release_publication.ts, 915 lines and therefore UNDER the cap, so the prose costs nothing; and the two pure text helpers _splitlines and _rstrip moved out of release.ts into release_env.ts (333 lines, also under the cap), taking release.ts to 1,996 -- 12 BELOW where it started. The branch came in at +10 and leaves at -12. Recorded while moving them: _splitlines is duplicated a third and fourth time at adoption_report.ts:106 and readme_linter.ts:418; collapsing all three onto one definition touches three modules and was left out of this change rather than done silently. LOWERED 18,193 -> 18,180 on 2026-09-03 by fix/release-branch-reuse. The step-1 branch decision -- twenty lines of if/else in release.ts -- became one call to checkout_release_branch in release_publication.ts, which is 1,019 lines and therefore UNDER the 1500 cap, so the twenty lines plus a ~35-line rationale now cost nothing. release.ts 1,996 -> 1,983, every one of those lines being past the cap. The drill grew 765 -> 918 for two new scenarios and the snapshot/restore that keeps them from writing the real CHANGELOG, and it is under the cap too. A change that adds a feature and lowers the excess is the shape this gate's doctrine asks for; the alternative was a +13 raise for logic that had no business in the god-file. LOWERED 18,180 -> 18,068 on 2026-09-05 by fix/release-gate-producer-parity. The change wires the governance-mix obligation into the writer and the two local guards and adds a release-gate locality registry; its own additions to release.ts landed at +19 excess, every one of those lines past the 1500 cap. Paid down rather than baselined, in the way this entry's doctrine prescribes: the 132-line test-count-trend unit (_TEST_COUNT_LINE_RE, _TEST_LIST_MAX_BUFFER, _count_from_list_result, _count_tests_current, _previous_test_count_from_changelog, _render_test_trend_line) moved from release.ts into release_publication.ts, which is under the cap and where it now belongs on merit rather than only on arithmetic -- the same diff makes the missing `Tests: N` footer a section-level publication blocker, so the writer that emits the footer and the guard that refuses its absence sit in one module. release.ts 1,983 -> 1,871. All four names are re-exported from release.ts, so tests/scripts/release.test.ts is unchanged. LOCAL READING, ADMISSIBLE ON THIS ENTRY'S OWN DISCRIMINATOR: the gate walks only SOURCE_ROOTS=['src'] counting excess over the ceiling, and `git status --porcelain -uall -- src/` reports ZERO untracked files with the two new src/ files staged, so a clean CI checkout of this commit measures the same 18,068. If CI reads anything else, CI's number is the correct one and this note is the record of why."
41
41
  },
42
42
  "ci-parity:local-only": {
43
43
  "count": 165,
@@ -0,0 +1,129 @@
1
+ # Release-gate locality registry
2
+ #
3
+ # WHY THIS FILE EXISTS
4
+ #
5
+ # `release-validation.yml` is the last gate a release passes and the first one
6
+ # that costs something to fail: by the time it runs, a branch, a pull request
7
+ # and a CI run already exist. Until 2026-09-05 nothing related its jobs to the
8
+ # commands a maintainer can run BEFORE any of that leaves the machine, and the
9
+ # result was a recurring failure shape rather than a one-off — 14.14.0 (PR
10
+ # #1812) on the curated head, 14.17.0 (PR #1856) on the governance-mix
11
+ # response. Both were discoverable locally in under two seconds. Neither was
12
+ # discovered locally, because nothing asked.
13
+ #
14
+ # The registry is the relation the tree was missing. It is read by two
15
+ # consumers, which is what keeps it from rotting into a comment:
16
+ #
17
+ # 1. `tests/scripts/release_gate_locality.test.ts` — every job in the
18
+ # workflow has a row, every row names a live job, every declared script
19
+ # exists, and every `local: ~` carries a reason with an approved prefix.
20
+ # A new release-validation job therefore cannot land without answering
21
+ # "and how does a maintainer run this before pushing?".
22
+ # 2. `task release:verify` — runs every row marked `verify: true` against the
23
+ # working tree. That is the command the release pipeline points at, and
24
+ # the one to run before `task release`.
25
+ #
26
+ # WHAT A ROW MEANS
27
+ #
28
+ # local: the shell command that reproduces the job's assertion locally.
29
+ # `~` (null) means it genuinely cannot run before the PR exists.
30
+ # reason: REQUIRED when `local` is null. Must start with one of
31
+ # NEEDS_PR: / NEEDS_ARTIFACT: / NEEDS_NETWORK: / NEEDS_TOKEN:
32
+ # so the cause is classified rather than merely narrated.
33
+ # verify: whether `task release:verify` runs it. Cheap and deterministic
34
+ # rows are true; anything that packs or installs is false and stays
35
+ # opt-in.
36
+ # network: marks a row that leaves the machine. Excluded from `--cheap`,
37
+ # which is the set the release pipeline runs on its own, so a
38
+ # registry outage cannot read as a release defect.
39
+ # release_branch_only:
40
+ # the assertion only means anything on a `release/X.Y.Z` head.
41
+ # Skipped with a printed reason elsewhere — never silently, and
42
+ # never reported as a pass.
43
+ # script: the implementing script, when the assertion lives in one. `~`
44
+ # marks an assertion that exists only as inline workflow YAML —
45
+ # which is itself the defect this column makes countable.
46
+ # guard: the local guard site that ALSO refuses this, when one exists.
47
+ # A row with a guard fails before the push, not merely before CI.
48
+ # partial: REQUIRED when the `local` command reproduces only part of what the
49
+ # job asserts. Names the uncovered part. A row silently claiming
50
+ # full coverage it does not have is the defect this file exists to
51
+ # end, so the gap is written down rather than inferred.
52
+
53
+ version: 1
54
+ workflow: .github/workflows/release-validation.yml
55
+
56
+ jobs:
57
+ release-shape:
58
+ local: 'git diff --name-only origin/main...HEAD | ./scripts-run src/scripts/check_release_pr_shape --files -'
59
+ verify: true
60
+ # The assertion is about a RELEASE PR's diff — every ordinary branch fails
61
+ # it by construction, so running it off a release branch would teach a
62
+ # maintainer to ignore this command's red. Skipped with a printed reason
63
+ # instead, which is the difference between a gate that is not applicable
64
+ # and one that passed.
65
+ release_branch_only: true
66
+ script: src/scripts/check_release_pr_shape.ts
67
+ guard: ~
68
+
69
+ changelog-entry:
70
+ # The job asserts three things: a heading for the head version, a non-empty
71
+ # body, and a `Tests: N (+M since PREV)` footer.
72
+ #
73
+ # `check_release_highlights` covers the first two — it exits 2 with
74
+ # "CHANGELOG carries no section for <v>". It does NOT read the footer, and
75
+ # saying otherwise here would be the exact defect this registry exists to
76
+ # end: a row asserting a coverage it does not have. The footer half is
77
+ # covered locally by the guard named below, not by the `local` command, so
78
+ # `partial` states that rather than leaving a reader to grep for it.
79
+ local: './scripts-run src/scripts/check_release_highlights --version ${VERSION}'
80
+ verify: true
81
+ partial: 'heading + non-empty body only; the `Tests: N` footer half is covered by the guard, not by this command'
82
+ script: ~
83
+ guard: guard_release_curation
84
+
85
+ surface-equality:
86
+ local: ~
87
+ reason: 'NEEDS_PR: compares the live PR body against the CHANGELOG section; there is no PR body before `gh pr create`. The body is DERIVED from that same section (`pr_body_from_section`), so the local half is the section check above.'
88
+ verify: false
89
+ script: src/scripts/check_release_surface_equality.ts
90
+ guard: ~
91
+
92
+ highlight-plausibility:
93
+ # The job that failed 14.17.0. All four sub-checks (authoring instruction,
94
+ # unrewritten derived marker, `_none_` contradiction, governance-mix
95
+ # response) run locally from local tags alone — no origin, no HEAD_REF.
96
+ local: './scripts-run src/scripts/check_release_highlights --version ${VERSION}'
97
+ verify: true
98
+ script: src/scripts/check_release_highlights.ts
99
+ guard: guard_release_branch_push
100
+
101
+ version-consistency:
102
+ local: './scripts-run src/scripts/check_template_pin_drift'
103
+ verify: true
104
+ script: src/scripts/check_template_pin_drift.ts
105
+ guard: ~
106
+
107
+ finding-dispositions:
108
+ local: './scripts-run src/scripts/check_finding_dispositions --release ${VERSION}'
109
+ verify: false
110
+ reason: 'NEEDS_PR: the unrecorded-findings half reads live PR review comments. The missing-dispositions half does run locally and is what the `local` command covers.'
111
+ script: src/scripts/check_finding_dispositions.ts
112
+ guard: ~
113
+
114
+ audit-gate:
115
+ local: 'npm audit --omit=dev --audit-level=high'
116
+ verify: true
117
+ # Reaches the registry, so it is excluded from the `--cheap` set the
118
+ # release pipeline runs unattended: a registry outage must not read as a
119
+ # release defect. `task release:verify` (no flag) still runs it.
120
+ network: true
121
+ script: ~
122
+ guard: ~
123
+
124
+ release-install-e2e:
125
+ local: ~
126
+ reason: 'NEEDS_ARTIFACT: needs a packed tarball plus a BASELINE_TARBALL of the upgrade-from version; runnable by hand as `bash tests/test_release_install_e2e.sh`, too slow for a pre-push gate.'
127
+ verify: false
128
+ script: ~
129
+ guard: ~
@@ -165,7 +165,7 @@
165
165
  "rule": "The external corpus is a STUDY OBJECT and a per-defect harvest source. Its rule set is not imported. Rules enter ONE AT A TIME, each against a named defect, each carrying its own remediation and verification.",
166
166
  "why": "An imported rule set arrives with its authors' assumptions about site type, jurisdiction and stack, and none of those is checked on arrival. The failure is silent: the skill grows to N checks, each individually plausible, and the conditional axis this file exists to enforce is the first thing a bulk import flattens. One-at-a-time keeps the axis honest because every entry has to answer `applies_to` before it lands.",
167
167
  "ceiling": 50,
168
- "ceiling_enforced_by": "tests/scripts/web_launch_readiness_config.test.ts asserts checks.length < 50. Currently 7."
168
+ "ceiling_enforced_by": "tests/scripts/web_launch_readiness_config.test.ts asserts checks.length < 50. The live count is NOT retyped here: a number a human retypes on every ledger edit drifts, and this field had said 7 while `checks` held 9. Read `checks.length`; the ceiling is the only number this row owns."
169
169
  },
170
170
  "regions": {
171
171
  "_comment": "Region axis (road-to-web-launch-readiness 2.3). LEGAL-ROW CONTRACT: any row here or in `checks` whose `why` names a statute MUST carry `authority` (the citation and where it can be read) and `review_by` (the ISO date it must be re-read by); `authority` and `review_by` travel together, so a citation moved out of prose into `authority` stays dated; and a lapsed `review_by` FAILS the loader rather than warning, because a date nothing reads is a comment. Enforced by legalRowViolations() in src/scripts/check_web_launch_readiness.ts, which loadConfig() calls before returning. Separate from site_types because it answers a different question: site_types decides WHETHER a check applies, regions decides HOW SEVERELY. A DE-targeted site owes an Impressum and a Datenschutzerklaerung by law -- the citations sit in the escalation's `authority` field, dated, rather than in prose, which is what the `ddg-citation-authority` decision of 2026-09-03 settled -- so `required-legal-pages` is critical there and situational elsewhere -- the same check, a different tier. Modelled as a tier ESCALATION rather than as a second applies_to list, because a legal page is not more or less applicable in Germany, it is more or less optional.",
@@ -144,7 +144,7 @@ export function build_trace(
144
144
  inputs: _inputs.build(project_root),
145
145
  route: _route.build(project_root, state),
146
146
  council: _council.build(project_root, state_file),
147
- memory: _memory.build(project_root, state),
147
+ memory: _memory.build(state),
148
148
  pack: _inputs.build_pack(project_root),
149
149
  assumptions: _assumptions.build(state),
150
150
  halt: _halt.build(state),
@@ -2,20 +2,20 @@
2
2
  * Resolve the `memory` why-slot for the trace.
3
3
  *
4
4
  * Ported from the retired Python `src/scripts/_cli/explain_last/memory.py` (ADR-200).
5
- * Behaviour pins the historical contract exactly — same two sources, same
6
- * coercion order, same `float()` semantics, same `None`-on-empty branch.
7
- * No behaviour changes.
5
+ * The coercion order, the `float()` semantics and the `None`-on-empty branch
6
+ * still pin that historical contract. The second source the port carried — a
7
+ * sidecar whose only documented producer was the Layer-2 memory-MCP
8
+ * integration — does not, because
9
+ * `docs/decisions/ADR-094-agent-memory-layer-removal.md` removed that layer
10
+ * and rejected leaving its surfaces inert. Nothing in the tree ever wrote it.
8
11
  *
9
- * Two sources are consulted:
12
+ * One source is consulted:
10
13
  *
11
14
  * - `state.memory[]` — the work-engine writes per-run memory hits here
12
15
  * during the `memory` step. Each entry carries `{entry_id, hit_score,
13
16
  * used_in}` already shaped to the trace contract.
14
- * - `<root>/.agent-memory/hits.jsonl` — optional sidecar produced by the
15
- * memory-MCP integration. Filtered to entries tagged with the run id
16
- * when present.
17
17
  *
18
- * Returns `null` when neither source produced a non-empty list (the schema
18
+ * Returns `null` when that source produced no non-empty list (the schema
19
19
  * accepts a null memory slot so the renderer can drop the section cleanly).
20
20
  *
21
21
  * Parity note (ADR-200): Python's `hit_score = float(...)` always yields a
@@ -23,9 +23,6 @@
23
23
  * The twin carries it as a `PyFloat` marker so the downstream JSON
24
24
  * serializer (and the section renderer's `:.2f`) stays byte-identical.
25
25
  */
26
- import * as fs from 'node:fs';
27
- import * as path from 'node:path';
28
-
29
26
  import { scrub_string } from './scrubber.js';
30
27
 
31
28
  /**
@@ -37,8 +34,6 @@ export class PyFloat {
37
34
  constructor(readonly value: number) {}
38
35
  }
39
36
 
40
- const MEMORY_SIDECAR = path.join('.agent-memory', 'hits.jsonl');
41
-
42
37
  /** Python truthiness (empty string / 0 / null / empty container falsy). */
43
38
  function _pyTruthy(value: unknown): boolean {
44
39
  if (value === null || value === undefined) {
@@ -132,100 +127,9 @@ function _from_state(state: Record<string, unknown>): Record<string, unknown>[]
132
127
  return entries;
133
128
  }
134
129
 
135
- /**
136
- * Python `str.splitlines()` (no args). Splits on the Python line-boundary
137
- * set (`\n \r \r\n \v \f \x1c \x1d \x1e \x85 
 
`) and emits no
138
- * trailing empty element when the string ends on a break.
139
- */
140
- function _splitlines(s: string): string[] {
141
- if (s === '') {
142
- return [];
143
- }
144
- const breaks = new Set([
145
- '\n', '\r', '\v', '\f',
146
- '\x1c', '\x1d', '\x1e', '\x85',
147
- '
', '
',
148
- ]);
149
- const out: string[] = [];
150
- let cur = '';
151
- for (let i = 0; i < s.length; i += 1) {
152
- const ch = s[i] as string;
153
- if (ch === '\r' && s[i + 1] === '\n') {
154
- out.push(cur);
155
- cur = '';
156
- i += 1;
157
- continue;
158
- }
159
- if (breaks.has(ch)) {
160
- out.push(cur);
161
- cur = '';
162
- continue;
163
- }
164
- cur += ch;
165
- }
166
- if (cur !== '') {
167
- out.push(cur);
168
- }
169
- return out;
170
- }
171
-
172
- function _from_sidecar(project_root: string, run_id: string | null): Record<string, unknown>[] {
173
- const p = path.join(project_root, MEMORY_SIDECAR);
174
- if (!fs.existsSync(p)) {
175
- return [];
176
- }
177
- const entries: Record<string, unknown>[] = [];
178
- let content: string;
179
- try {
180
- content = fs.readFileSync(p, 'utf-8');
181
- } catch {
182
- return [];
183
- }
184
- for (let line of _splitlines(content)) {
185
- line = line.trim();
186
- if (!line) {
187
- continue;
188
- }
189
- let raw: unknown;
190
- try {
191
- raw = JSON.parse(line);
192
- } catch {
193
- continue; // json.JSONDecodeError → skip line
194
- }
195
- if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
196
- continue;
197
- }
198
- const rawDict = raw as Record<string, unknown>;
199
- // if run_id and raw.get("run_id") not in (None, run_id): continue
200
- if (run_id) {
201
- const got = rawDict.run_id ?? null;
202
- if (got !== null && got !== run_id) {
203
- continue;
204
- }
205
- }
206
- const entry = _coerce_entry(rawDict);
207
- if (entry !== null) {
208
- entries.push(entry);
209
- }
210
- }
211
- return entries;
212
- }
213
-
214
130
  export function build(
215
- project_root: string,
216
131
  state: Record<string, unknown>,
217
132
  ): Record<string, unknown>[] | null {
218
- // run_id = (state.get("input") or {}).get("data", {}).get("id")
219
- const input = _pyTruthy(state.input) ? (state.input as Record<string, unknown>) : {};
220
- const dataRaw = 'data' in input ? input.data : {};
221
- const data =
222
- typeof dataRaw === 'object' && dataRaw !== null && !Array.isArray(dataRaw)
223
- ? (dataRaw as Record<string, unknown>)
224
- : {};
225
- const run_id = data.id;
226
133
  const entries = _from_state(state);
227
- entries.push(
228
- ..._from_sidecar(project_root, typeof run_id === 'string' ? run_id : null),
229
- );
230
134
  return entries.length > 0 ? entries : null;
231
135
  }
@@ -25,7 +25,11 @@
25
25
  */
26
26
  import { spawnSync } from 'node:child_process';
27
27
 
28
- import { CURATED_HEAD_INSTRUCTION } from './release_material.js';
28
+ import {
29
+ CURATED_HEAD_INSTRUCTION,
30
+ MIX_RESPONSE_MARKER,
31
+ MIX_RESPONSE_PLACEHOLDERS,
32
+ } from './release_material.js';
29
33
 
30
34
  /** The five curated labels, in the order an operator reads them. */
31
35
  export const HEAD_LABELS: readonly string[] = [
@@ -398,6 +402,7 @@ export function publication_blockers(
398
402
  sectionBody: string,
399
403
  version: string,
400
404
  where = '`main`',
405
+ mix: MixObligation | null = null,
401
406
  ): string[] {
402
407
  const out: string[] = [];
403
408
  if (sectionBody.includes(DERIVED_MARKER)) {
@@ -414,9 +419,175 @@ export function publication_blockers(
414
419
  'comment line from the section and re-run.',
415
420
  );
416
421
  }
422
+ out.push(...mix_response_blockers(sectionBody, version, where, mix));
417
423
  return out;
418
424
  }
419
425
 
426
+ /**
427
+ * Everything that makes a whole SECTION unpublishable — the head-level
428
+ * blockers above plus the two obligations that only exist for a full section.
429
+ *
430
+ * Two levels and not one, because `publication_blockers` is legitimately called
431
+ * with a bare curated head (the prefill tests do exactly that), and a head
432
+ * fragment has no tests footer by construction. Folding a section obligation
433
+ * into the head predicate would make those callers red on a rule that does not
434
+ * apply to what they passed.
435
+ *
436
+ * The TWO cheapest guard sites call this one — `guard_release_curation` (before
437
+ * any commit) and `guard_release_branch_push` (before any remote state). The
438
+ * two publication sites reached through `_refuse_unpublishable` (the annotated
439
+ * tag and the GitHub Release) stay on the head-level predicate: by then the
440
+ * section has already shipped in a merged PR, and adding a refusal there would
441
+ * block a resumed publication over an obligation the release cannot still act
442
+ * on. Stated rather than implied, because "the guard sites call this one" read
443
+ * as all four and is not true of any of them but the two named here.
444
+ */
445
+ export function section_publication_blockers(
446
+ sectionBody: string,
447
+ version: string,
448
+ where = '`main`',
449
+ mix: MixObligation | null = null,
450
+ ): string[] {
451
+ const out = publication_blockers(sectionBody, version, where, mix);
452
+ if (!TEST_FOOTER_RE.test(sectionBody)) {
453
+ out.push(
454
+ `the ${version} section carries no \`Tests: N (+M since PREV)\` footer. ` +
455
+ '`_render_test_trend_line` degrades to `null` when the vitest collection ' +
456
+ 'fails, so a missing line means the collection failed during `task release`. ' +
457
+ 'Fix the collection, then render the number and put it on the section — ' +
458
+ `the release commit already exists on ${where}, so amend it there. Do not ` +
459
+ 'invent the count: `npx vitest list | wc -l` is the same probe the writer runs.',
460
+ );
461
+ }
462
+ return out;
463
+ }
464
+
465
+ /**
466
+ * The tests-footer convention, in force for every release since 9.1.0.
467
+ *
468
+ * Duplicated from `release-validation.yml`'s inline check until 2026-09-05,
469
+ * where it was the ONLY copy — so the absence was red at release-PR time and
470
+ * invisible before it. 9.8.0 shipped without the line. The regex is the same
471
+ * one the workflow uses; the workflow's own comment already recorded that the
472
+ * writer "degrades SILENTLY to null on collection failure", which is exactly a
473
+ * defect a local guard should see first.
474
+ */
475
+ const TEST_FOOTER_RE = /^Tests: \d+/mu;
476
+
477
+ /**
478
+ * The measured governance-versus-product level, as the writer and the guards
479
+ * both receive it.
480
+ *
481
+ * A plain record and not the measurement itself: this module is reached by the
482
+ * push guard, the pre-commit ask and the CI gate, and only the callers have git
483
+ * to measure with. Passing the reading in keeps the predicate pure and keeps
484
+ * `measure_release_mix`'s git access out of a module three guards import.
485
+ */
486
+ export interface MixObligation {
487
+ triggered: boolean;
488
+ level: string;
489
+ }
490
+
491
+ /** Minimum written characters after the marker — a bare marker is not an answer. */
492
+ export const MIX_RESPONSE_MIN_CHARS = 40;
493
+
494
+ /**
495
+ * The ONE predicate for the governance-versus-product obligation.
496
+ *
497
+ * `check_release_highlights` refuses this from the CI side and
498
+ * `guard_release_branch_push` from the local side, and until 2026-09-05 only
499
+ * the first of the two knew the obligation existed. That asymmetry is what the
500
+ * contract itself recorded — *"the earliest refusal for this one obligation is
501
+ * the PR, not the push"* (`docs/contracts/CHANGELOG-conventions.md`) — and what
502
+ * cost 14.17.0 a red release PR. Both sides now read this function, so a change
503
+ * to the obligation cannot reach one guard and miss the other.
504
+ *
505
+ * `null` means the measurement did not run (shallow clone, missing tag). That
506
+ * degrades to no blocker, matching the gate's documented stance: this is a
507
+ * governance signal, not a correctness control, and an environment fact must
508
+ * not block a release.
509
+ */
510
+ export function mix_response_blockers(
511
+ sectionBody: string,
512
+ version: string,
513
+ where = '`main`',
514
+ mix: MixObligation | null = null,
515
+ ): string[] {
516
+ if (!mix || !mix.triggered) {
517
+ return [];
518
+ }
519
+ const idx = sectionBody.indexOf(MIX_RESPONSE_MARKER);
520
+ if (idx === -1) {
521
+ return [
522
+ `the ${version} section owes a governance-versus-product response ` +
523
+ `(${mix.level}) and carries none. Add one \`> ${MIX_RESPONSE_MARKER}\` line ` +
524
+ `immediately under the curated head on ${where}, naming the next cycle's ` +
525
+ 'consumer work or a maintainer justification.',
526
+ ];
527
+ }
528
+ const block = mix_response_block(sectionBody, idx);
529
+ const left = MIX_RESPONSE_PLACEHOLDERS.filter((t) => block.includes(t));
530
+ if (left.length > 0) {
531
+ return [
532
+ `the ${version} governance-versus-product response still carries the writer's ` +
533
+ `placeholder(s) (${left.map((t) => `\`${t}\``).join(', ')}) — the measured level ` +
534
+ 'is the generator\'s, the answer is not, and `CHANGELOG.md` is published to npm. ' +
535
+ `Name the next cycle's consumer work (or the justification) on ${where} and re-run.`,
536
+ ];
537
+ }
538
+ if (human_answer(block, mix.level).length < MIX_RESPONSE_MIN_CHARS) {
539
+ return [
540
+ `the ${version} governance-versus-product response carries no written answer ` +
541
+ `beyond the measured level (${mix.level}). The level is the generator's; the ` +
542
+ `response has to name the next cycle's consumer work or a maintainer ` +
543
+ `justification. Write it on ${where}, in the \`> \` block under the marker.`,
544
+ ];
545
+ }
546
+ return [];
547
+ }
548
+
549
+ /**
550
+ * The whole response block: the marker's line plus every blockquote line under it.
551
+ *
552
+ * Reading only the FIRST line after the marker was wrong in both directions, and
553
+ * both were reproduced before this was written. It **accepted** a section whose
554
+ * placeholder line had simply been deleted, because the machine-written level
555
+ * alone is 54 characters and cleared the floor — the smuggled auto-approval the
556
+ * placeholder exists to prevent. And it **refused** the writer's own two-line
557
+ * template, where the level sits on line 1 and the answer on line 2.
558
+ *
559
+ * Scoping to the block also fixes the placeholder scan, which searched the
560
+ * entire section body: a legitimate changelog line containing
561
+ * `<roadmap or issue>` blocked the release.
562
+ */
563
+ export function mix_response_block(sectionBody: string, markerIdx: number): string {
564
+ const lines = sectionBody.slice(markerIdx).split('\n');
565
+ const out: string[] = [lines[0] ?? ''];
566
+ for (const line of lines.slice(1)) {
567
+ if (!line.trimStart().startsWith('>')) break;
568
+ out.push(line);
569
+ }
570
+ return out.join('\n');
571
+ }
572
+
573
+ /**
574
+ * What is left of the response block once the generator's own contribution is
575
+ * removed — i.e. the part a human had to write.
576
+ *
577
+ * The measured level is stripped because the writer emitted it; a floor that
578
+ * counted it would be satisfied by the tool on the author's behalf, which is
579
+ * the whole failure this obligation guards against.
580
+ */
581
+ export function human_answer(block: string, level: string): string {
582
+ return block
583
+ .replace(MIX_RESPONSE_MARKER, '')
584
+ .split(level)
585
+ .join('')
586
+ .replace(/^[>\s.]+|[>\s.]+$/gu, '')
587
+ .replace(/[>\s]+/gu, ' ')
588
+ .trim();
589
+ }
590
+
420
591
  /** Labels whose curated value is still the generator's unedited draft. */
421
592
  export function stale_draft_labels(
422
593
  curated: Readonly<Record<string, string>>,
@@ -288,3 +288,69 @@ function _first_diff(a: string, b: string): string {
288
288
  }
289
289
  return 'surfaces differ only in normalization-invisible content';
290
290
  }
291
+
292
+ /**
293
+ * The marker the governance-versus-product response line carries.
294
+ *
295
+ * Relocated here on 2026-09-05 from `check_release_highlights.ts`, where it was
296
+ * defined next to the refusal that reads it. That placement is why 14.17.0
297
+ * failed: the gate owned the marker, so the **generator** could not write the
298
+ * line without importing a gate, and therefore did not write it at all. The
299
+ * obligation was then discoverable only after `gh pr create` had already spent
300
+ * a branch, a PR and a CI run — the exact cost `guard_release_branch_push` was
301
+ * added to avoid for the sibling obligation two releases earlier.
302
+ *
303
+ * This module has no imports on purpose (see `CURATED_HEAD_INSTRUCTION`), which
304
+ * is what lets the writer, the local push guard and the CI gate all reach one
305
+ * definition instead of three copies.
306
+ */
307
+ export const MIX_RESPONSE_MARKER = '**Governance mix:**';
308
+
309
+ /**
310
+ * The sentinel that marks an EMITTED-BUT-UNANSWERED response.
311
+ *
312
+ * A generator that emits the measured level alone would satisfy the gate's
313
+ * length floor (`MIX_RESPONSE_MIN_CHARS`, 40) with a number the machine
314
+ * produced, turning a written-answer obligation into a formality the tool
315
+ * discharges for itself. That is a smuggled auto-approval, not a fix.
316
+ *
317
+ * So the writer emits the level AND this sentinel, and every gate refuses the
318
+ * sentinel — the same discipline `CURATED_HEAD_INSTRUCTION` and
319
+ * `DERIVED_MARKER` already follow. The net effect is only that the obligation
320
+ * moves from CI to the moment the section is written, where the fix is an edit
321
+ * to a file already open in the working tree.
322
+ */
323
+ export const MIX_RESPONSE_PLACEHOLDER = '<the consumer work>';
324
+
325
+ /**
326
+ * Every placeholder token the writer emits, so none of them can ship.
327
+ *
328
+ * The block carries two, and refusing only the first would leave
329
+ * `<roadmap or issue>` publishable — `CHANGELOG.md` is in `package.json`
330
+ * `files`, so that is the same "the generator's own scaffolding reached npm"
331
+ * failure `CURATED_HEAD_INSTRUCTION` exists for, one token to the right.
332
+ *
333
+ * A named list rather than a shape match over angle brackets, for the reason
334
+ * that constant already states: a pattern such as `/<[a-z ]+>/` rejects
335
+ * unrelated legitimate prose and misses a reworded placeholder. Adding a token
336
+ * to `render_mix_response` means adding it here.
337
+ */
338
+ export const MIX_RESPONSE_PLACEHOLDERS: readonly string[] = [
339
+ MIX_RESPONSE_PLACEHOLDER,
340
+ '<roadmap or issue>',
341
+ ];
342
+
343
+ /**
344
+ * Render the response block the writer emits when the obligation triggers.
345
+ *
346
+ * Two blockquote lines, immediately under the curated head and outside it, per
347
+ * `docs/contracts/CHANGELOG-conventions.md` § Governance-versus-product
348
+ * response — the head's ten-line cap is for product lines, and a sixth label
349
+ * would make every historical section retroactively incomplete.
350
+ */
351
+ export function render_mix_response(level: string): string[] {
352
+ return [
353
+ `> ${MIX_RESPONSE_MARKER} ${level}.`,
354
+ `> Next cycle ships ${MIX_RESPONSE_PLACEHOLDER}, tracked in <roadmap or issue>.`,
355
+ ];
356
+ }