@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +13 -12
- package/CHANGELOG.md +165 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/analyze/conformance.md +24 -0
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +13 -12
- package/dist/agent-src/commands/team/review.md +9 -8
- package/dist/agent-src/commands/team/status.md +9 -8
- package/dist/agent-src/commands/team.md +29 -19
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +28 -3
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
- package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
- package/dist/agent-src/rules/context-hygiene.md +3 -1
- package/dist/agent-src/rules/council-availability.md +93 -0
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/delegation-policy.md +38 -10
- package/dist/agent-src/rules/design-fidelity.md +3 -0
- package/dist/agent-src/rules/evaluator-independence.md +2 -1
- package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
- package/dist/agent-src/rules/finance-safety-floor.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +1 -1
- package/dist/agent-src/rules/history-discipline.md +3 -1
- package/dist/agent-src/rules/legal-safety-floor.md +1 -1
- package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
- package/dist/agent-src/rules/onboarding-gate.md +3 -1
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
- package/dist/agent-src/rules/role-mode-adherence.md +1 -1
- package/dist/agent-src/rules/scale-discipline.md +3 -1
- package/dist/agent-src/rules/self-repair-loop.md +103 -0
- package/dist/agent-src/rules/session-canary.md +2 -1
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
- package/dist/agent-src/rules/token-budget-discipline.md +4 -2
- package/dist/agent-src/rules/tool-safety.md +5 -1
- package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
- package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
- package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
- package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
- package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
- package/dist/agent-src/skills/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
- package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +8 -12
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +23 -0
- package/dist/cli/registry.js +7 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-66OVWGB5.js +270 -0
- package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-LCFJIQ4J.js} +89 -7
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
- package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-MP3JT3IU.js} +3 -3
- package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-NZ5N2XQL.js} +467 -305
- package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-OJM3MBJM.js} +1 -1
- package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-RCIA67XN.js} +117 -96
- package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +134 -0
- package/dist/cli-delegate/cmd_conformance.js +10 -47
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +10 -6
- package/dist/cli-delegate/cmd_explain.js +80 -330
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- package/dist/cli-delegate/cmd_mcp_available.js +433 -0
- package/dist/cli-delegate/cmd_migrate.js +3 -3
- package/dist/cli-delegate/cmd_packs_active.js +153 -0
- package/dist/cli-delegate/cmd_preflight.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_route_audit.js +8 -8
- package/dist/cli-delegate/cmd_route_explain.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +266 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +8 -2968
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +4 -4
- package/dist/cli-delegate/cmd_upgrade.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +164 -55
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +44 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +16 -6
- package/dist/hooks/dispatch.js +9341 -6131
- package/dist/install/install.mjs +739 -524
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +777 -281
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +134 -15
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +147 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +6 -7
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsCarveOut.js +0 -9
- package/dist/shared/settingsCarveOut.js.map +1 -1
- package/dist/shared/settingsClasses.js +11 -9
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
- package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +68 -17
- package/docs/guidelines/agent-infra/memory-access.md +14 -3
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +8 -12
- package/src/config/agent-settings.template.yml +52 -66
- package/src/config/budgets.yml +29 -0
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +26 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_cli/cmd_brand_status.ts +174 -0
- package/src/scripts/_cli/cmd_conformance.ts +14 -42
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
- package/src/scripts/_cli/cmd_packs_active.ts +225 -0
- package/src/scripts/_cli/cmd_settings_get.ts +324 -0
- package/src/scripts/_dispatch.bash +57 -0
- package/src/scripts/_lib/agent_settings.ts +140 -16
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
- package/src/scripts/_lib/auto_dispatch.ts +23 -18
- package/src/scripts/_lib/capsule_trigger.ts +124 -0
- package/src/scripts/_lib/carrier_divergence.ts +135 -0
- package/src/scripts/_lib/host_capability.ts +144 -4
- package/src/scripts/_lib/judgment_ladder.ts +382 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +37 -0
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/prompt_shape.ts +88 -0
- package/src/scripts/_lib/review_skipped_record.ts +102 -0
- package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
- package/src/scripts/_lib/self_repair.ts +489 -0
- package/src/scripts/_lib/self_repair_store.ts +128 -0
- package/src/scripts/_lib/subagent_capsule.ts +182 -0
- package/src/scripts/_lib/subagent_response.ts +19 -0
- package/src/scripts/_lib/subagent_spawn.ts +13 -0
- package/src/scripts/_lib/subagent_steering.ts +9 -4
- package/src/scripts/_lib/tier_budget_routing.ts +6 -1
- package/src/scripts/_lib/transcript_entry.ts +80 -0
- package/src/scripts/_lib/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +76 -54
- package/src/scripts/ai_council/config.ts +94 -6
- package/src/scripts/ai_council/handoff.ts +113 -0
- package/src/scripts/ai_council/modes.ts +19 -4
- package/src/scripts/ai_council/orchestrator.ts +105 -0
- package/src/scripts/ai_council/quorum.ts +84 -0
- package/src/scripts/ai_council/session.ts +44 -0
- package/src/scripts/ai_council/transport_resolver.ts +127 -1
- package/src/scripts/ai_team/availability.ts +110 -0
- package/src/scripts/ai_team/config.ts +34 -14
- package/src/scripts/ai_team/team_dispatch.ts +71 -29
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/check_branch_freshness.ts +255 -0
- package/src/scripts/check_council_config_location.ts +125 -1
- package/src/scripts/check_detector_corpus.ts +202 -0
- package/src/scripts/check_hook_bundle_freshness.ts +4 -1
- package/src/scripts/check_standing_rule_delivery.ts +314 -0
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/config/profiles.ts +13 -3
- package/src/scripts/conformance_scan.ts +272 -24
- package/src/scripts/council_availability_hook.ts +188 -0
- package/src/scripts/council_cli.ts +527 -32
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/hook_manifest.yaml +107 -26
- package/src/scripts/hooks/block_no_verify.ts +39 -2
- package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
- package/src/scripts/hooks/concern_registry.ts +10 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +484 -0
- package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
- package/src/scripts/install.ts +151 -0
- package/src/scripts/language_mirror_hook.ts +30 -37
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/measure_scope_dedup.ts +19 -13
- package/src/scripts/onboarding_gate_hook.ts +40 -1
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/preamble_byte_census.ts +9 -20
- package/src/scripts/rebuild_hook_bundle.ts +177 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/report_carrier_divergence.ts +364 -0
- package/src/scripts/report_skill_activation.ts +257 -0
- package/src/scripts/report_skill_obligation_violations.ts +579 -0
- package/src/scripts/routing_doctor.ts +95 -39
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +470 -0
- package/src/scripts/self_repair_hook.ts +241 -0
- package/src/scripts/team_events_spike.ts +271 -0
- package/src/scripts/team_review_gate_hook.ts +31 -9
- package/src/server/schemas/settings.ts +10 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- 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
|
+
}
|