@event4u/agent-config 9.27.0 → 9.29.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 (220) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +13 -12
  3. package/CHANGELOG.md +165 -0
  4. package/README.md +1 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/analyze/conformance.md +24 -0
  7. package/dist/agent-src/commands/cost/report.md +2 -2
  8. package/dist/agent-src/commands/team/adversarial.md +8 -7
  9. package/dist/agent-src/commands/team/delegate.md +13 -12
  10. package/dist/agent-src/commands/team/review.md +9 -8
  11. package/dist/agent-src/commands/team/status.md +9 -8
  12. package/dist/agent-src/commands/team.md +29 -19
  13. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  14. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  15. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  16. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +28 -3
  17. package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
  18. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  19. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  20. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  21. package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
  22. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  23. package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
  24. package/dist/agent-src/rules/context-hygiene.md +3 -1
  25. package/dist/agent-src/rules/council-availability.md +93 -0
  26. package/dist/agent-src/rules/cross-source-consistency.md +3 -0
  27. package/dist/agent-src/rules/delegation-policy.md +38 -10
  28. package/dist/agent-src/rules/design-fidelity.md +3 -0
  29. package/dist/agent-src/rules/evaluator-independence.md +2 -1
  30. package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
  31. package/dist/agent-src/rules/finance-safety-floor.md +1 -1
  32. package/dist/agent-src/rules/git-history-discipline.md +1 -1
  33. package/dist/agent-src/rules/history-discipline.md +3 -1
  34. package/dist/agent-src/rules/legal-safety-floor.md +1 -1
  35. package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
  36. package/dist/agent-src/rules/onboarding-gate.md +3 -1
  37. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
  38. package/dist/agent-src/rules/role-mode-adherence.md +1 -1
  39. package/dist/agent-src/rules/scale-discipline.md +3 -1
  40. package/dist/agent-src/rules/self-repair-loop.md +103 -0
  41. package/dist/agent-src/rules/session-canary.md +2 -1
  42. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  43. package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
  44. package/dist/agent-src/rules/token-budget-discipline.md +4 -2
  45. package/dist/agent-src/rules/tool-safety.md +5 -1
  46. package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
  47. package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  48. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  49. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  50. package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
  51. package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
  52. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  53. package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
  54. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  55. package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
  56. package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
  57. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  58. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
  59. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  60. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  61. package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
  62. package/dist/agent-src/templates/AGENTS.md +8 -12
  63. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  64. package/dist/agent-src/templates/roadmaps.md +23 -0
  65. package/dist/cli/registry.js +7 -0
  66. package/dist/cli/registry.js.map +1 -1
  67. package/dist/cli-delegate/chunk-66OVWGB5.js +270 -0
  68. package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-LCFJIQ4J.js} +89 -7
  69. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  70. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
  71. package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-MP3JT3IU.js} +3 -3
  72. package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-NZ5N2XQL.js} +467 -305
  73. package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-OJM3MBJM.js} +1 -1
  74. package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-RCIA67XN.js} +117 -96
  75. package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
  76. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  77. package/dist/cli-delegate/cmd_brand_status.js +134 -0
  78. package/dist/cli-delegate/cmd_conformance.js +10 -47
  79. package/dist/cli-delegate/cmd_converge.js +3 -3
  80. package/dist/cli-delegate/cmd_doctor.js +10 -6
  81. package/dist/cli-delegate/cmd_explain.js +80 -330
  82. package/dist/cli-delegate/cmd_export.js +6 -6
  83. package/dist/cli-delegate/cmd_fleet.js +3 -3
  84. package/dist/cli-delegate/cmd_handoff.js +3 -3
  85. package/dist/cli-delegate/cmd_mcp_available.js +433 -0
  86. package/dist/cli-delegate/cmd_migrate.js +3 -3
  87. package/dist/cli-delegate/cmd_packs_active.js +153 -0
  88. package/dist/cli-delegate/cmd_preflight.js +1 -1
  89. package/dist/cli-delegate/cmd_prune.js +6 -6
  90. package/dist/cli-delegate/cmd_route_audit.js +8 -8
  91. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  92. package/dist/cli-delegate/cmd_settings_get.js +266 -0
  93. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  94. package/dist/cli-delegate/cmd_settings_set.js +8 -2968
  95. package/dist/cli-delegate/cmd_sync.js +6 -6
  96. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  97. package/dist/cli-delegate/cmd_update.js +4 -4
  98. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  99. package/dist/cli-delegate/cmd_validate.js +6 -6
  100. package/dist/cli-delegate/cmd_versions.js +4 -4
  101. package/dist/discovery/deprecation-report.md +1 -1
  102. package/dist/discovery/discovery-manifest.json +164 -55
  103. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  104. package/dist/discovery/discovery-manifest.summary.md +4 -3
  105. package/dist/discovery/orphan-report.md +1 -1
  106. package/dist/discovery/packs.json +44 -5
  107. package/dist/discovery/trust-report.md +11 -11
  108. package/dist/discovery/workspaces.json +16 -6
  109. package/dist/hooks/dispatch.js +9341 -6131
  110. package/dist/install/install.mjs +739 -524
  111. package/dist/mcp/registry-manifest.json +3 -3
  112. package/dist/mcp/server.json +2 -2
  113. package/dist/mcp/server.mjs +777 -281
  114. package/dist/router.json +1 -1
  115. package/dist/scripts/_lib/agent_settings.js +134 -15
  116. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  117. package/dist/scripts/condense.js +147 -2
  118. package/dist/scripts/condense.js.map +1 -1
  119. package/dist/server/schemas/settings.js +6 -7
  120. package/dist/server/schemas/settings.js.map +1 -1
  121. package/dist/shared/settingsCarveOut.js +0 -9
  122. package/dist/shared/settingsCarveOut.js.map +1 -1
  123. package/dist/shared/settingsClasses.js +11 -9
  124. package/dist/shared/settingsClasses.js.map +1 -1
  125. package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
  126. package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
  127. package/dist/ui/index.html +1 -1
  128. package/docs/contracts/settings-classes.md +68 -17
  129. package/docs/guidelines/agent-infra/memory-access.md +14 -3
  130. package/package.json +1 -1
  131. package/src/agent-src/templates/AGENTS.md +8 -12
  132. package/src/config/agent-settings.template.yml +52 -66
  133. package/src/config/budgets.yml +29 -0
  134. package/src/config/discovery/packs.yml +11 -0
  135. package/src/config/discovery/workspaces.yml +1 -1
  136. package/src/config/evaluator-budgets.json +4 -4
  137. package/src/config/gate-coverage.yml +26 -0
  138. package/src/config/gate-violation-baselines.json +3 -3
  139. package/src/scripts/_cli/cmd_brand_status.ts +174 -0
  140. package/src/scripts/_cli/cmd_conformance.ts +14 -42
  141. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  142. package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
  143. package/src/scripts/_cli/cmd_packs_active.ts +225 -0
  144. package/src/scripts/_cli/cmd_settings_get.ts +324 -0
  145. package/src/scripts/_dispatch.bash +57 -0
  146. package/src/scripts/_lib/agent_settings.ts +140 -16
  147. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  148. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  149. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  150. package/src/scripts/_lib/carrier_divergence.ts +135 -0
  151. package/src/scripts/_lib/host_capability.ts +144 -4
  152. package/src/scripts/_lib/judgment_ladder.ts +382 -0
  153. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  154. package/src/scripts/_lib/orchestration_record.ts +37 -0
  155. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  156. package/src/scripts/_lib/prompt_shape.ts +88 -0
  157. package/src/scripts/_lib/review_skipped_record.ts +102 -0
  158. package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
  159. package/src/scripts/_lib/self_repair.ts +489 -0
  160. package/src/scripts/_lib/self_repair_store.ts +128 -0
  161. package/src/scripts/_lib/subagent_capsule.ts +182 -0
  162. package/src/scripts/_lib/subagent_response.ts +19 -0
  163. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  164. package/src/scripts/_lib/subagent_steering.ts +9 -4
  165. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  166. package/src/scripts/_lib/transcript_entry.ts +80 -0
  167. package/src/scripts/_lib/worker_budget.ts +37 -2
  168. package/src/scripts/ai_council/clients.ts +76 -54
  169. package/src/scripts/ai_council/config.ts +94 -6
  170. package/src/scripts/ai_council/handoff.ts +113 -0
  171. package/src/scripts/ai_council/modes.ts +19 -4
  172. package/src/scripts/ai_council/orchestrator.ts +105 -0
  173. package/src/scripts/ai_council/quorum.ts +84 -0
  174. package/src/scripts/ai_council/session.ts +44 -0
  175. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  176. package/src/scripts/ai_team/availability.ts +110 -0
  177. package/src/scripts/ai_team/config.ts +34 -14
  178. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  179. package/src/scripts/bench_critic_protocol.ts +396 -0
  180. package/src/scripts/check_branch_freshness.ts +255 -0
  181. package/src/scripts/check_council_config_location.ts +125 -1
  182. package/src/scripts/check_detector_corpus.ts +202 -0
  183. package/src/scripts/check_hook_bundle_freshness.ts +4 -1
  184. package/src/scripts/check_standing_rule_delivery.ts +314 -0
  185. package/src/scripts/condense.ts +162 -2
  186. package/src/scripts/config/profiles.ts +13 -3
  187. package/src/scripts/conformance_scan.ts +272 -24
  188. package/src/scripts/council_availability_hook.ts +188 -0
  189. package/src/scripts/council_cli.ts +527 -32
  190. package/src/scripts/forensics_report.ts +333 -0
  191. package/src/scripts/hook_manifest.yaml +107 -26
  192. package/src/scripts/hooks/block_no_verify.ts +39 -2
  193. package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
  194. package/src/scripts/hooks/concern_registry.ts +10 -0
  195. package/src/scripts/hooks/delegation_nudge_hook.ts +484 -0
  196. package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
  197. package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
  198. package/src/scripts/install.ts +151 -0
  199. package/src/scripts/language_mirror_hook.ts +30 -37
  200. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  201. package/src/scripts/lint_never_silent.ts +276 -0
  202. package/src/scripts/lint_no_activation_gates.ts +271 -0
  203. package/src/scripts/measure_scope_dedup.ts +19 -13
  204. package/src/scripts/onboarding_gate_hook.ts +40 -1
  205. package/src/scripts/orchestration_record.ts +12 -1
  206. package/src/scripts/preamble_byte_census.ts +9 -20
  207. package/src/scripts/rebuild_hook_bundle.ts +177 -0
  208. package/src/scripts/release.ts +74 -2
  209. package/src/scripts/report_carrier_divergence.ts +364 -0
  210. package/src/scripts/report_skill_activation.ts +257 -0
  211. package/src/scripts/report_skill_obligation_violations.ts +579 -0
  212. package/src/scripts/routing_doctor.ts +95 -39
  213. package/src/scripts/score_skill_selection.ts +19 -5
  214. package/src/scripts/self_repair_cli.ts +470 -0
  215. package/src/scripts/self_repair_hook.ts +241 -0
  216. package/src/scripts/team_events_spike.ts +271 -0
  217. package/src/scripts/team_review_gate_hook.ts +31 -9
  218. package/src/server/schemas/settings.ts +10 -17
  219. package/src/shared/settingsCarveOut.ts +0 -10
  220. package/src/shared/settingsClasses.ts +11 -9
@@ -0,0 +1,489 @@
1
+ /**
2
+ * Self-repair core — the pure half of the "the package fixes its own defects"
3
+ * loop (road-to-self-repair Phase 1).
4
+ *
5
+ * Two intake paths produce the same record shape:
6
+ *
7
+ * - USER-REPORTED — the user says the agent worked wrongly
8
+ * (`user_prompt_submit`).
9
+ * - SELF-DETECTED — a deterministic detector fires over the finished turn
10
+ * (`stop`).
11
+ *
12
+ * A record is a *defect against agent-config*, not against the consumer's
13
+ * project. It carries the class, a short redacted evidence span, and an
14
+ * occurrence count; the agent authors the actual fix, and the one outward
15
+ * step (push + PR, or an issue when a PR is impossible) stays behind the
16
+ * `non-destructive-by-default` Hard Floor.
17
+ *
18
+ * DELIBERATELY NOT HERE: an `attempt → critic → re-attempt` loop. That exact
19
+ * mechanism was built, benchmarked and falsified under ADR-106 (capability
20
+ * Δ = 0, McNemar p = 1.0; council verdict TERMINAL), and the same verdict
21
+ * names the replacement lever — "refining the rules on the failure tail" —
22
+ * which is what a defect record feeds. Detection and reporting are in scope;
23
+ * silently re-running the turn to paper over the miss is not.
24
+ *
25
+ * Everything in this module is pure and clock-injected so the detectors can be
26
+ * unit-tested without a host, a transcript, or a network.
27
+ */
28
+ import { createHash } from 'node:crypto';
29
+
30
+ import { redact_low_impact_entry } from '../ai_council/redact_low_impact_entry.js';
31
+
32
+ /** Where a record came from. */
33
+ export type DefectSource = 'user-reported' | 'self-detected';
34
+
35
+ /** The defect classes a record can carry. One class = one detector. */
36
+ export type DefectClass =
37
+ | 'user-reported'
38
+ | 'council-availability-claim'
39
+ | 'language-mirror';
40
+
41
+ /**
42
+ * Runtime mirror of `DefectClass`, in declaration order. The upstream issue
43
+ * form's dropdown (`.github/ISSUE_TEMPLATE/self_repair_report.yml`) is pinned
44
+ * against this list by test, so adding a detector class without extending the
45
+ * form is a red test, not silent drift.
46
+ */
47
+ export const DEFECT_CLASSES: readonly DefectClass[] = [
48
+ 'user-reported',
49
+ 'council-availability-claim',
50
+ 'language-mirror',
51
+ ];
52
+
53
+ /** How far a record may travel outward. */
54
+ export type EgressRoute = 'pull-request' | 'issue' | 'local-only';
55
+
56
+ export interface DefectFinding {
57
+ defect_class: DefectClass;
58
+ source: DefectSource;
59
+ /** Short span quoted from the offending text — sanitized at capture. */
60
+ evidence: string;
61
+ /** One line naming what the fix should change. */
62
+ suggested_surface: string;
63
+ }
64
+
65
+ export interface DefectRecord extends DefectFinding {
66
+ fingerprint: string;
67
+ first_seen: string;
68
+ last_seen: string;
69
+ occurrences: number;
70
+ status: 'open' | 'released';
71
+ /**
72
+ * Failed egress attempts from the most recent `release` call, sanitized at
73
+ * write time. Local-only bookkeeping: `renderReport` never includes them,
74
+ * so they cannot widen what leaves the machine.
75
+ */
76
+ release_errors?: string[];
77
+ }
78
+
79
+ /** One finished turn, as much of it as a hook can observe. */
80
+ export interface TurnSnapshot {
81
+ /** The user's prompt for this turn. */
82
+ prompt: string;
83
+ /** The assistant's final reply text. */
84
+ reply: string;
85
+ /** Shell commands / tool names invoked during the turn. */
86
+ toolCommands: readonly string[];
87
+ /** The language the turn was pinned to, when the host pinned one. */
88
+ pinnedLanguage?: 'de' | 'en' | null;
89
+ }
90
+
91
+ // ── evidence hygiene ───────────────────────────────────────────────
92
+
93
+ const MAX_EVIDENCE = 160;
94
+ const HOME_PATH_RE = /(\/Users\/|\/home\/|[A-Za-z]:\\Users\\)[^\s/\\]+/g;
95
+ const EMAIL_RE = /[^\s@]+@[^\s@]+\.[^\s@]+/g;
96
+
97
+ /**
98
+ * Capture-time hygiene: drop the two identifier classes that a quoted span
99
+ * realistically carries, collapse whitespace, and cap the length. This is a
100
+ * narrowing pass, never a clearance — `egressBlockedReason` is the gate.
101
+ */
102
+ export function sanitizeEvidence(raw: string): string {
103
+ const flat = raw
104
+ .replace(HOME_PATH_RE, '<home>')
105
+ .replace(EMAIL_RE, '<email>')
106
+ .replace(/\s+/g, ' ')
107
+ .trim();
108
+ return flat.length <= MAX_EVIDENCE ? flat : `${flat.slice(0, MAX_EVIDENCE - 1)}…`;
109
+ }
110
+
111
+ /**
112
+ * Publication gate. Runs the record's own text through the audited privacy
113
+ * floor (`redact_low_impact_entry`) and returns a refusal reason, or null when
114
+ * the record may leave the machine.
115
+ *
116
+ * The redactor refuses rather than rewrites — a rewritten secret is still a
117
+ * leaked secret — so a violation downgrades the record to `local-only` instead
118
+ * of being scrubbed into publishable shape.
119
+ */
120
+ export function egressBlockedReason(record: DefectRecord, repoRoot?: string | null): string | null {
121
+ const result = redact_low_impact_entry(`${record.suggested_surface}\n${record.evidence}`, {
122
+ repoRoot: repoRoot ?? null,
123
+ });
124
+ if (result.ok) {
125
+ return null;
126
+ }
127
+ const kinds = result.violations.map((v) => v.category).join(', ');
128
+ return `privacy floor refused the evidence (${kinds}) — record stays local`;
129
+ }
130
+
131
+ // ── detectors ──────────────────────────────────────────────────────
132
+
133
+ // An agent-directed complaint, not a complaint about the code. Deliberately
134
+ // narrow: a spurious record becomes a spurious PR, so precision beats recall.
135
+ const COMPLAINT_PATTERNS: readonly RegExp[] = [
136
+ // No \b before the alternation: JS \b is ASCII-only, so a leading umlaut
137
+ // ("übersehen") never sits on a word boundary — the corpus gate's
138
+ // near-miss class caught that as a dead alternation branch.
139
+ /\bdu hast\b[^.!?]{0,60}(nicht|falsch|vergessen|ignoriert|übersehen)\b/i,
140
+ /\bdu (ignorierst|übersiehst)\b/i,
141
+ /\b(das|es) (war|ist) (aber )?falsch\b/i,
142
+ /\bhat nicht (richtig|korrekt) (gearbeitet|funktioniert)\b/i,
143
+ /\bnicht richtig gearbeitet\b/i,
144
+ /\byou (didn'?t|did not|failed to|ignored|forgot)\b/i,
145
+ /\bthat'?s (wrong|not right|incorrect)\b/i,
146
+ /\byou (worked|did (it|that)) (wrong|incorrectly)\b/i,
147
+ ];
148
+
149
+ // Phrasings that MATCH a complaint pattern but are not complaints — a curious
150
+ // question or a reassurance. Scoped to the matched span (± a small margin),
151
+ // never the whole prompt: a pleasantry in one clause must not mute a genuine
152
+ // complaint in another (R2 finding #2). These feed the must-not-fire class of
153
+ // the detector corpus gate.
154
+ const COMPLAINT_EXONERATIONS: readonly RegExp[] = [
155
+ /\bdu hast nicht zufällig\b/i,
156
+ /\bhast du nicht zufällig\b/i,
157
+ /\byou didn'?t (need|have) to\b/i,
158
+ /\bit'?s fine\b/i,
159
+ /\b(kein problem|passt schon|alles gut)\b/i,
160
+ // Explicit absolution INSIDE the complaint span, which is why it has to be
161
+ // its own entry rather than relying on a neighbouring pleasantry: in
162
+ // "das ist fine, du hast nichts falsch gemacht" the matched span is
163
+ // `du hast nichts falsch`, and the `it's fine` above sits outside the
164
+ // ± EXONERATION_MARGIN window — correctly, since that margin exists to stop
165
+ // a pleasantry in one clause muting a complaint in another. `nichts falsch`
166
+ // is not a neighbouring pleasantry: it is the negation of the complaint
167
+ // itself. Caught by the detector-corpus gate, which flagged this exact
168
+ // string as a must-not-fire fixture that fired (PR #1231).
169
+ /\bnichts falsch\b/i,
170
+ /\bkein fehler\b/i,
171
+ ];
172
+
173
+ /** How far (chars) an exoneration may sit from the complaint span it excuses. */
174
+ const EXONERATION_MARGIN = 12;
175
+
176
+ function spanExonerated(prompt: string, start: number, end: number): boolean {
177
+ for (const ex of COMPLAINT_EXONERATIONS) {
178
+ const g = new RegExp(ex.source, ex.flags.includes('g') ? ex.flags : `${ex.flags}g`);
179
+ let m: RegExpExecArray | null;
180
+ while ((m = g.exec(prompt)) !== null) {
181
+ const s = m.index;
182
+ const e = m.index + m[0].length;
183
+ if (e >= start - EXONERATION_MARGIN && s <= end + EXONERATION_MARGIN) {
184
+ return true;
185
+ }
186
+ }
187
+ }
188
+ return false;
189
+ }
190
+
191
+ /** Intake path 1 — the user says the agent worked wrongly. */
192
+ export function detectUserReport(prompt: string): DefectFinding | null {
193
+ for (const re of COMPLAINT_PATTERNS) {
194
+ const m = re.exec(prompt);
195
+ if (m !== null && !spanExonerated(prompt, m.index, m.index + m[0].length)) {
196
+ return {
197
+ defect_class: 'user-reported',
198
+ source: 'user-reported',
199
+ evidence: sanitizeEvidence(m[0]),
200
+ suggested_surface:
201
+ 'Analyse the turn the user is objecting to and name the rule, skill, or ' +
202
+ 'gate whose absence allowed it.',
203
+ };
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+
209
+ // The #1218 defect, now with a runtime detector: the claim is only a defect
210
+ // when the resolver was never consulted — running the probe and reporting its
211
+ // answer is correct behaviour.
212
+ const COUNCIL_CLAIM_RE =
213
+ /\b(kein|keine|no)\b[^.!?\n]{0,40}\bcouncil\b[^.!?\n]{0,60}\b(konfiguriert|verfügbar|configured|available|infra)\b/i;
214
+ const COUNCIL_PROBE_RE =
215
+ /council[:_](status|estimate|run|cli)|council_cli|AI_COUNCIL_CONFIG/i;
216
+
217
+ /** Detector 1 — "council not configured" asserted without asking the resolver. */
218
+ export function detectCouncilClaim(turn: TurnSnapshot): DefectFinding | null {
219
+ const m = COUNCIL_CLAIM_RE.exec(turn.reply);
220
+ if (m === null) {
221
+ return null;
222
+ }
223
+ if (turn.toolCommands.some((c) => COUNCIL_PROBE_RE.test(c))) {
224
+ return null;
225
+ }
226
+ return {
227
+ defect_class: 'council-availability-claim',
228
+ source: 'self-detected',
229
+ evidence: sanitizeEvidence(m[0]),
230
+ suggested_surface:
231
+ 'Council availability was asserted without running the resolver — see the ' +
232
+ 'council-availability rule; report why its Iron Law did not reach the decision.',
233
+ };
234
+ }
235
+
236
+ // Conservative language check: the Iron Law is about the FIRST token, so only
237
+ // the opening is inspected, and only a one-sided margin fires. A heuristic that
238
+ // fires on a mixed-language paragraph would manufacture defects.
239
+ const DE_MARKERS =
240
+ /\b(der|die|das|und|ich|nicht|ist|ein|eine|für|mit|auf|dass|wird|sind|hat|kann|noch|schon|aber|oder|wenn|habe|beim|vom)\b/gi;
241
+ const EN_MARKERS =
242
+ /\b(the|and|is|are|you|not|for|with|that|this|have|has|can|will|but|or|if|from|there|were)\b/gi;
243
+ const OPENING_CHARS = 240;
244
+
245
+ function countMatches(text: string, re: RegExp): number {
246
+ return (text.match(re) ?? []).length;
247
+ }
248
+
249
+ /** Strip the spans whose language says nothing about the reply's own prose. */
250
+ function proseOpening(reply: string): string {
251
+ return reply
252
+ .replace(/```[\s\S]*?```/g, ' ')
253
+ .replace(/`[^`\n]*`/g, ' ')
254
+ .replace(/https?:\/\/\S+/g, ' ')
255
+ .trim()
256
+ .slice(0, OPENING_CHARS);
257
+ }
258
+
259
+ /** Detector 2 — the reply opens in the language the turn was NOT pinned to. */
260
+ export function detectLanguageMirror(turn: TurnSnapshot): DefectFinding | null {
261
+ const pinned = turn.pinnedLanguage ?? null;
262
+ if (pinned === null) {
263
+ return null;
264
+ }
265
+ const opening = proseOpening(turn.reply);
266
+ if (opening.length < 40) {
267
+ return null;
268
+ }
269
+ const de = countMatches(opening, DE_MARKERS) + (/[äöüßÄÖÜ]/.test(opening) ? 2 : 0);
270
+ const en = countMatches(opening, EN_MARKERS);
271
+ const wrong = pinned === 'de' ? en : de;
272
+ const right = pinned === 'de' ? de : en;
273
+ // One-sided margin: the wrong language is clearly present and the pinned
274
+ // one is entirely absent from the opening.
275
+ if (wrong < 3 || right > 0) {
276
+ return null;
277
+ }
278
+ return {
279
+ defect_class: 'language-mirror',
280
+ source: 'self-detected',
281
+ evidence: sanitizeEvidence(opening.slice(0, 120)),
282
+ suggested_surface:
283
+ `Reply opened in the language the turn was not pinned to (pinned: ${pinned}) — ` +
284
+ 'report which surface carried the pin and why it did not bind.',
285
+ };
286
+ }
287
+
288
+ /** One registered detector — the unit the corpus gate enumerates. */
289
+ export interface RegisteredDetector {
290
+ defect_class: DefectClass;
291
+ source: DefectSource;
292
+ detect: (turn: TurnSnapshot) => DefectFinding | null;
293
+ }
294
+
295
+ /**
296
+ * The detector registry — the single list both `runDetectors` and the corpus
297
+ * gate (`tests/scripts/self_repair_detector_corpus.test.ts`) iterate. A
298
+ * detector added here without its three fixture classes (fire /
299
+ * near-miss-fire / must-not-fire) is a red test, not a silently untested
300
+ * code path.
301
+ */
302
+ export const DETECTOR_REGISTRY: readonly RegisteredDetector[] = [
303
+ {
304
+ defect_class: 'user-reported',
305
+ source: 'user-reported',
306
+ detect: (turn) => detectUserReport(turn.prompt),
307
+ },
308
+ { defect_class: 'council-availability-claim', source: 'self-detected', detect: detectCouncilClaim },
309
+ { defect_class: 'language-mirror', source: 'self-detected', detect: detectLanguageMirror },
310
+ ];
311
+
312
+ /** Run every self-detected detector over a finished turn. */
313
+ export function runDetectors(turn: TurnSnapshot): DefectFinding[] {
314
+ const out: DefectFinding[] = [];
315
+ for (const d of DETECTOR_REGISTRY) {
316
+ if (d.source !== 'self-detected') {
317
+ continue;
318
+ }
319
+ const f = d.detect(turn);
320
+ if (f !== null) {
321
+ out.push(f);
322
+ }
323
+ }
324
+ return out;
325
+ }
326
+
327
+ // ── record identity ────────────────────────────────────────────────
328
+
329
+ /**
330
+ * Fingerprint = class + shape of the evidence, never its incidentals. Digits,
331
+ * quoted spans and punctuation are normalized away so the same defect seen
332
+ * with a different name or count folds into one record instead of flooding
333
+ * the queue with near-duplicates.
334
+ */
335
+ export function fingerprint(defect_class: DefectClass, evidence: string): string {
336
+ const shape = evidence
337
+ .toLowerCase()
338
+ .replace(/\d+/g, '#')
339
+ .replace(/[`"'‚'„"»«]/g, '')
340
+ .replace(/[^a-zà-ÿ#\s]/g, ' ')
341
+ .replace(/\s+/g, ' ')
342
+ .trim();
343
+ return createHash('sha256').update(`${defect_class}|${shape}`).digest('hex').slice(0, 16);
344
+ }
345
+
346
+ /** Fold a finding into an existing record, or open a new one. */
347
+ export function mergeRecord(
348
+ existing: DefectRecord | null,
349
+ finding: DefectFinding,
350
+ now: string,
351
+ ): DefectRecord {
352
+ const fp = fingerprint(finding.defect_class, finding.evidence);
353
+ if (existing !== null && existing.fingerprint === fp) {
354
+ return {
355
+ ...existing,
356
+ last_seen: now,
357
+ occurrences: existing.occurrences + 1,
358
+ // A record the user re-reports after a release is open again.
359
+ status: 'open',
360
+ };
361
+ }
362
+ return {
363
+ ...finding,
364
+ fingerprint: fp,
365
+ first_seen: now,
366
+ last_seen: now,
367
+ occurrences: 1,
368
+ status: 'open',
369
+ };
370
+ }
371
+
372
+ // ── egress ─────────────────────────────────────────────────────────
373
+
374
+ export interface EgressCapability {
375
+ /** An agent-config source checkout the fix can be authored in. */
376
+ hasAgentConfigCheckout: boolean;
377
+ /** `gh` is installed AND authenticated. */
378
+ ghAuthenticated: boolean;
379
+ /**
380
+ * The authenticated user has push permission on the UPSTREAM repo — a
381
+ * pre-flight probe of actual rights (`gh api repos/<repo>` →
382
+ * `.permissions.push`), never an inference from `git remote` existing.
383
+ * A consumer who cloned the public repo has a remote and no write access;
384
+ * the old heuristic classified exactly that machine as push-capable.
385
+ */
386
+ canPushUpstream: boolean;
387
+ /** The upstream repo allows forking and the user is authenticated. */
388
+ canFork: boolean;
389
+ }
390
+
391
+ /** Where a pull-request route pushes its branch. */
392
+ export type PushVia = 'upstream' | 'fork';
393
+
394
+ export interface EgressChoice {
395
+ route: EgressRoute;
396
+ /** Set only when `route === 'pull-request'`. */
397
+ pushVia: PushVia | null;
398
+ }
399
+
400
+ /**
401
+ * Route selection, exactly as specified: a pull request when the fix can be
402
+ * authored and pushed (directly, or via fork → cross-repo PR), an issue when
403
+ * it cannot, and a local record when nothing can leave the machine.
404
+ */
405
+ export function chooseEgress(cap: EgressCapability): EgressChoice {
406
+ if (cap.hasAgentConfigCheckout && cap.ghAuthenticated && cap.canPushUpstream) {
407
+ return { route: 'pull-request', pushVia: 'upstream' };
408
+ }
409
+ if (cap.hasAgentConfigCheckout && cap.ghAuthenticated && cap.canFork) {
410
+ return { route: 'pull-request', pushVia: 'fork' };
411
+ }
412
+ if (cap.ghAuthenticated) {
413
+ return { route: 'issue', pushVia: null };
414
+ }
415
+ return { route: 'local-only', pushVia: null };
416
+ }
417
+
418
+ /** The PR / issue body. Deterministic — same record, same bytes. */
419
+ export function renderReport(record: DefectRecord, route: EgressRoute): string {
420
+ const seen =
421
+ record.occurrences === 1
422
+ ? `once (${record.first_seen})`
423
+ : `${record.occurrences}× (${record.first_seen} → ${record.last_seen})`;
424
+ const intake =
425
+ record.source === 'user-reported'
426
+ ? 'A user stated the agent worked wrongly.'
427
+ : 'A deterministic detector fired at turn end; no user complaint was needed.';
428
+ return [
429
+ `## Self-repair report — \`${record.defect_class}\``,
430
+ '',
431
+ intake,
432
+ '',
433
+ `- **Fingerprint:** \`${record.fingerprint}\``,
434
+ `- **Seen:** ${seen}`,
435
+ `- **Route:** ${route}`,
436
+ '',
437
+ '### Evidence',
438
+ '',
439
+ '> ' + record.evidence,
440
+ '',
441
+ '### What the fix should address',
442
+ '',
443
+ record.suggested_surface,
444
+ '',
445
+ '---',
446
+ '',
447
+ 'Filed by the agent-config self-repair loop. The evidence span is capture-time',
448
+ 'sanitized and was cleared by the privacy floor before this report left the',
449
+ 'machine. No project paths, prompts, or file contents are included by',
450
+ 'construction — the record type has no field that can carry them.',
451
+ ].join('\n');
452
+ }
453
+
454
+ /** The structured fields the upstream intake form collects. */
455
+ export interface ParsedReport {
456
+ defect_class: string;
457
+ fingerprint: string;
458
+ occurrences: number;
459
+ evidence: string;
460
+ suggested_surface: string;
461
+ }
462
+
463
+ /**
464
+ * Inverse of `renderReport` for the structured fields — the round-trip that
465
+ * pins the rendered body to the upstream issue form: every field the form
466
+ * collects is recoverable from a rendered report, so a `release` body and a
467
+ * manually-filed form carry the same clustering keys (fingerprint above all).
468
+ */
469
+ export function parseReport(body: string): ParsedReport | null {
470
+ const cls = /^## Self-repair report — `([^`]+)`$/m.exec(body)?.[1];
471
+ const fp = /^- \*\*Fingerprint:\*\* `([^`]+)`$/m.exec(body)?.[1];
472
+ const seen = /^- \*\*Seen:\*\* (.+)$/m.exec(body)?.[1];
473
+ const evidence = /^### Evidence\n\n> (.*)$/m.exec(body)?.[1];
474
+ const surface = /### What the fix should address\n\n([\s\S]*?)\n\n---/.exec(body)?.[1];
475
+ if (!cls || !fp || !seen || evidence === undefined || !surface) {
476
+ return null;
477
+ }
478
+ const occurrences = seen.startsWith('once') ? 1 : Number(/^(\d+)×/.exec(seen)?.[1] ?? Number.NaN);
479
+ if (!Number.isFinite(occurrences)) {
480
+ return null;
481
+ }
482
+ return {
483
+ defect_class: cls,
484
+ fingerprint: fp,
485
+ occurrences,
486
+ evidence,
487
+ suggested_surface: surface.trim(),
488
+ };
489
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Self-repair record store — the I/O half kept out of `self_repair.ts` so the
3
+ * detectors stay pure and testable.
4
+ *
5
+ * Records live under `agents/runtime/self-repair/`, which the repo `.gitignore`
6
+ * already covers via the `/agents/runtime/` catch-all: a defect record is
7
+ * machine-generated local state, never a tracked artefact. One file per
8
+ * fingerprint, so a recurring defect increments a counter instead of adding a
9
+ * queue entry.
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+
14
+ import { type DefectFinding, type DefectRecord, fingerprint, mergeRecord } from './self_repair.js';
15
+
16
+ export const STORE_REL = path.join('agents', 'runtime', 'self-repair');
17
+
18
+ export function storeDir(root: string): string {
19
+ return path.join(root, STORE_REL);
20
+ }
21
+
22
+ function recordPath(root: string, fp: string): string {
23
+ return path.join(storeDir(root), `${fp}.json`);
24
+ }
25
+
26
+ function isRecord(v: unknown): v is DefectRecord {
27
+ if (typeof v !== 'object' || v === null) {
28
+ return false;
29
+ }
30
+ const r = v as Partial<DefectRecord>;
31
+ return typeof r.fingerprint === 'string' && typeof r.defect_class === 'string';
32
+ }
33
+
34
+ /** Read one record, or null when absent / unreadable / malformed. */
35
+ export function readRecord(root: string, fp: string): DefectRecord | null {
36
+ try {
37
+ const parsed: unknown = JSON.parse(fs.readFileSync(recordPath(root, fp), 'utf-8'));
38
+ return isRecord(parsed) ? parsed : null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ export function writeRecord(root: string, record: DefectRecord): void {
45
+ const dir = storeDir(root);
46
+ fs.mkdirSync(dir, { recursive: true });
47
+ fs.writeFileSync(
48
+ recordPath(root, record.fingerprint),
49
+ `${JSON.stringify(record, null, 2)}\n`,
50
+ 'utf-8',
51
+ );
52
+ }
53
+
54
+ /** Every record in the store, newest activity first. */
55
+ export function listRecords(root: string): DefectRecord[] {
56
+ let names: string[];
57
+ try {
58
+ names = fs.readdirSync(storeDir(root));
59
+ } catch {
60
+ return [];
61
+ }
62
+ const out: DefectRecord[] = [];
63
+ for (const n of names.sort()) {
64
+ if (!n.endsWith('.json')) {
65
+ continue;
66
+ }
67
+ const rec = readRecord(root, n.slice(0, -'.json'.length));
68
+ if (rec !== null) {
69
+ out.push(rec);
70
+ }
71
+ }
72
+ out.sort((a, b) => (a.last_seen < b.last_seen ? 1 : a.last_seen > b.last_seen ? -1 : 0));
73
+ return out;
74
+ }
75
+
76
+ export function openRecords(root: string): DefectRecord[] {
77
+ return listRecords(root).filter((r) => r.status === 'open');
78
+ }
79
+
80
+ /**
81
+ * Fold a finding into the store: increments the existing record when the
82
+ * fingerprint matches, otherwise opens a new one. Returns the stored record.
83
+ */
84
+ export function upsertFinding(root: string, finding: DefectFinding, now: string): DefectRecord {
85
+ const fp = fingerprint(finding.defect_class, finding.evidence);
86
+ const merged = mergeRecord(readRecord(root, fp), finding, now);
87
+ writeRecord(root, merged);
88
+ return merged;
89
+ }
90
+
91
+ /** Mark a record released (a PR or issue now carries it). */
92
+ export function markReleased(root: string, fp: string, now: string): DefectRecord | null {
93
+ const rec = readRecord(root, fp);
94
+ if (rec === null) {
95
+ return null;
96
+ }
97
+ // A successful release supersedes any earlier failed attempts.
98
+ const next: DefectRecord = { ...rec, status: 'released', last_seen: now };
99
+ delete next.release_errors;
100
+ writeRecord(root, next);
101
+ return next;
102
+ }
103
+
104
+ /**
105
+ * A release attempt exhausted the whole egress ladder: keep the record open
106
+ * and attach every failed step so the next `self-repair:status` shows what
107
+ * went wrong. Errors arrive pre-sanitized (the CLI runs them through
108
+ * `sanitizeEvidence` — command output can carry local paths).
109
+ */
110
+ export function attachReleaseErrors(
111
+ root: string,
112
+ fp: string,
113
+ errors: string[],
114
+ now: string,
115
+ ): DefectRecord | null {
116
+ const rec = readRecord(root, fp);
117
+ if (rec === null) {
118
+ return null;
119
+ }
120
+ const next: DefectRecord = {
121
+ ...rec,
122
+ status: 'open',
123
+ last_seen: now,
124
+ release_errors: errors,
125
+ };
126
+ writeRecord(root, next);
127
+ return next;
128
+ }