@event4u/agent-config 8.10.0 → 8.12.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 (226) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -0
  3. package/CHANGELOG.md +67 -149
  4. package/README.md +6 -6
  5. package/dist/agent-src/commands/challenge-me/vision.md +121 -32
  6. package/dist/agent-src/commands/challenge-me/with-docs.md +21 -8
  7. package/dist/agent-src/commands/commit/in-chunks.md +1 -1
  8. package/dist/agent-src/commands/commit.md +2 -1
  9. package/dist/agent-src/commands/cost/report.md +2 -1
  10. package/dist/agent-src/commands/feature/dev.md +4 -4
  11. package/dist/agent-src/commands/humanize.md +101 -0
  12. package/dist/agent-src/commands/jira-ticket.md +2 -2
  13. package/dist/agent-src/commands/memory/load.md +15 -0
  14. package/dist/agent-src/commands/optimize/project.md +12 -0
  15. package/dist/agent-src/commands/roadmap/create.md +12 -11
  16. package/dist/agent-src/commands/tests/create.md +1 -1
  17. package/dist/agent-src/commands/worktree/cleanup.md +22 -17
  18. package/dist/agent-src/commands/worktree/create.md +3 -1
  19. package/dist/agent-src/commands/worktree/status.md +12 -0
  20. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +66 -0
  21. package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +1 -0
  22. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +47 -3
  23. package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
  24. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +111 -85
  25. package/dist/agent-src/contexts/execution/roadmap-writing-source-derived.md +65 -0
  26. package/dist/agent-src/contexts/execution/subagent-modes-detail.md +84 -0
  27. package/dist/agent-src/rules/architecture.md +6 -51
  28. package/dist/agent-src/rules/autonomous-execution.md +7 -37
  29. package/dist/agent-src/rules/broken-access-control.md +7 -45
  30. package/dist/agent-src/rules/code-comment-discipline.md +64 -0
  31. package/dist/agent-src/rules/context-hygiene.md +14 -0
  32. package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
  33. package/dist/agent-src/rules/delegation-policy.md +10 -8
  34. package/dist/agent-src/rules/design-fidelity.md +4 -0
  35. package/dist/agent-src/rules/git-history-discipline.md +4 -79
  36. package/dist/agent-src/rules/improve-before-implement.md +7 -66
  37. package/dist/agent-src/rules/legal-safety-floor.md +11 -55
  38. package/dist/agent-src/rules/no-roadmap-references.md +13 -77
  39. package/dist/agent-src/rules/persona-governance.md +5 -47
  40. package/dist/agent-src/rules/provider-lifecycle-discipline.md +6 -32
  41. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +21 -104
  42. package/dist/agent-src/rules/roadmap-progress-sync.md +5 -56
  43. package/dist/agent-src/rules/think-before-action.md +10 -0
  44. package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
  45. package/dist/agent-src/rules/verify-before-complete.md +12 -0
  46. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +96 -12
  47. package/dist/agent-src/skills/ai-council/SKILL.md +19 -9
  48. package/dist/agent-src/skills/api-design/SKILL.md +7 -5
  49. package/dist/agent-src/skills/api-testing/SKILL.md +1 -1
  50. package/dist/agent-src/skills/authz-review/SKILL.md +59 -2
  51. package/dist/agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
  52. package/dist/agent-src/skills/bug-analyzer/SKILL.md +19 -16
  53. package/dist/agent-src/skills/code-refactoring/SKILL.md +9 -9
  54. package/dist/agent-src/skills/content-funnel-design/SKILL.md +2 -0
  55. package/dist/agent-src/skills/data-flow-mapper/SKILL.md +1 -1
  56. package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
  57. package/dist/agent-src/skills/defense-in-depth/SKILL.md +2 -2
  58. package/dist/agent-src/skills/dependency-upgrade/SKILL.md +7 -7
  59. package/dist/agent-src/skills/design-intelligence/SKILL.md +8 -8
  60. package/dist/agent-src/skills/design-review/SKILL.md +23 -2
  61. package/dist/agent-src/skills/doc-coauthoring/SKILL.md +2 -0
  62. package/dist/agent-src/skills/editorial-calendar/SKILL.md +2 -0
  63. package/dist/agent-src/skills/fe-design/SKILL.md +7 -9
  64. package/dist/agent-src/skills/git-workflow/SKILL.md +86 -0
  65. package/dist/agent-src/skills/humanizer/SKILL.md +147 -0
  66. package/dist/agent-src/skills/humanizer/data/patterns.md +190 -0
  67. package/dist/agent-src/skills/humanizer/evals/triggers.json +17 -0
  68. package/dist/agent-src/skills/laravel/SKILL.md +14 -1
  69. package/dist/agent-src/skills/launch-readiness/SKILL.md +20 -19
  70. package/dist/agent-src/skills/legal-practice-profile/SKILL.md +126 -0
  71. package/dist/agent-src/skills/logging-monitoring/SKILL.md +1 -1
  72. package/dist/agent-src/skills/mcp/SKILL.md +1 -1
  73. package/dist/agent-src/skills/memory-consolidation/SKILL.md +14 -1
  74. package/dist/agent-src/skills/migration-architect/SKILL.md +2 -1
  75. package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
  76. package/dist/agent-src/skills/performance/SKILL.md +1 -1
  77. package/dist/agent-src/skills/php-debugging/SKILL.md +17 -0
  78. package/dist/agent-src/skills/readme-writing/SKILL.md +2 -0
  79. package/dist/agent-src/skills/readme-writing-package/SKILL.md +2 -0
  80. package/dist/agent-src/skills/release-comms/SKILL.md +2 -0
  81. package/dist/agent-src/skills/roadmap-writing/SKILL.md +22 -51
  82. package/dist/agent-src/skills/security-audit/SKILL.md +6 -9
  83. package/dist/agent-src/skills/sentry-integration/SKILL.md +2 -2
  84. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +8 -0
  85. package/dist/agent-src/skills/source-discovery/SKILL.md +26 -24
  86. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +55 -46
  87. package/dist/agent-src/skills/subagent-orchestration/evals/evals.json +42 -0
  88. package/dist/agent-src/skills/systematic-debugging/SKILL.md +10 -0
  89. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +10 -0
  90. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +27 -20
  91. package/dist/agent-src/skills/worktree-lifecycle/evals/evals.json +38 -0
  92. package/dist/agent-src/templates/agent-settings.md +3 -2
  93. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +15 -1
  94. package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
  95. package/dist/agent-src/templates/roadmaps.md +14 -0
  96. package/dist/cli/commands/recordTriggerEval.js +4 -14
  97. package/dist/cli/commands/recordTriggerEval.js.map +1 -1
  98. package/dist/discovery/deprecation-report.md +1 -1
  99. package/dist/discovery/discovery-manifest.json +157 -88
  100. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  101. package/dist/discovery/discovery-manifest.summary.md +6 -6
  102. package/dist/discovery/orphan-report.md +1 -1
  103. package/dist/discovery/packs.json +13 -10
  104. package/dist/discovery/trust-report.md +3 -3
  105. package/dist/discovery/workspaces.json +14 -9
  106. package/dist/install/install.mjs +12 -7
  107. package/dist/mcp/registry-manifest.json +4 -4
  108. package/dist/router.json +1 -1
  109. package/dist/scripts/_lib/claude_settings_hooks.js +8 -0
  110. package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
  111. package/dist/scripts/_lib/trigger_eval_floors.js +24 -0
  112. package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -0
  113. package/dist/scripts/condense.js +12 -2
  114. package/dist/scripts/condense.js.map +1 -1
  115. package/dist/server/schemas/settings.js +5 -2
  116. package/dist/server/schemas/settings.js.map +1 -1
  117. package/docs/CLAIMS.md +54 -5
  118. package/docs/architecture.md +4 -4
  119. package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
  120. package/docs/benchmark.md +90 -0
  121. package/docs/catalog.md +8 -5
  122. package/docs/command-flows.md +2 -1
  123. package/docs/contracts/ai-council-config.md +97 -0
  124. package/docs/contracts/command-clusters.md +1 -0
  125. package/docs/contracts/evidence-based-pruning.md +1 -0
  126. package/docs/contracts/hook-architecture-v1.md +25 -0
  127. package/docs/contracts/kernel-membership.md +35 -0
  128. package/docs/contracts/measurement-baseline.md +1 -0
  129. package/docs/contracts/persona-schema.md +49 -0
  130. package/docs/contracts/provider-lifecycle.md +29 -4
  131. package/docs/contracts/release-sizing.md +22 -0
  132. package/docs/contracts/write-engine.md +38 -1
  133. package/docs/customization.md +12 -0
  134. package/docs/decisions/ADR-002-kernel-bucket-overrides.md +11 -0
  135. package/docs/decisions/ADR-003-flat-cluster-subs-and-colon-syntax.md +11 -0
  136. package/docs/decisions/ADR-006-skill-tools-python-pilot.md +1 -1
  137. package/docs/decisions/ADR-028-root-layout.md +1 -1
  138. package/docs/decisions/ADR-038-canonical-settings-path.md +10 -0
  139. package/docs/decisions/ADR-092-defer-command-tier-alias-removal.md +29 -5
  140. package/docs/decisions/ADR-093-ai-council-config-user-global.md +1 -1
  141. package/docs/decisions/ADR-100-global-knowledge-card-sharing.md +7 -0
  142. package/docs/decisions/ADR-103-global-knowledge-default-off-until-measured.md +2 -2
  143. package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +1 -1
  144. package/docs/decisions/ADR-114-commit-policy-iron-law-override.md +11 -0
  145. package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +11 -1
  146. package/docs/decisions/ADR-117-subagents-auto-default-on.md +2 -1
  147. package/docs/decisions/ADR-118-loop-engineering-boundaries.md +162 -0
  148. package/docs/decisions/ADR-119-global-knowledge-default-on.md +104 -0
  149. package/docs/decisions/ADR-120-council-chairman-mode.md +70 -0
  150. package/docs/decisions/ADR-121-knowledge-sensitivity-classes.md +90 -0
  151. package/docs/decisions/ADR-200-python-to-typescript-migration.md +11 -1
  152. package/docs/decisions/ADR-rule-kernel-and-router.md +11 -0
  153. package/docs/decisions/INDEX.md +13 -9
  154. package/docs/design/council-vs-solo-baseline.md +92 -0
  155. package/docs/design/cross-model-parity-eval.md +82 -0
  156. package/docs/design/length-neutral-judge-rerun.md +92 -0
  157. package/docs/design/utilization-window-criteria.md +51 -0
  158. package/docs/featured-skills.md +2 -2
  159. package/docs/getting-started-by-role.md +1 -1
  160. package/docs/getting-started.md +2 -2
  161. package/docs/governance-advantage.md +1 -1
  162. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
  163. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +80 -5
  164. package/docs/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
  165. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +80 -3
  166. package/docs/guidelines/code-clarity.md +191 -1
  167. package/docs/guidelines/design-antipatterns.md +20 -0
  168. package/docs/guidelines/php/php-coding-patterns.md +12 -2
  169. package/docs/maintainers/branch-protection.md +87 -0
  170. package/docs/maintainers/system-map.md +85 -0
  171. package/docs/proof.md +32 -15
  172. package/docs/wedge/production-validator/production-validator.md +27 -0
  173. package/package.json +1 -1
  174. package/src/config/agent-settings.template.yml +38 -9
  175. package/src/scripts/_cli/cmd_doctor.ts +129 -1
  176. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +87 -0
  177. package/src/scripts/_lib/claude_settings_hooks.ts +8 -0
  178. package/src/scripts/_lib/knowledge_global.ts +50 -2
  179. package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
  180. package/src/scripts/_lib/knowledge_global_redaction.ts +42 -4
  181. package/src/scripts/_lib/lexical_index.ts +15 -2
  182. package/src/scripts/_lib/orchestration_record.ts +14 -0
  183. package/src/scripts/_lib/trigger_eval_floors.ts +31 -0
  184. package/src/scripts/ai_council/chairman.ts +90 -0
  185. package/src/scripts/ai_council/clients.ts +18 -3
  186. package/src/scripts/ai_council/config.ts +157 -0
  187. package/src/scripts/ai_council/debate_gates.ts +89 -0
  188. package/src/scripts/ai_council/orchestrator.ts +252 -2
  189. package/src/scripts/ai_council/prompts.ts +121 -11
  190. package/src/scripts/ai_council/replay.ts +17 -0
  191. package/src/scripts/ai_council/stance_tally.ts +184 -0
  192. package/src/scripts/ai_tells_rules.ts +433 -0
  193. package/src/scripts/bench_humanizer_eval.ts +240 -0
  194. package/src/scripts/bench_persona_placebo.ts +308 -0
  195. package/src/scripts/bench_quality_rerun.ts +357 -0
  196. package/src/scripts/build_discovery_manifest.ts +6 -0
  197. package/src/scripts/build_proof.ts +30 -9
  198. package/src/scripts/check_artefact_count_messaging.ts +56 -0
  199. package/src/scripts/check_claims.ts +40 -0
  200. package/src/scripts/check_knowledge_cards.ts +33 -4
  201. package/src/scripts/check_portability.ts +1 -0
  202. package/src/scripts/check_rule_invariants.ts +10 -3
  203. package/src/scripts/complexity_report.ts +776 -0
  204. package/src/scripts/condense.ts +16 -2
  205. package/src/scripts/consumer_matrix.ts +292 -14
  206. package/src/scripts/council_cli.ts +203 -1
  207. package/src/scripts/detect_ai_tells.ts +312 -0
  208. package/src/scripts/explain_run.ts +593 -0
  209. package/src/scripts/install.ts +12 -0
  210. package/src/scripts/knowledge_global_cli.ts +235 -46
  211. package/src/scripts/lint_framework_leakage.ts +88 -7
  212. package/src/scripts/lint_framework_leakage_allowlist.json +59 -572
  213. package/src/scripts/lint_knowledge_scale.ts +30 -0
  214. package/src/scripts/lint_originality_shingles.ts +210 -0
  215. package/src/scripts/orchestration_record.ts +1 -0
  216. package/src/scripts/rule_backlinks.ts +0 -0
  217. package/src/scripts/schemas/agent-settings.schema.json +11 -0
  218. package/src/scripts/self_review_gate.ts +41 -15
  219. package/src/scripts/skill_eval_coverage.ts +71 -3
  220. package/src/scripts/skill_linter.ts +1 -0
  221. package/src/scripts/trigger_eval_rotation.ts +344 -0
  222. package/src/scripts/update_counts.ts +5 -17
  223. package/src/scripts/worktree_cleanup_check.ts +263 -0
  224. package/src/server/schemas/settings.ts +8 -3
  225. package/src/scripts/analysis_freshness.ts +0 -362
  226. package/src/scripts/export_replay_corpus.ts +0 -208
@@ -34,6 +34,7 @@
34
34
  */
35
35
  import { spawnSync } from 'node:child_process';
36
36
  import * as fs from 'node:fs';
37
+ import * as os from 'node:os';
37
38
  import * as path from 'node:path';
38
39
  import process from 'node:process';
39
40
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -71,7 +72,7 @@ function _list_cards(): string[] {
71
72
  return _globMdSorted(store).filter((p) => path.basename(p).toLowerCase() !== 'readme.md');
72
73
  }
73
74
 
74
- function _freshness_state(text: string, cfg: SettingsDict): string {
75
+ export function _freshness_state(text: string, cfg: SettingsDict): string {
75
76
  const fresh = _isPlainObject(cfg['freshness']) ? (cfg['freshness'] as SettingsDict) : {};
76
77
  const hyp = _pyInt(fresh['hypothesis_after_days'], 90);
77
78
  const stale = _pyInt(fresh['stale_after_days'], 180);
@@ -105,10 +106,13 @@ function _disabled_notice(): number {
105
106
  // Subcommands
106
107
  // ---------------------------------------------------------------------------
107
108
 
108
- function cmd_list(args: { json: boolean }): number {
109
+ function cmd_list(args: { json: boolean; revoked: boolean }): number {
109
110
  if (!kg.is_enabled()) {
110
111
  return _disabled_notice();
111
112
  }
113
+ if (args.revoked) {
114
+ return _list_revoked(args.json);
115
+ }
112
116
  const cfg = kg.load_global_sharing_config();
113
117
  const cards = _list_cards();
114
118
  const rows: Record<string, string>[] = [];
@@ -142,6 +146,24 @@ function cmd_list(args: { json: boolean }): number {
142
146
  return 0;
143
147
  }
144
148
 
149
+ /** Render the revocation ledger (`list --revoked`) — table or `--json`. */
150
+ function _list_revoked(json: boolean): number {
151
+ const trail = kgp.load_tombstones();
152
+ if (json) {
153
+ process.stdout.write(kg.pyJsonDumps(trail, 2, true) + '\n');
154
+ return 0;
155
+ }
156
+ if (trail.length === 0) {
157
+ process.stdout.write(`No revocations recorded in ${kg.global_store_dir()}.\n`);
158
+ return 0;
159
+ }
160
+ process.stdout.write(`${_ljust('REVOKED-AT', 12)} ${_ljust('CARD', 28)} REASON\n`);
161
+ for (const t of trail) {
162
+ process.stdout.write(`${_ljust(t.revoked_at, 12)} ${_ljust(t.card_id, 28)} ${t.reason}\n`);
163
+ }
164
+ return 0;
165
+ }
166
+
145
167
  function cmd_show(args: { card: string }): number {
146
168
  if (!kg.is_enabled()) {
147
169
  return _disabled_notice();
@@ -185,37 +207,45 @@ function cmd_trace(args: { card: string; json: boolean }): number {
185
207
  return 0;
186
208
  }
187
209
 
188
- function _forget_one(card: string): boolean {
210
+ /**
211
+ * Remove one global card + its usage entry. Writes an append-only tombstone
212
+ * to the revocation ledger BEFORE deleting anything — no silent deletes.
213
+ */
214
+ function _forget_one(card: string, reason: string): boolean {
189
215
  const p = _card_path(card);
190
- let removed = false;
191
- if (fs.existsSync(p)) {
192
- fs.unlinkSync(p);
193
- removed = true;
194
- }
195
216
  const cid = kgp.card_id_from({ card_name: card });
196
217
  const usage = kgp.load_usage();
197
218
  const cards = usage['cards'] as SettingsDict;
198
- if (cid in cards) {
219
+ const has_card = fs.existsSync(p);
220
+ const has_usage = cid in cards;
221
+ if (!has_card && !has_usage) {
222
+ return false;
223
+ }
224
+ kgp.append_tombstone(cid, reason, { today: _today() });
225
+ if (has_card) {
226
+ fs.unlinkSync(p);
227
+ }
228
+ if (has_usage) {
199
229
  delete cards[cid];
200
230
  write_atomic(
201
231
  path.join(kg.global_store_dir(null, { create: true }), kgp.USAGE_FILENAME),
202
232
  kg.pyJsonDumps(usage, 2, true) + '\n',
203
233
  );
204
- removed = true;
205
234
  }
206
- return removed;
235
+ return true;
207
236
  }
208
237
 
209
- function cmd_forget(args: { card: string; tier: string }): number {
238
+ function cmd_forget(args: { card: string; tier: string; reason: string }): number {
210
239
  if (!kg.is_enabled()) {
211
240
  return _disabled_notice();
212
241
  }
213
242
  if (args.tier) {
243
+ const reason = args.reason || `bulk forget (tier=${args.tier})`;
214
244
  let removed = 0;
215
245
  for (const p of _list_cards()) {
216
246
  const prov = kg.parse_provenance_footer(_readReplace(p));
217
247
  if (prov['tier'] === args.tier) {
218
- if (_forget_one(_stem(p))) {
248
+ if (_forget_one(_stem(p), reason)) {
219
249
  removed += 1;
220
250
  }
221
251
  }
@@ -228,7 +258,7 @@ function cmd_forget(args: { card: string; tier: string }): number {
228
258
  process.exitCode = 1;
229
259
  return 1;
230
260
  }
231
- if (_forget_one(args.card)) {
261
+ if (_forget_one(args.card, args.reason || 'manual forget')) {
232
262
  process.stdout.write(`Forgot global card '${args.card}'.\n`);
233
263
  return 0;
234
264
  }
@@ -237,7 +267,16 @@ function cmd_forget(args: { card: string; tier: string }): number {
237
267
  return 1;
238
268
  }
239
269
 
240
- function cmd_promote(args: { path: string; source: string; tier: string; manual: boolean }): number {
270
+ function cmd_promote(args: {
271
+ path: string;
272
+ source: string;
273
+ tier: string;
274
+ manual: boolean;
275
+ sensitivity: string;
276
+ reason: string;
277
+ owner: string;
278
+ review_after: string;
279
+ }): number {
241
280
  if (!kg.is_enabled()) {
242
281
  return _disabled_notice();
243
282
  }
@@ -258,13 +297,20 @@ function cmd_promote(args: { path: string; source: string; tier: string; manual:
258
297
  const allowed = [...kg.allowed_tiers()];
259
298
 
260
299
  const redaction = _isPlainObject(cfg['redaction']) ? (cfg['redaction'] as SettingsDict) : {};
300
+ const redaction_enabled = _pyBool(redaction['enabled'], true);
301
+ const halt_on_trigger = _pyBool(redaction['halt_on_trigger'], true);
302
+ // Computed once — feeds BOTH the existing tier gate below AND the new
303
+ // sensitivity auto-derivation, independent of `halt_on_trigger` (a
304
+ // `prohibited` override never depends on that unrelated config flag).
305
+ const violations = redaction_enabled ? kgr.redaction_scan(text) : [];
306
+
261
307
  const result = kgr.gate_card_for_global(text, {
262
308
  tier,
263
309
  source: args.source,
264
310
  card_name: path.basename(src_path),
265
311
  allowed_tiers: allowed,
266
- redaction_enabled: _pyBool(redaction['enabled'], true),
267
- halt_on_trigger: _pyBool(redaction['halt_on_trigger'], true),
312
+ redaction_enabled,
313
+ halt_on_trigger,
268
314
  });
269
315
  if (!result.eligible) {
270
316
  if (result.manual_only && !args.manual) {
@@ -282,8 +328,7 @@ function cmd_promote(args: { path: string; source: string; tier: string; manual:
282
328
  return 2;
283
329
  }
284
330
  // manual override of proprietary: still run redaction, never skip it.
285
- const violations = kgr.redaction_scan(text);
286
- if (violations.length > 0 && _pyBool(redaction['halt_on_trigger'], true)) {
331
+ if (violations.length > 0 && halt_on_trigger) {
287
332
  process.stderr.write(
288
333
  'global-share BLOCKED (manual proprietary): redaction halt — ' +
289
334
  violations.map((v) => `${v.category}: ${_pyRepr(v.snippet)}`).join('; ') +
@@ -294,6 +339,19 @@ function cmd_promote(args: { path: string; source: string; tier: string; manual:
294
339
  }
295
340
  }
296
341
 
342
+ // Sensitivity gate — layered ON TOP of the tier/redaction gate above.
343
+ const declared_sensitivity =
344
+ args.sensitivity || _frontmatter_sensitivity(text) || kg.DEFAULT_SENSITIVITY;
345
+ const sens = kgp.gate_sensitivity_for_promotion(declared_sensitivity, {
346
+ violations_present: violations.length > 0,
347
+ promotion_reason: args.reason,
348
+ });
349
+ if (!sens.eligible) {
350
+ process.stderr.write(`global-share BLOCKED: ${sens.reason}\n`);
351
+ process.exitCode = 2;
352
+ return 2;
353
+ }
354
+
297
355
  // Build provenance + usage.
298
356
  const cid = kgp.card_id_from({ card_name: path.basename(src_path), source: args.source });
299
357
  const slug = kgp.repo_slug();
@@ -306,6 +364,11 @@ function cmd_promote(args: { path: string; source: string; tier: string; manual:
306
364
  ? (entry['first_seen'] as SettingsDict)
307
365
  : { repo: slug, date: today };
308
366
 
367
+ const fresh = _isPlainObject(cfg['freshness']) ? (cfg['freshness'] as SettingsDict) : {};
368
+ const review_after =
369
+ args.review_after || _addDaysIso(today, _pyInt(fresh['stale_after_days'], 180));
370
+ const owner = args.owner || _defaultOwner();
371
+
309
372
  const footer = kg.render_provenance_footer({
310
373
  first_seen_repo: (first['repo'] as string) ?? slug,
311
374
  first_seen_date: (first['date'] as string) ?? today,
@@ -313,8 +376,13 @@ function cmd_promote(args: { path: string; source: string; tier: string; manual:
313
376
  last_verified: today,
314
377
  tier,
315
378
  seen_in,
379
+ source_repo: slug,
380
+ owner,
381
+ review_after,
382
+ promotion_reason: args.reason,
316
383
  });
317
- let out_text = _ensure_tier_frontmatter(text, tier);
384
+ let out_text = _ensure_frontmatter_field(text, 'tier', tier);
385
+ out_text = _ensure_frontmatter_field(out_text, 'sensitivity', sens.sensitivity);
318
386
  out_text = _rstrip(kg.strip_provenance_footer(out_text)) + '\n\n' + footer;
319
387
  const dest = _card_path(cid);
320
388
  write_atomic(path.join(kg.global_store_dir(null, { create: true }), path.basename(dest)), out_text);
@@ -329,11 +397,11 @@ function cmd_promote(args: { path: string; source: string; tier: string; manual:
329
397
  kg.pyJsonDumps(usage, 2, true) + '\n',
330
398
  );
331
399
  }
332
- process.stdout.write(`Promoted '${cid}' (tier=${tier}) → ${dest}\n`);
400
+ process.stdout.write(`Promoted '${cid}' (tier=${tier}, sensitivity=${sens.sensitivity}) → ${dest}\n`);
333
401
  return 0;
334
402
  }
335
403
 
336
- function cmd_purge(args: { confirm: boolean }): number {
404
+ function cmd_purge(args: { confirm: boolean; reason: string }): number {
337
405
  // Remove the global store and strip provenance footers from project cards.
338
406
  if (!args.confirm) {
339
407
  process.stderr.write(
@@ -345,9 +413,30 @@ function cmd_purge(args: { confirm: boolean }): number {
345
413
  }
346
414
 
347
415
  const store = kg.global_store_dir();
416
+ const reason = args.reason || 'full store purge';
417
+ if (fs.existsSync(store)) {
418
+ // Tombstone every surviving card_id BEFORE deleting anything — no
419
+ // silent deletes, even on the biggest deletion event this CLI has.
420
+ const usage = kgp.load_usage();
421
+ const cards = usage['cards'] as SettingsDict;
422
+ const seen_ids = new Set<string>();
423
+ for (const p of _list_cards()) {
424
+ const cid = _stem(p);
425
+ seen_ids.add(cid);
426
+ kgp.append_tombstone(cid, reason, { today: _today() });
427
+ }
428
+ for (const cid of Object.keys(cards)) {
429
+ if (!seen_ids.has(cid)) {
430
+ kgp.append_tombstone(cid, reason, { today: _today() });
431
+ }
432
+ }
433
+ }
348
434
  let removed = 0;
349
435
  if (fs.existsSync(store)) {
350
436
  for (const p of _globAllSorted(store)) {
437
+ if (path.basename(p) === kgp.REVOCATIONS_FILENAME) {
438
+ continue; // the ledger survives its own purge entry — the audit trail persists
439
+ }
351
440
  if (_isFile(p)) {
352
441
  fs.unlinkSync(p);
353
442
  removed += 1;
@@ -356,7 +445,7 @@ function cmd_purge(args: { confirm: boolean }): number {
356
445
  try {
357
446
  fs.rmdirSync(store);
358
447
  } catch {
359
- // non-empty (subdirs) — leave it
448
+ // non-empty (the ledger, or subdirs) — leave it
360
449
  }
361
450
  }
362
451
  process.stdout.write(`Purged global store (${removed} file(s)) at ${store}.\n`);
@@ -511,32 +600,43 @@ function cmd_lead_check(args: { report: string; strict: boolean }): number {
511
600
  // ---------------------------------------------------------------------------
512
601
 
513
602
  function _frontmatter_tier(text: string): string {
603
+ const val = _frontmatter_scalar(text, 'tier');
604
+ return kg.TIERS.includes(val) ? val : '';
605
+ }
606
+
607
+ function _frontmatter_sensitivity(text: string): string {
608
+ const val = _frontmatter_scalar(text, 'sensitivity');
609
+ return kg.SENSITIVITIES.includes(val) ? val : '';
610
+ }
611
+
612
+ /** Read a top-level scalar frontmatter field's raw (quote-stripped) value. */
613
+ function _frontmatter_scalar(text: string, field: string): string {
514
614
  const m = _FRONTMATTER_RE.exec(text);
515
615
  if (!m) {
516
616
  return '';
517
617
  }
518
618
  for (const line of _splitlines(m[1] as string)) {
519
619
  const s = _strip(line);
520
- if (s.startsWith('tier:')) {
521
- const val = _stripQuotes(_strip(s.slice('tier:'.length)));
522
- return kg.TIERS.includes(val) ? val : '';
620
+ if (s.startsWith(`${field}:`)) {
621
+ return _stripQuotes(_strip(s.slice(field.length + 1)));
523
622
  }
524
623
  }
525
624
  return '';
526
625
  }
527
626
 
528
- function _ensure_tier_frontmatter(text: string, tier: string): string {
529
- // Ensure the card frontmatter carries an accurate `tier:` field.
627
+ /** Ensure the card frontmatter carries an accurate `<field>: <value>` line. */
628
+ function _ensure_frontmatter_field(text: string, field: string, value: string): string {
530
629
  const m = _FRONTMATTER_RE.exec(text);
531
630
  if (!m) {
532
631
  return text;
533
632
  }
534
633
  const block = m[1] as string;
634
+ const line_re = new RegExp(`^\\s*${field}:.*$`, 'm');
535
635
  let new_block: string;
536
- if (/^\s*tier:/m.test(block)) {
537
- new_block = _reSubFirst(block, /^\s*tier:.*$/m, `tier: ${tier}`);
636
+ if (line_re.test(block)) {
637
+ new_block = _reSubFirst(block, line_re, `${field}: ${value}`);
538
638
  } else {
539
- new_block = `tier: ${tier}\n${block}`;
639
+ new_block = `${field}: ${value}\n${block}`;
540
640
  }
541
641
  // m.index of group 1: in Python `m.start(1)` / `m.end(1)`. The full match
542
642
  // starts at 0 (anchored `^`); group 1 begins after the leading `---\n`.
@@ -612,20 +712,23 @@ function _wrap<T>(cmd: string, rest: string[], parse: (rest: string[]) => T | nu
612
712
 
613
713
  // Per-subcommand argparse-faithful parsers.
614
714
 
615
- function _parseList(rest: string[]): { json: boolean } | number {
715
+ function _parseList(rest: string[]): { json: boolean; revoked: boolean } | number {
616
716
  let json = false;
617
- const usage = `${_PROG} list [-h] [--json]`;
717
+ let revoked = false;
718
+ const usage = `${_PROG} list [-h] [--json] [--revoked]`;
618
719
  for (const a of rest) {
619
720
  if (a === '-h' || a === '--help') {
620
721
  return _subHelp(usage);
621
722
  }
622
723
  if (a === '--json') {
623
724
  json = true;
725
+ } else if (a === '--revoked') {
726
+ revoked = true;
624
727
  } else {
625
728
  return _subError('list', `unrecognized arguments: ${a}`, usage);
626
729
  }
627
730
  }
628
- return { json };
731
+ return { json, revoked };
629
732
  }
630
733
 
631
734
  function _parseShow(rest: string[]): { card: string } | number {
@@ -674,10 +777,11 @@ function _parseTrace(rest: string[]): { card: string; json: boolean } | number {
674
777
  return { card, json };
675
778
  }
676
779
 
677
- function _parseForget(rest: string[]): { card: string; tier: string } | number {
678
- const usage = `${_PROG} forget [-h] [--tier {public,vendor,proprietary}] [card]`;
780
+ function _parseForget(rest: string[]): { card: string; tier: string; reason: string } | number {
781
+ const usage = `${_PROG} forget [-h] [--tier {public,vendor,proprietary}] [--reason REASON] [card]`;
679
782
  let card = '';
680
783
  let tier = '';
784
+ let reason = '';
681
785
  let cardSet = false;
682
786
  for (let i = 0; i < rest.length; i += 1) {
683
787
  const a = rest[i] as string;
@@ -689,6 +793,10 @@ function _parseForget(rest: string[]): { card: string; tier: string } | number {
689
793
  tier = choice;
690
794
  } else if (a.startsWith('--tier=')) {
691
795
  tier = _choiceOrExit(a.slice('--tier='.length), ['public', 'vendor', 'proprietary'], '--tier', 'forget', usage);
796
+ } else if (a === '--reason') {
797
+ reason = (rest[++i] ?? '') as string;
798
+ } else if (a.startsWith('--reason=')) {
799
+ reason = a.slice('--reason='.length);
692
800
  } else if (a.startsWith('-')) {
693
801
  return _subError('forget', `unrecognized arguments: ${a}`, usage);
694
802
  } else if (!cardSet) {
@@ -698,15 +806,35 @@ function _parseForget(rest: string[]): { card: string; tier: string } | number {
698
806
  return _subError('forget', `unrecognized arguments: ${a}`, usage);
699
807
  }
700
808
  }
701
- return { card, tier };
702
- }
703
-
704
- function _parsePromote(rest: string[]): { path: string; source: string; tier: string; manual: boolean } | number {
705
- const usage = `${_PROG} promote [-h] [--source SOURCE] [--tier {public,vendor,proprietary}] [--manual] path`;
809
+ return { card, tier, reason };
810
+ }
811
+
812
+ function _parsePromote(
813
+ rest: string[],
814
+ ):
815
+ | {
816
+ path: string;
817
+ source: string;
818
+ tier: string;
819
+ manual: boolean;
820
+ sensitivity: string;
821
+ reason: string;
822
+ owner: string;
823
+ review_after: string;
824
+ }
825
+ | number {
826
+ const usage =
827
+ `${_PROG} promote [-h] [--source SOURCE] [--tier {public,vendor,proprietary}] ` +
828
+ '[--manual] [--sensitivity {prohibited,project,shareable}] [--reason REASON] ' +
829
+ '[--owner OWNER] [--review-after DATE] path';
706
830
  let p: string | null = null;
707
831
  let source = '';
708
832
  let tier = '';
709
833
  let manual = false;
834
+ let sensitivity = '';
835
+ let reason = '';
836
+ let owner = '';
837
+ let review_after = '';
710
838
  for (let i = 0; i < rest.length; i += 1) {
711
839
  const a = rest[i] as string;
712
840
  if (a === '-h' || a === '--help') {
@@ -722,6 +850,34 @@ function _parsePromote(rest: string[]): { path: string; source: string; tier: st
722
850
  tier = _choiceOrExit(a.slice('--tier='.length), ['public', 'vendor', 'proprietary'], '--tier', 'promote', usage);
723
851
  } else if (a === '--manual') {
724
852
  manual = true;
853
+ } else if (a === '--sensitivity') {
854
+ sensitivity = _choiceOrExit(
855
+ rest[++i],
856
+ ['prohibited', 'project', 'shareable'],
857
+ '--sensitivity',
858
+ 'promote',
859
+ usage,
860
+ );
861
+ } else if (a.startsWith('--sensitivity=')) {
862
+ sensitivity = _choiceOrExit(
863
+ a.slice('--sensitivity='.length),
864
+ ['prohibited', 'project', 'shareable'],
865
+ '--sensitivity',
866
+ 'promote',
867
+ usage,
868
+ );
869
+ } else if (a === '--reason') {
870
+ reason = (rest[++i] ?? '') as string;
871
+ } else if (a.startsWith('--reason=')) {
872
+ reason = a.slice('--reason='.length);
873
+ } else if (a === '--owner') {
874
+ owner = (rest[++i] ?? '') as string;
875
+ } else if (a.startsWith('--owner=')) {
876
+ owner = a.slice('--owner='.length);
877
+ } else if (a === '--review-after') {
878
+ review_after = (rest[++i] ?? '') as string;
879
+ } else if (a.startsWith('--review-after=')) {
880
+ review_after = a.slice('--review-after='.length);
725
881
  } else if (a.startsWith('-')) {
726
882
  return _subError('promote', `unrecognized arguments: ${a}`, usage);
727
883
  } else if (p === null) {
@@ -733,7 +889,7 @@ function _parsePromote(rest: string[]): { path: string; source: string; tier: st
733
889
  if (p === null) {
734
890
  return _subError('promote', 'the following arguments are required: path', usage);
735
891
  }
736
- return { path: p, source, tier, manual };
892
+ return { path: p, source, tier, manual, sensitivity, reason, owner, review_after };
737
893
  }
738
894
 
739
895
  function _parseValidate(rest: string[]): { check_urls: boolean } | number {
@@ -774,20 +930,26 @@ function _parseLeadCheck(rest: string[]): { report: string; strict: boolean } |
774
930
  return { report, strict };
775
931
  }
776
932
 
777
- function _parsePurge(rest: string[]): { confirm: boolean } | number {
778
- const usage = `${_PROG} purge [-h] [--confirm]`;
933
+ function _parsePurge(rest: string[]): { confirm: boolean; reason: string } | number {
934
+ const usage = `${_PROG} purge [-h] [--confirm] [--reason REASON]`;
779
935
  let confirm = false;
780
- for (const a of rest) {
936
+ let reason = '';
937
+ for (let i = 0; i < rest.length; i += 1) {
938
+ const a = rest[i] as string;
781
939
  if (a === '-h' || a === '--help') {
782
940
  return _subHelp(usage);
783
941
  }
784
942
  if (a === '--confirm') {
785
943
  confirm = true;
944
+ } else if (a === '--reason') {
945
+ reason = (rest[++i] ?? '') as string;
946
+ } else if (a.startsWith('--reason=')) {
947
+ reason = a.slice('--reason='.length);
786
948
  } else {
787
949
  return _subError('purge', `unrecognized arguments: ${a}`, usage);
788
950
  }
789
951
  }
790
- return { confirm };
952
+ return { confirm, reason };
791
953
  }
792
954
 
793
955
  function _subHelp(usage: string): number {
@@ -1077,6 +1239,33 @@ function _dateDiffDays(a: YMD, b: YMD): number {
1077
1239
  return Math.round((ta - tb) / 86_400_000);
1078
1240
  }
1079
1241
 
1242
+ /** `YYYY-MM-DD` + N days → `YYYY-MM-DD` (UTC). Invalid input → the input unchanged. */
1243
+ function _addDaysIso(iso: string, days: number): string {
1244
+ const parsed = _strptime(iso);
1245
+ if (parsed === null) {
1246
+ return iso;
1247
+ }
1248
+ const ms = Date.UTC(parsed.y, parsed.m - 1, parsed.d) + days * 86_400_000;
1249
+ const d = new Date(ms);
1250
+ const y = d.getUTCFullYear();
1251
+ const mo = String(d.getUTCMonth() + 1).padStart(2, '0');
1252
+ const dd = String(d.getUTCDate()).padStart(2, '0');
1253
+ return `${y}-${mo}-${dd}`;
1254
+ }
1255
+
1256
+ /** Default `owner` for a promoted card when `--owner` is not given. */
1257
+ function _defaultOwner(): string {
1258
+ try {
1259
+ const name = os.userInfo().username;
1260
+ if (name) {
1261
+ return name;
1262
+ }
1263
+ } catch {
1264
+ // platform without a resolvable user (rare) — fall through
1265
+ }
1266
+ return process.env['USER'] ?? process.env['USERNAME'] ?? 'unknown';
1267
+ }
1268
+
1080
1269
  /** Mirror Python `OSError`'s `str(exc)` for the read-failure message. */
1081
1270
  function _osErrorStr(exc: unknown): string {
1082
1271
  if (exc && typeof exc === 'object') {
@@ -128,7 +128,7 @@ const LEAKAGE: Record<string, string[]> = {
128
128
  ],
129
129
  'JS-specific': [
130
130
  '\\bpackage\\.json\\b',
131
- '\\bnpm (install|run|test|ci)\\b',
131
+ '\\bnpm (install|run|test|ci|update|audit|exec)\\b',
132
132
  '\\byarn (install|add|test)\\b',
133
133
  '\\bpnpm (install|add|run|test)\\b',
134
134
  '\\bnode_modules\\b',
@@ -147,20 +147,69 @@ const FAMILY: Record<string, string> = {
147
147
  const CROSS_STACK_HINTS: Record<string, string[]> = {
148
148
  ruby: ['\\bRails\\b', '\\bbin/rails\\b', '\\bGemfile\\b', '\\bbundle exec\\b'],
149
149
  python: ['\\bDjango\\b', '\\bFastAPI\\b', '\\bFlask\\b', '\\bpoetry\\b',
150
- '\\buv (add|sync|run|pip)\\b', '\\bvenv\\b'],
150
+ '\\buv (add|sync|run|pip)\\b', '\\bvenv\\b', '\\bPydantic\\b', '\\bJinja\\b',
151
+ 'docs\\.python\\.org'],
151
152
  node: ['\\bExpress\\b', '\\bNext\\.?js\\b', '\\bNode\\.?js\\b', '\\bnpx\\b',
152
- '\\bvitest\\b', '\\bjest\\b', '\\beslint\\b', '\\bprettier\\b'],
153
- go: ['\\bgo (test|build|run|mod)\\b', '\\bgolangci-lint\\b', '\\bGoLand\\b'],
153
+ '\\bvitest\\b', '\\bjest\\b', '\\beslint\\b', '\\bprettier\\b',
154
+ '\\bReact\\b', '\\bVue\\b', '\\bNestJS\\b', '\\bTypeScript\\b',
155
+ '\\btsc\\b', '\\bzod\\b', '\\bPrisma\\b', '\\bTailwind\\b',
156
+ '\\bTurborepo\\b', '\\bclass-validator\\b', '\\bInertia\\b',
157
+ '\\bJSX\\b', '\\bNx\\b',
158
+ '\\.tsx?\\b', 'typescriptlang\\.org'],
159
+ go: ['\\bgo (test|build|run|mod)\\b', '\\bgolangci-lint\\b', '\\bGoLand\\b',
160
+ '\\bGolang\\b', '\\bgo\\.dev\\b'],
154
161
  rust: ['\\bcargo (test|build|run|check|fmt|clippy|add|update)\\b',
155
- '\\bClippy\\b', '\\brustfmt\\b', '\\bCargo\\.toml\\b'],
162
+ '\\bClippy\\b', '\\brustfmt\\b', '\\bCargo\\.toml\\b',
163
+ 'rust-lang\\.org'],
156
164
  dotnet: ['\\bdotnet (test|build|run|add|restore)\\b', '\\b\\.NET\\b'],
157
165
  java: ['\\bSpring\\b', '\\bmvn (test|clean|install|package)\\b',
158
166
  '\\bgradle\\b', '\\bMaven\\b'],
167
+ // Additional hint vocabulary for the PHP family — the LEAKAGE patterns
168
+ // are hit patterns; these are window-only hints so a JS/Python hit next
169
+ // to plain-language PHP mentions self-suppresses as cross-stack docs.
170
+ php: ['\\bvendor/', '\\bPHP\\b'],
171
+ // Polyglot-runner pseudo-family: a Taskfile/Makefile mention marks the
172
+ // window as multi-stack tooling documentation (runner files are
173
+ // ecosystem-neutral by definition).
174
+ 'polyglot-runner': ['\\bTaskfile(\\.ya?ml)?\\b', '\\bMakefile\\b'],
159
175
  };
176
+ // Case-insensitive on purpose (fix G2): "Prettier" / "ESLint" in prose are
177
+ // the same hint as their lowercase binary names.
160
178
  const CROSS_STACK_RE: Record<string, RegExp> = Object.fromEntries(
161
- Object.entries(CROSS_STACK_HINTS).map(([fam, pats]) => [fam, new RegExp(pats.join('|'))]),
179
+ Object.entries(CROSS_STACK_HINTS).map(([fam, pats]) => [fam, new RegExp(pats.join('|'), 'i')]),
162
180
  );
163
181
 
182
+ // Framework-name pair rule (fix G3): Laravel and Symfony share the `php`
183
+ // family, so a genuine Laravel-vs-Symfony comparison never counted as
184
+ // cross-stack. Two DISTINCT framework names in the window are documentation
185
+ // of alternatives, not a mandate.
186
+ const FRAMEWORK_NAMES: readonly string[] = [
187
+ 'Laravel', 'Symfony', 'Django', 'Rails', 'NestJS', 'Next\\.js', 'Spring',
188
+ 'Flask', 'FastAPI',
189
+ ];
190
+ const FRAMEWORK_NAME_RES: readonly RegExp[] = FRAMEWORK_NAMES.map(
191
+ (n) => new RegExp(`\\b${n}\\b`),
192
+ );
193
+
194
+ // Carve-out-pointer suppression (fix G6): the neutrality rule's own
195
+ // "Allowed: carve-out pointers" shape — a handoff line linking to a
196
+ // framework-specific artifact, or the sanctioned `→ ` pointer prefix.
197
+ const POINTER_LINK_RE = /\]\((?:\.\.\/)+([a-z0-9-]+)\//;
198
+ function _is_carve_out_pointer_line(line: string): boolean {
199
+ const trimmed = line.trimStart();
200
+ if (trimmed.startsWith('→ ') || trimmed.startsWith('→')) {
201
+ return true;
202
+ }
203
+ const m = POINTER_LINK_RE.exec(line);
204
+ return m !== null && CARVE_OUT_RE.test(m[1] as string);
205
+ }
206
+
207
+ // Sanctioned single-stack annotation (fix G5): the corpus-wide
208
+ // "(Laravel shape: …)" / "Laravel example:" marker on a generic mechanism
209
+ // (road-to-configurable-modules Phase D decision).
210
+ const SHAPE_EXAMPLE_RE =
211
+ /\((?:Laravel|Symfony|Django|Rails|Next\.js)[^)]*\b(?:shape|example)\b|\b(?:Laravel|Symfony|Django|Rails) (?:shape|example):/i;
212
+
164
213
  const FRONTMATTER_FRAMEWORK_RE = /^---\s*\n([\s\S]*?)\n---/m;
165
214
  const FRAMEWORK_KEY_RE = /^(?:framework|\s+framework)\s*:\s*(\S+)/m;
166
215
 
@@ -287,6 +336,22 @@ function _families_in_window(lines: string[], idx: number, radius = 10): Set<str
287
336
  return families;
288
337
  }
289
338
 
339
+ /** Fix G3: count DISTINCT framework names in the window (pair rule). */
340
+ function _framework_names_in_window(lines: string[], idx: number, radius = 10): number {
341
+ const lo = Math.max(0, idx - radius);
342
+ const hi = Math.min(lines.length, idx + radius + 1);
343
+ const seen = new Set<number>();
344
+ for (let j = lo; j < hi; j++) {
345
+ const line = lines[j] as string;
346
+ for (let k = 0; k < FRAMEWORK_NAME_RES.length; k++) {
347
+ if (!seen.has(k) && (FRAMEWORK_NAME_RES[k] as RegExp).test(line)) {
348
+ seen.add(k);
349
+ }
350
+ }
351
+ }
352
+ return seen.size;
353
+ }
354
+
290
355
  export interface Hit {
291
356
  line: number;
292
357
  category: string;
@@ -310,13 +375,23 @@ export function scan_file(p: string): Hit[] {
310
375
  for (let i = 0; i < lines.length; i++) {
311
376
  const line = lines[i] as string;
312
377
  if (rx.test(line)) {
378
+ // Sanctioned shapes suppress the hit outright (fixes G5/G6):
379
+ // a carve-out pointer line or a "(Laravel shape/example)"
380
+ // annotation on a generic mechanism is the neutrality
381
+ // rule's OWN allowed form, not leakage.
382
+ if (_is_carve_out_pointer_line(line) || SHAPE_EXAMPLE_RE.test(line)) {
383
+ continue;
384
+ }
313
385
  const families = _families_in_window(lines, i);
386
+ const cross =
387
+ families.size >= 2 ||
388
+ _framework_names_in_window(lines, i) >= 2;
314
389
  hits.push({
315
390
  line: i + 1,
316
391
  category,
317
392
  pattern: pat,
318
393
  snippet: line.trim().slice(0, 160),
319
- cross_stack: families.size >= 2,
394
+ cross_stack: cross,
320
395
  });
321
396
  }
322
397
  }
@@ -426,6 +501,12 @@ export function main(argv?: readonly string[]): number {
426
501
  if (has_framework_frontmatter(f)) {
427
502
  continue;
428
503
  }
504
+ // Self-exemption: the neutrality rule itself enumerates the forbidden
505
+ // tokens (frontmatter triggers + fix table) — it can never be clean
506
+ // by construction.
507
+ if (f.endsWith(`${path.sep}framework-neutrality-in-generic-skills.md`)) {
508
+ continue;
509
+ }
429
510
  const rel = relToRepo(f);
430
511
  const raw_hits = scan_file(f);
431
512
  if (raw_hits.length === 0) {