@event4u/agent-config 9.28.0 → 9.30.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 +6 -6
- package/CHANGELOG.md +172 -5
- package/README.md +2 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/analyze/inbox.md +22 -1
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/explain-run.md +31 -1
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +14 -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 +22 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +17 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +65 -1
- 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/delegation-policy.md +11 -7
- package/dist/agent-src/rules/preservation-guard.md +9 -0
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/token-budget-discipline.md +1 -1
- package/dist/agent-src/scripts/roadmap_gates.ts +312 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +72 -4
- 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/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +17 -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/templates/AGENTS.md +4 -4
- 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 +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
- package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-BTJNRUX3.js} +22 -35
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-DDUNNFYX.js} +24 -17
- package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-EQY4FLJB.js} +3 -3
- package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-IYTGIG2Y.js} +155 -91
- package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/chunk-MMJL4TIX.js +18 -0
- package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
- package/dist/cli-delegate/chunk-O3CO5BFB.js +161 -0
- package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
- package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +2803 -137
- package/dist/cli-delegate/cmd_doctor.js +10 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +88 -8
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +3 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +177 -0
- package/dist/cli-delegate/cmd_settings_get.js +2 -8
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +72 -23
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -2
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +39 -2
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +12 -4
- package/dist/hooks/dispatch.js +2278 -611
- package/dist/install/install.mjs +56 -17
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +4122 -699
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +54 -0
- 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 +12 -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-B5qGYOJo.js +26 -0
- package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +58 -12
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +312 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
- package/src/agent-src/templates/AGENTS.md +4 -4
- package/src/config/agent-settings.template.yml +88 -66
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/dispatch-economy-metrics.json +61 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/hook-token-budget.json +91 -0
- package/src/config/recycle-threshold-budget.json +12 -0
- package/src/config/rule-enforcement-baseline.json +86 -0
- package/src/scripts/_cli/cmd_conformance.ts +31 -44
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
- package/src/scripts/_dispatch.bash +23 -0
- package/src/scripts/_lib/agent_settings.ts +58 -0
- 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/host_capability.ts +63 -6
- package/src/scripts/_lib/judgment_ladder.ts +634 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +78 -10
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
- package/src/scripts/_lib/self_repair.ts +354 -13
- package/src/scripts/_lib/self_repair_store.ts +28 -0
- package/src/scripts/_lib/session_eol.ts +230 -0
- package/src/scripts/_lib/session_role.ts +42 -0
- package/src/scripts/_lib/subagent_capsule.ts +341 -0
- package/src/scripts/_lib/subagent_response.ts +94 -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/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +126 -55
- 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/ask_transport.ts +221 -0
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/bench_hook_injection.ts +234 -0
- package/src/scripts/check_adr_frontmatter.ts +84 -40
- package/src/scripts/check_branch_freshness.ts +1 -0
- package/src/scripts/check_corpus_staleness.ts +18 -4
- package/src/scripts/check_detector_corpus.ts +219 -0
- package/src/scripts/check_gitignore_freshness.ts +42 -8
- package/src/scripts/check_hook_bundle_freshness.ts +24 -1
- package/src/scripts/check_kernel_prefix_stability.ts +1 -0
- package/src/scripts/check_standing_rule_delivery.ts +73 -32
- package/src/scripts/check_static_layer_stability.ts +154 -0
- package/src/scripts/check_suppression_hygiene.ts +48 -0
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/council_cli.ts +401 -25
- package/src/scripts/dispatch_economy_report.ts +448 -0
- package/src/scripts/dispatch_r2_reviewer.ts +15 -2
- package/src/scripts/explain_run.ts +206 -4
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/generate_settings_reference.ts +1 -0
- package/src/scripts/handoff_context_hook.ts +112 -4
- package/src/scripts/hook_manifest.yaml +117 -10
- package/src/scripts/hooks/concern_registry.ts +8 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +106 -35
- package/src/scripts/hooks/dispatch_hook.ts +39 -2
- package/src/scripts/hooks/edit_shape_hook.ts +303 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
- package/src/scripts/hooks/reread_guard_hook.ts +314 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
- package/src/scripts/hooks/session_eol_hook.ts +268 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +507 -0
- package/src/scripts/install-hooks.sh +1 -1
- package/src/scripts/language_mirror_hook.ts +89 -0
- package/src/scripts/lint_decision_impact.ts +14 -1
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_hook_manifest.ts +82 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/lint_readme_serial_comma.ts +1 -0
- package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
- package/src/scripts/measure_turn_end_gate.ts +194 -0
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/rebuild_hook_bundle.ts +178 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/report_conformance_funnel.ts +284 -0
- package/src/scripts/report_imperative_density.ts +1 -0
- package/src/scripts/routing_doctor.ts +118 -36
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +337 -59
- package/src/scripts/session_eol_report.ts +261 -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 +24 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- package/src/shared/settingsClasses.ts +11 -9
- package/src/templates/compact-instructions.md +40 -0
- package/dist/ui/assets/index-DmR0mbk4.js +0 -26
- package/dist/ui/assets/index-DmR0mbk4.js.map +0 -1
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
import type { LookupClass } from './auto_dispatch.js';
|
|
20
20
|
|
|
21
21
|
export type DispatchOutcome = 'DONE' | 'DONE_WITH_CONCERNS' | 'NEEDS_CONTEXT' | 'BLOCKED' | 'killed';
|
|
22
|
-
/** Which route the
|
|
23
|
-
|
|
22
|
+
/** Which route the rung took: deterministic primitive (lean-init L0),
|
|
23
|
+
* single-completion ask (token-economy-dispatch rung 0.5), or a full
|
|
24
|
+
* subagent spawn. */
|
|
25
|
+
export type RouteTaken = 'primitive' | 'subagent' | 'ask';
|
|
24
26
|
export type VerifyMode = 'deterministic' | 'judge' | 'none';
|
|
25
27
|
export type Provenance = 'measured' | 'estimated';
|
|
26
28
|
export type TierChosen = 'lite' | 'medium' | 'high';
|
|
@@ -28,6 +30,8 @@ export type TierSource = 'static' | 'inferred' | 'inherit';
|
|
|
28
30
|
export type Band = 'low' | 'medium' | 'high';
|
|
29
31
|
export type LinePhase = 'refine' | 'memory' | 'analyze' | 'plan' | 'implement' | 'test' | 'verify' | 'report';
|
|
30
32
|
export type LineOutcome = 'success' | 'blocked' | 'skipped' | 'error';
|
|
33
|
+
/** Which capsule-emission trigger arm fired first (Phase 1 shadow comparison). */
|
|
34
|
+
export type TriggerArm = 'watermark' | 'saturation' | 'tie';
|
|
31
35
|
/** The orchestration form the form-gate selected (road-to-opt-subagent-harvest P2). */
|
|
32
36
|
export type DispatchModeId =
|
|
33
37
|
| 'do-and-judge'
|
|
@@ -96,6 +100,37 @@ export interface RecordInput {
|
|
|
96
100
|
/** Rules the worker actually applied/cited (count of the envelope's
|
|
97
101
|
* `rules_applied`-equivalent on the worker side). Counts only. */
|
|
98
102
|
rules_used?: number | null | undefined;
|
|
103
|
+
// ── dispatch-economy additive fields (road-to-token-economy-dispatch
|
|
104
|
+
// Phase 1.1). `init_tokens` above (lean-init) is the spawn-payload
|
|
105
|
+
// half; `work_tokens` is the delta from first worker turn to envelope
|
|
106
|
+
// close. Both are counts; the pair's provenance rides
|
|
107
|
+
// `floor_provenance` exactly like `token_delta_provenance` tags
|
|
108
|
+
// `token_delta`. ──
|
|
109
|
+
/** Tokens the worker consumed AFTER init (delta to envelope close). */
|
|
110
|
+
work_tokens?: number | null | undefined;
|
|
111
|
+
/** Chars the dispatch RETURNED into the orchestrator context (serialized
|
|
112
|
+
* tool-result length on sync completions) — the Phase 6.3 detector for
|
|
113
|
+
* "isolation win refunded through the return channel". A count, never
|
|
114
|
+
* content. */
|
|
115
|
+
return_channel_chars?: number | null | undefined;
|
|
116
|
+
/** Provenance of the init/work pair: 'measured' (transcript ledger via
|
|
117
|
+
* cc_transcript) or 'estimated'. Defaults to 'estimated' when either
|
|
118
|
+
* field is present without a tag. */
|
|
119
|
+
floor_provenance?: Provenance | null | undefined;
|
|
120
|
+
// ── capsule shadow-measurement fields (road-to-worker-generation-recycling
|
|
121
|
+
// Phase 1.2). SHADOW ONLY: the worker still runs to stop-loss, nothing
|
|
122
|
+
// reads a capsule. All counts / enums — a capsule's CONTENT never
|
|
123
|
+
// reaches telemetry, only its shape. ──
|
|
124
|
+
/** A CHECKPOINT capsule was emitted at the watermark. */
|
|
125
|
+
capsule_emitted?: boolean | undefined;
|
|
126
|
+
/** Total entries across the capsule's arrays — a size proxy, never content. */
|
|
127
|
+
capsule_entries?: number | null | undefined;
|
|
128
|
+
/** 1-based step at which the token-watermark arm fired (null = never). */
|
|
129
|
+
watermark_step?: number | null | undefined;
|
|
130
|
+
/** 1-based step at which the novelty-saturation arm fired (null = never). */
|
|
131
|
+
saturation_step?: number | null | undefined;
|
|
132
|
+
/** Which arm fired first on this dispatch — the paired-comparison datum. */
|
|
133
|
+
trigger_arm_earlier?: TriggerArm | null | undefined;
|
|
99
134
|
// Audit-log envelope (sensible defaults for a dispatch record)
|
|
100
135
|
phase?: LinePhase | undefined;
|
|
101
136
|
outcome?: LineOutcome | undefined;
|
|
@@ -122,7 +157,8 @@ const TIER_SOURCES: readonly TierSource[] = ['static', 'inferred', 'inherit'];
|
|
|
122
157
|
const BANDS: readonly Band[] = ['low', 'medium', 'high'];
|
|
123
158
|
const PHASES: readonly LinePhase[] = ['refine', 'memory', 'analyze', 'plan', 'implement', 'test', 'verify', 'report'];
|
|
124
159
|
const LOOKUP_CLASSES: readonly LookupClass[] = ['definition', 'references', 'string-existence', 'report-run'];
|
|
125
|
-
const ROUTES_TAKEN: readonly RouteTaken[] = ['primitive', 'subagent'];
|
|
160
|
+
const ROUTES_TAKEN: readonly RouteTaken[] = ['primitive', 'subagent', 'ask'];
|
|
161
|
+
const TRIGGER_ARMS: readonly TriggerArm[] = ['watermark', 'saturation', 'tie'];
|
|
126
162
|
/** Hex-only payload hash — a hash can never smuggle content (privacy by construction). */
|
|
127
163
|
const PAYLOAD_HASH_RE = /^[a-f0-9]{8,64}$/i;
|
|
128
164
|
/** Id-shaped origin tag — enum-ish, never free-form prose. */
|
|
@@ -147,13 +183,15 @@ function isInt(n: unknown): n is number {
|
|
|
147
183
|
export function buildOrchestrationLine(input: RecordInput): BuiltLine {
|
|
148
184
|
const errors: string[] = [];
|
|
149
185
|
|
|
150
|
-
// A lookup-class primitive route
|
|
151
|
-
// (
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
186
|
+
// A lookup-class primitive route (lean-init L0) and a rung-0.5 ask route
|
|
187
|
+
// (token-economy-dispatch Phase 4) are the TWO recordable zero-spawn
|
|
188
|
+
// events: the routing decision itself is the datum the cost-reduction
|
|
189
|
+
// claim reads. Everything else with spawn_count 0 stays unrecordable
|
|
190
|
+
// in-session work.
|
|
191
|
+
const zeroSpawnRoute =
|
|
192
|
+
(input.route_taken === 'primitive' || input.route_taken === 'ask') && input.spawn_count === 0;
|
|
193
|
+
if (!zeroSpawnRoute && (!isInt(input.spawn_count) || input.spawn_count < 1)) {
|
|
194
|
+
errors.push('spawn_count must be an integer ≥ 1 (0 = handled in-session, not a dispatch — do not record; exception: route_taken=primitive|ask routes record with spawn_count 0)');
|
|
157
195
|
}
|
|
158
196
|
if (!isInt(input.token_delta)) errors.push('token_delta must be an integer (negative = net saved vs the in-session baseline)');
|
|
159
197
|
if (input.task_size_estimate !== undefined && (!isInt(input.task_size_estimate) || input.task_size_estimate < 0)) {
|
|
@@ -222,9 +260,14 @@ export function buildOrchestrationLine(input: RecordInput): BuiltLine {
|
|
|
222
260
|
for (const [key, v] of [
|
|
223
261
|
['rules_carried', input.rules_carried],
|
|
224
262
|
['rules_used', input.rules_used],
|
|
263
|
+
['work_tokens', input.work_tokens],
|
|
264
|
+
['return_channel_chars', input.return_channel_chars],
|
|
225
265
|
] as const) {
|
|
226
266
|
if (v != null && (!isInt(v) || v < 0)) errors.push(`${key} must be a non-negative integer count`);
|
|
227
267
|
}
|
|
268
|
+
if (input.floor_provenance != null && !PROVENANCES.includes(input.floor_provenance)) {
|
|
269
|
+
errors.push(`floor_provenance must be one of ${PROVENANCES.join(' | ')} (or omitted)`);
|
|
270
|
+
}
|
|
228
271
|
if (
|
|
229
272
|
input.rules_carried != null &&
|
|
230
273
|
input.rules_used != null &&
|
|
@@ -233,6 +276,20 @@ export function buildOrchestrationLine(input: RecordInput): BuiltLine {
|
|
|
233
276
|
errors.push('rules_used cannot exceed rules_carried (a worker cannot apply a rule it was not given)');
|
|
234
277
|
}
|
|
235
278
|
|
|
279
|
+
if (input.capsule_emitted !== undefined && typeof input.capsule_emitted !== 'boolean') {
|
|
280
|
+
errors.push('capsule_emitted must be a boolean or omitted');
|
|
281
|
+
}
|
|
282
|
+
for (const [key, v] of [
|
|
283
|
+
['capsule_entries', input.capsule_entries],
|
|
284
|
+
['watermark_step', input.watermark_step],
|
|
285
|
+
['saturation_step', input.saturation_step],
|
|
286
|
+
] as const) {
|
|
287
|
+
if (v != null && (!isInt(v) || v < 0)) errors.push(`${key} must be a non-negative integer count`);
|
|
288
|
+
}
|
|
289
|
+
if (input.trigger_arm_earlier != null && !TRIGGER_ARMS.includes(input.trigger_arm_earlier)) {
|
|
290
|
+
errors.push(`trigger_arm_earlier must be one of ${TRIGGER_ARMS.join(' | ')} (or omitted / null when neither arm fired)`);
|
|
291
|
+
}
|
|
292
|
+
|
|
236
293
|
if (!input.ts) errors.push('ts (ISO-8601 UTC) is required');
|
|
237
294
|
if (!input.id) errors.push('id (ULID or content hash) is required');
|
|
238
295
|
|
|
@@ -267,6 +324,17 @@ export function buildOrchestrationLine(input: RecordInput): BuiltLine {
|
|
|
267
324
|
origin: input.origin ?? null,
|
|
268
325
|
rules_carried: input.rules_carried ?? null,
|
|
269
326
|
rules_used: input.rules_used ?? null,
|
|
327
|
+
// dispatch-economy additive fields — readers ignore unknowns per audit-log-v1
|
|
328
|
+
work_tokens: input.work_tokens ?? null,
|
|
329
|
+
return_channel_chars: input.return_channel_chars ?? null,
|
|
330
|
+
floor_provenance:
|
|
331
|
+
input.floor_provenance ?? (input.init_tokens != null || input.work_tokens != null ? 'estimated' : null),
|
|
332
|
+
// capsule shadow-measurement — readers ignore unknowns per audit-log-v1
|
|
333
|
+
capsule_emitted: input.capsule_emitted ?? null,
|
|
334
|
+
capsule_entries: input.capsule_entries ?? null,
|
|
335
|
+
watermark_step: input.watermark_step ?? null,
|
|
336
|
+
saturation_step: input.saturation_step ?? null,
|
|
337
|
+
trigger_arm_earlier: input.trigger_arm_earlier ?? null,
|
|
270
338
|
};
|
|
271
339
|
|
|
272
340
|
const line: Record<string, unknown> = {
|
|
@@ -212,7 +212,7 @@ export function aggregateOrchestrationSavings(
|
|
|
212
212
|
|
|
213
213
|
// Honest caveats — always attached so a consumer never over-reads the number.
|
|
214
214
|
if (report.dispatches === 0) {
|
|
215
|
-
report.notes.push('No orchestration telemetry yet (0 dispatches). Data accrues as
|
|
215
|
+
report.notes.push('No orchestration telemetry yet (0 dispatches). Data accrues as delegable slices dispatch (always-on orchestration; recorded via orchestration_record).');
|
|
216
216
|
} else {
|
|
217
217
|
report.notes.push('Token counts are MEASURED but blind to the downshift rate win (same tokens on a cheaper tier ≈ same count). The MODELED cost-% below captures that axis.');
|
|
218
218
|
if (report.by_provenance.estimated.dispatches > 0) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recycle-envelope filesystem contract (road-to-token-economy-recycling
|
|
3
|
+
* Phase 2.2/2.3) — the ONE place the producer (`session:recycle`) and the
|
|
4
|
+
* consumer (`handoff_context_hook`) agree on where the envelope lives and
|
|
5
|
+
* when it is stale. A second copy of any of these values is the drift seam
|
|
6
|
+
* Risk 6 warns about.
|
|
7
|
+
*/
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
|
|
10
|
+
/** Where `session:recycle` writes the pending envelope (gitignored runtime state). */
|
|
11
|
+
export const RECYCLE_ENVELOPE_REL = path.join('agents', 'runtime', 'state', 'recycle-envelope.json');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Where the consumer MOVES the envelope on consumption — moved, not copied,
|
|
15
|
+
* so a consumed or rejected envelope can never leak into a later session,
|
|
16
|
+
* while the last one stays inspectable for debugging.
|
|
17
|
+
*/
|
|
18
|
+
export const RECYCLE_CONSUMED_REL = path.join(
|
|
19
|
+
'agents',
|
|
20
|
+
'runtime',
|
|
21
|
+
'state',
|
|
22
|
+
'recycle-envelope.consumed.json',
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
/** Staleness guard — mirrors the handoff file's 48 h discipline. */
|
|
26
|
+
export const RECYCLE_MAX_AGE_HOURS = 48;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Producer-side size cap on the serialized envelope. The schema's per-field
|
|
30
|
+
* caps make a transcript-shaped envelope invalid; this cap keeps even a
|
|
31
|
+
* legal envelope inside the `handoff-context` injection budget
|
|
32
|
+
* (`src/config/hook-token-budget.json`: 8192 B for the concern) with room
|
|
33
|
+
* for the wrapper block and a co-pending handoff.
|
|
34
|
+
*/
|
|
35
|
+
export const RECYCLE_ENVELOPE_MAX_BYTES = 6144;
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { createHash } from 'node:crypto';
|
|
29
29
|
|
|
30
30
|
import { redact_low_impact_entry } from '../ai_council/redact_low_impact_entry.js';
|
|
31
|
+
import { is_kernel_rule } from './kernel_rules.js';
|
|
31
32
|
|
|
32
33
|
/** Where a record came from. */
|
|
33
34
|
export type DefectSource = 'user-reported' | 'self-detected';
|
|
@@ -38,6 +39,18 @@ export type DefectClass =
|
|
|
38
39
|
| 'council-availability-claim'
|
|
39
40
|
| 'language-mirror';
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Runtime mirror of `DefectClass`, in declaration order. The upstream issue
|
|
44
|
+
* form's dropdown (`.github/ISSUE_TEMPLATE/self_repair_report.yml`) is pinned
|
|
45
|
+
* against this list by test, so adding a detector class without extending the
|
|
46
|
+
* form is a red test, not silent drift.
|
|
47
|
+
*/
|
|
48
|
+
export const DEFECT_CLASSES: readonly DefectClass[] = [
|
|
49
|
+
'user-reported',
|
|
50
|
+
'council-availability-claim',
|
|
51
|
+
'language-mirror',
|
|
52
|
+
];
|
|
53
|
+
|
|
41
54
|
/** How far a record may travel outward. */
|
|
42
55
|
export type EgressRoute = 'pull-request' | 'issue' | 'local-only';
|
|
43
56
|
|
|
@@ -56,6 +69,12 @@ export interface DefectRecord extends DefectFinding {
|
|
|
56
69
|
last_seen: string;
|
|
57
70
|
occurrences: number;
|
|
58
71
|
status: 'open' | 'released';
|
|
72
|
+
/**
|
|
73
|
+
* Failed egress attempts from the most recent `release` call, sanitized at
|
|
74
|
+
* write time. Local-only bookkeeping: `renderReport` never includes them,
|
|
75
|
+
* so they cannot widen what leaves the machine.
|
|
76
|
+
*/
|
|
77
|
+
release_errors?: string[];
|
|
59
78
|
}
|
|
60
79
|
|
|
61
80
|
/** One finished turn, as much of it as a hook can observe. */
|
|
@@ -110,12 +129,208 @@ export function egressBlockedReason(record: DefectRecord, repoRoot?: string | nu
|
|
|
110
129
|
return `privacy floor refused the evidence (${kinds}) — record stays local`;
|
|
111
130
|
}
|
|
112
131
|
|
|
132
|
+
// ── may-not-modify deny-list ───────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Surfaces self-repair may never target with a PATCH. Detection and reporting
|
|
136
|
+
* stay untouched — for a record targeting one of these, egress degrades to
|
|
137
|
+
* report-only: an issue (or the local record) is still allowed, the
|
|
138
|
+
* pull-request path refuses with the named reason.
|
|
139
|
+
*
|
|
140
|
+
* The point is structural: the loop that fixes the agent's own defects must
|
|
141
|
+
* not be a channel through which the agent loosens the floors it is bound by.
|
|
142
|
+
* Same posture as `block_kernel_rule_writes` (tool-call-time) and the settings
|
|
143
|
+
* class-C fence (write-time), applied to the one outward vehicle this loop
|
|
144
|
+
* owns. Nothing auto-applies a patch today; this keeps that true for the
|
|
145
|
+
* denied surfaces even when the PR path is otherwise available.
|
|
146
|
+
*/
|
|
147
|
+
export type DeniedSurface =
|
|
148
|
+
| 'kernel-rule'
|
|
149
|
+
| 'safety-floor-rule'
|
|
150
|
+
| 'settings-class-c'
|
|
151
|
+
| 'ci-enforcement'
|
|
152
|
+
| 'self-repair-policy';
|
|
153
|
+
|
|
154
|
+
export interface PatchDenial {
|
|
155
|
+
surface: DeniedSurface;
|
|
156
|
+
/** The token in the record that matched. */
|
|
157
|
+
match: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Characters that decorate a token but never appear inside a real path, rule
|
|
162
|
+
* id, or settings key: markdown emphasis / strikethrough, autolink angle
|
|
163
|
+
* brackets, table pipes. They are removed ANYWHERE in the token, not just at
|
|
164
|
+
* its edges, so `src/rules/**scope-control**.md` reduces like `**…**` does.
|
|
165
|
+
*/
|
|
166
|
+
const DECORATION_RE = /[*~<>|]/g;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Edge punctuation a sentence leaves on a token — sentence-final `.`, a `:`
|
|
170
|
+
* label separator, list/reference brackets, and underscore emphasis (`_` is
|
|
171
|
+
* stripped at the EDGES only: it is load-bearing inside `self_repair.ts`,
|
|
172
|
+
* `hook_manifest.yaml`, and every snake_case settings key).
|
|
173
|
+
*/
|
|
174
|
+
const EDGE_TRIM_RE = /^[.:[\]_]+|[.:[\]_]+$/g;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Word-ish / path-ish tokens of a record's text, punctuation-stripped.
|
|
178
|
+
*
|
|
179
|
+
* `suggested_surface` is free-text the agent writes, and this repo's prose is
|
|
180
|
+
* saturated with `**bold**` file paths — so a tokenizer that only splits on
|
|
181
|
+
* whitespace-and-quotes lets ordinary markdown emphasis smuggle a denied
|
|
182
|
+
* surface past the scan (`**src/rules/scope-control.md**` read as ALLOWED
|
|
183
|
+
* while the same path bare read as DENIED). Both the raw token and its
|
|
184
|
+
* un-decorated form are emitted, and the edge trim runs AFTER the decoration
|
|
185
|
+
* strip so an unwrapped `**.github/workflows/ci.yml**` cannot keep a leading
|
|
186
|
+
* dot in front of the anchored `github/workflows/` match.
|
|
187
|
+
*/
|
|
188
|
+
function denyTokens(text: string): string[] {
|
|
189
|
+
const out: string[] = [];
|
|
190
|
+
for (const raw of text.split(/[\s,;()!?"'`]+/)) {
|
|
191
|
+
if (raw.length === 0) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
for (const candidate of new Set([raw, raw.replace(DECORATION_RE, '')])) {
|
|
195
|
+
const token = candidate.replace(EDGE_TRIM_RE, '');
|
|
196
|
+
if (token.length > 0) {
|
|
197
|
+
out.push(token);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Basename stem (no `.md`) of a path-or-id token. */
|
|
205
|
+
function stemOf(token: string): string {
|
|
206
|
+
const base = token.replace(/\\/g, '/').split('/').pop() ?? token;
|
|
207
|
+
return base.replace(/\.md$/, '');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Dotted settings-key shape — `personal.autonomy`, `tokens.rich_skills`, … */
|
|
211
|
+
const DOTTED_KEY_RE = /^[a-z0-9_]+(\.[a-z0-9_]+)+$/;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The surfaces through which enforcement is actually armed or disarmed. A
|
|
215
|
+
* workflow file is only ONE of them, and in this repo it is not even the usual
|
|
216
|
+
* one — a new gate is registered in the taskfiles the workflow calls, and a
|
|
217
|
+
* tool-call-time guard is armed in the hook manifest. Each entry, and why it
|
|
218
|
+
* arms enforcement:
|
|
219
|
+
*
|
|
220
|
+
* - `.github/workflows/` — the CI job definitions; deleting a step removes
|
|
221
|
+
* the gate from every PR. (The leading dot is stripped by `denyTokens`, so
|
|
222
|
+
* the match is on the segment pair, not the dotfile spelling.)
|
|
223
|
+
* - `Taskfile.yml` / `taskfiles/*.yml` — the task graph the workflows invoke
|
|
224
|
+
* (`task ci`, `task ci-fast`). A gate that is not listed here does not run,
|
|
225
|
+
* however green the workflow looks.
|
|
226
|
+
* - `src/scripts/hook_manifest.yaml` — the concern↔slot bindings. Unbinding
|
|
227
|
+
* a concern disarms a PreToolUse guard without touching the guard's code.
|
|
228
|
+
* - `src/scripts/hooks/` — the guard implementations themselves
|
|
229
|
+
* (`block_no_verify`, `block_kernel_rule_writes`, `block_config_weakening`).
|
|
230
|
+
* - `src/scripts/check_*.ts` / `src/scripts/lint_*.ts` — the gate scripts. A
|
|
231
|
+
* loosened threshold or a narrowed scan root disarms the gate in place,
|
|
232
|
+
* which is exactly the "gates that scan nothing exit green" failure.
|
|
233
|
+
*
|
|
234
|
+
* Deliberately NOT the whole of `src/scripts/`: that would deny nearly every
|
|
235
|
+
* legitimate repair this loop exists to file.
|
|
236
|
+
*/
|
|
237
|
+
function isCiEnforcementPath(token: string): boolean {
|
|
238
|
+
const t = token.replace(/\\/g, '/');
|
|
239
|
+
if (/(^|\/)github\/workflows\//.test(t)) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
if (/(^|\/)Taskfile(\.[a-z0-9_-]+)?\.ya?ml$/i.test(t)) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
if (/(^|\/)taskfiles\//.test(t)) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
if (/(^|\/)hook_manifest\.ya?ml$/.test(t)) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
if (/(^|\/)scripts\/hooks\//.test(t)) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
return /(^|\/)scripts\/(check|lint)_[a-z0-9_]+\.ts$/.test(t);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Is this record's fix aimed at a denied surface? Scans the two fields that
|
|
259
|
+
* name what the fix should change (`suggested_surface`) and what was observed
|
|
260
|
+
* (`evidence`). A match errs toward denial on purpose — the cost of a false
|
|
261
|
+
* positive is a report instead of a patch, still egress; the cost of a false
|
|
262
|
+
* negative is a patch channel into a safety floor.
|
|
263
|
+
*
|
|
264
|
+
* The kernel set resolves from `kernel_rules.ts` — the same canonical source
|
|
265
|
+
* `block_kernel_rule_writes` uses — never a copy that can drift. Settings
|
|
266
|
+
* class-C keys are matched against `classCKeys` when the caller can supply
|
|
267
|
+
* them (parsed from `docs/contracts/settings-classes.md`); the contract file
|
|
268
|
+
* itself is denied by path regardless, so an unreadable contract narrows the
|
|
269
|
+
* dynamic check without opening the fence's own definition to patching.
|
|
270
|
+
*/
|
|
271
|
+
export function patchDeniedSurface(
|
|
272
|
+
record: DefectFinding,
|
|
273
|
+
opts?: { classCKeys?: ReadonlySet<string> | null },
|
|
274
|
+
): PatchDenial | null {
|
|
275
|
+
const classCKeys = opts?.classCKeys ?? null;
|
|
276
|
+
for (const token of denyTokens(`${record.suggested_surface}\n${record.evidence}`)) {
|
|
277
|
+
// Checked before `ci-enforcement` so a self-repair file that also lives
|
|
278
|
+
// under an enforcement path (`src/scripts/hooks/self_repair_hook.ts`)
|
|
279
|
+
// reports the more specific surface. Either way it is denied.
|
|
280
|
+
if (/self[-_]repair/i.test(token)) {
|
|
281
|
+
return { surface: 'self-repair-policy', match: token };
|
|
282
|
+
}
|
|
283
|
+
if (isCiEnforcementPath(token)) {
|
|
284
|
+
return { surface: 'ci-enforcement', match: token };
|
|
285
|
+
}
|
|
286
|
+
if (token.includes('settings-classes.md')) {
|
|
287
|
+
return { surface: 'settings-class-c', match: token };
|
|
288
|
+
}
|
|
289
|
+
if (is_kernel_rule(token)) {
|
|
290
|
+
return { surface: 'kernel-rule', match: token };
|
|
291
|
+
}
|
|
292
|
+
if (stemOf(token).endsWith('-safety-floor')) {
|
|
293
|
+
return { surface: 'safety-floor-rule', match: token };
|
|
294
|
+
}
|
|
295
|
+
if (classCKeys !== null && DOTTED_KEY_RE.test(token)) {
|
|
296
|
+
// Exact key first, then every classified ancestor down to the root
|
|
297
|
+
// segment — a class-C key whose value is a MAP has children that
|
|
298
|
+
// never appear under the key's own name.
|
|
299
|
+
const parts = token.split('.');
|
|
300
|
+
for (let end = parts.length; end > 0; end--) {
|
|
301
|
+
if (classCKeys.has(parts.slice(0, end).join('.'))) {
|
|
302
|
+
return { surface: 'settings-class-c', match: token };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** The named refusal the patch/PR path reports. Null = patch path allowed. */
|
|
311
|
+
export function patchDeniedReason(
|
|
312
|
+
record: DefectFinding,
|
|
313
|
+
opts?: { classCKeys?: ReadonlySet<string> | null },
|
|
314
|
+
): string | null {
|
|
315
|
+
const denial = patchDeniedSurface(record, opts);
|
|
316
|
+
if (denial === null) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
return (
|
|
320
|
+
`self-repair may not patch a ${denial.surface} surface (matched: ${denial.match}) — ` +
|
|
321
|
+
'the pull-request path is refused; the record egresses report-only (issue / local record)'
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
113
325
|
// ── detectors ──────────────────────────────────────────────────────
|
|
114
326
|
|
|
115
327
|
// An agent-directed complaint, not a complaint about the code. Deliberately
|
|
116
328
|
// narrow: a spurious record becomes a spurious PR, so precision beats recall.
|
|
117
329
|
const COMPLAINT_PATTERNS: readonly RegExp[] = [
|
|
118
|
-
|
|
330
|
+
// No \b before the alternation: JS \b is ASCII-only, so a leading umlaut
|
|
331
|
+
// ("übersehen") never sits on a word boundary — the corpus gate's
|
|
332
|
+
// near-miss class caught that as a dead alternation branch.
|
|
333
|
+
/\bdu hast\b[^.!?]{0,60}(nicht|falsch|vergessen|ignoriert|übersehen)\b/i,
|
|
119
334
|
/\bdu (ignorierst|übersiehst)\b/i,
|
|
120
335
|
/\b(das|es) (war|ist) (aber )?falsch\b/i,
|
|
121
336
|
/\bhat nicht (richtig|korrekt) (gearbeitet|funktioniert)\b/i,
|
|
@@ -125,11 +340,53 @@ const COMPLAINT_PATTERNS: readonly RegExp[] = [
|
|
|
125
340
|
/\byou (worked|did (it|that)) (wrong|incorrectly)\b/i,
|
|
126
341
|
];
|
|
127
342
|
|
|
343
|
+
// Phrasings that MATCH a complaint pattern but are not complaints — a curious
|
|
344
|
+
// question or a reassurance. Scoped to the matched span (± a small margin),
|
|
345
|
+
// never the whole prompt: a pleasantry in one clause must not mute a genuine
|
|
346
|
+
// complaint in another (R2 finding #2). These feed the must-not-fire class of
|
|
347
|
+
// the detector corpus gate.
|
|
348
|
+
const COMPLAINT_EXONERATIONS: readonly RegExp[] = [
|
|
349
|
+
/\bdu hast nicht zufällig\b/i,
|
|
350
|
+
/\bhast du nicht zufällig\b/i,
|
|
351
|
+
/\byou didn'?t (need|have) to\b/i,
|
|
352
|
+
/\bit'?s fine\b/i,
|
|
353
|
+
/\b(kein problem|passt schon|alles gut)\b/i,
|
|
354
|
+
// Explicit absolution INSIDE the complaint span, which is why it has to be
|
|
355
|
+
// its own entry rather than relying on a neighbouring pleasantry: in
|
|
356
|
+
// "das ist fine, du hast nichts falsch gemacht" the matched span is
|
|
357
|
+
// `du hast nichts falsch`, and the `it's fine` above sits outside the
|
|
358
|
+
// ± EXONERATION_MARGIN window — correctly, since that margin exists to stop
|
|
359
|
+
// a pleasantry in one clause muting a complaint in another. `nichts falsch`
|
|
360
|
+
// is not a neighbouring pleasantry: it is the negation of the complaint
|
|
361
|
+
// itself. Caught by the detector-corpus gate, which flagged this exact
|
|
362
|
+
// string as a must-not-fire fixture that fired (PR #1231).
|
|
363
|
+
/\bnichts falsch\b/i,
|
|
364
|
+
/\bkein fehler\b/i,
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
/** How far (chars) an exoneration may sit from the complaint span it excuses. */
|
|
368
|
+
const EXONERATION_MARGIN = 12;
|
|
369
|
+
|
|
370
|
+
function spanExonerated(prompt: string, start: number, end: number): boolean {
|
|
371
|
+
for (const ex of COMPLAINT_EXONERATIONS) {
|
|
372
|
+
const g = new RegExp(ex.source, ex.flags.includes('g') ? ex.flags : `${ex.flags}g`);
|
|
373
|
+
let m: RegExpExecArray | null;
|
|
374
|
+
while ((m = g.exec(prompt)) !== null) {
|
|
375
|
+
const s = m.index;
|
|
376
|
+
const e = m.index + m[0].length;
|
|
377
|
+
if (e >= start - EXONERATION_MARGIN && s <= end + EXONERATION_MARGIN) {
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
128
385
|
/** Intake path 1 — the user says the agent worked wrongly. */
|
|
129
386
|
export function detectUserReport(prompt: string): DefectFinding | null {
|
|
130
387
|
for (const re of COMPLAINT_PATTERNS) {
|
|
131
388
|
const m = re.exec(prompt);
|
|
132
|
-
if (m !== null) {
|
|
389
|
+
if (m !== null && !spanExonerated(prompt, m.index, m.index + m[0].length)) {
|
|
133
390
|
return {
|
|
134
391
|
defect_class: 'user-reported',
|
|
135
392
|
source: 'user-reported',
|
|
@@ -222,11 +479,38 @@ export function detectLanguageMirror(turn: TurnSnapshot): DefectFinding | null {
|
|
|
222
479
|
};
|
|
223
480
|
}
|
|
224
481
|
|
|
482
|
+
/** One registered detector — the unit the corpus gate enumerates. */
|
|
483
|
+
export interface RegisteredDetector {
|
|
484
|
+
defect_class: DefectClass;
|
|
485
|
+
source: DefectSource;
|
|
486
|
+
detect: (turn: TurnSnapshot) => DefectFinding | null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* The detector registry — the single list both `runDetectors` and the corpus
|
|
491
|
+
* gate (`tests/scripts/self_repair_detector_corpus.test.ts`) iterate. A
|
|
492
|
+
* detector added here without its three fixture classes (fire /
|
|
493
|
+
* near-miss-fire / must-not-fire) is a red test, not a silently untested
|
|
494
|
+
* code path.
|
|
495
|
+
*/
|
|
496
|
+
export const DETECTOR_REGISTRY: readonly RegisteredDetector[] = [
|
|
497
|
+
{
|
|
498
|
+
defect_class: 'user-reported',
|
|
499
|
+
source: 'user-reported',
|
|
500
|
+
detect: (turn) => detectUserReport(turn.prompt),
|
|
501
|
+
},
|
|
502
|
+
{ defect_class: 'council-availability-claim', source: 'self-detected', detect: detectCouncilClaim },
|
|
503
|
+
{ defect_class: 'language-mirror', source: 'self-detected', detect: detectLanguageMirror },
|
|
504
|
+
];
|
|
505
|
+
|
|
225
506
|
/** Run every self-detected detector over a finished turn. */
|
|
226
507
|
export function runDetectors(turn: TurnSnapshot): DefectFinding[] {
|
|
227
508
|
const out: DefectFinding[] = [];
|
|
228
|
-
for (const d of
|
|
229
|
-
|
|
509
|
+
for (const d of DETECTOR_REGISTRY) {
|
|
510
|
+
if (d.source !== 'self-detected') {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const f = d.detect(turn);
|
|
230
514
|
if (f !== null) {
|
|
231
515
|
out.push(f);
|
|
232
516
|
}
|
|
@@ -286,23 +570,43 @@ export interface EgressCapability {
|
|
|
286
570
|
hasAgentConfigCheckout: boolean;
|
|
287
571
|
/** `gh` is installed AND authenticated. */
|
|
288
572
|
ghAuthenticated: boolean;
|
|
289
|
-
/**
|
|
290
|
-
|
|
573
|
+
/**
|
|
574
|
+
* The authenticated user has push permission on the UPSTREAM repo — a
|
|
575
|
+
* pre-flight probe of actual rights (`gh api repos/<repo>` →
|
|
576
|
+
* `.permissions.push`), never an inference from `git remote` existing.
|
|
577
|
+
* A consumer who cloned the public repo has a remote and no write access;
|
|
578
|
+
* the old heuristic classified exactly that machine as push-capable.
|
|
579
|
+
*/
|
|
580
|
+
canPushUpstream: boolean;
|
|
581
|
+
/** The upstream repo allows forking and the user is authenticated. */
|
|
582
|
+
canFork: boolean;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Where a pull-request route pushes its branch. */
|
|
586
|
+
export type PushVia = 'upstream' | 'fork';
|
|
587
|
+
|
|
588
|
+
export interface EgressChoice {
|
|
589
|
+
route: EgressRoute;
|
|
590
|
+
/** Set only when `route === 'pull-request'`. */
|
|
591
|
+
pushVia: PushVia | null;
|
|
291
592
|
}
|
|
292
593
|
|
|
293
594
|
/**
|
|
294
595
|
* Route selection, exactly as specified: a pull request when the fix can be
|
|
295
|
-
* authored and pushed,
|
|
296
|
-
* nothing can leave the machine.
|
|
596
|
+
* authored and pushed (directly, or via fork → cross-repo PR), an issue when
|
|
597
|
+
* it cannot, and a local record when nothing can leave the machine.
|
|
297
598
|
*/
|
|
298
|
-
export function
|
|
299
|
-
if (cap.hasAgentConfigCheckout && cap.ghAuthenticated && cap.
|
|
300
|
-
return 'pull-request';
|
|
599
|
+
export function chooseEgress(cap: EgressCapability): EgressChoice {
|
|
600
|
+
if (cap.hasAgentConfigCheckout && cap.ghAuthenticated && cap.canPushUpstream) {
|
|
601
|
+
return { route: 'pull-request', pushVia: 'upstream' };
|
|
602
|
+
}
|
|
603
|
+
if (cap.hasAgentConfigCheckout && cap.ghAuthenticated && cap.canFork) {
|
|
604
|
+
return { route: 'pull-request', pushVia: 'fork' };
|
|
301
605
|
}
|
|
302
606
|
if (cap.ghAuthenticated) {
|
|
303
|
-
return 'issue';
|
|
607
|
+
return { route: 'issue', pushVia: null };
|
|
304
608
|
}
|
|
305
|
-
return 'local-only';
|
|
609
|
+
return { route: 'local-only', pushVia: null };
|
|
306
610
|
}
|
|
307
611
|
|
|
308
612
|
/** The PR / issue body. Deterministic — same record, same bytes. */
|
|
@@ -340,3 +644,40 @@ export function renderReport(record: DefectRecord, route: EgressRoute): string {
|
|
|
340
644
|
'construction — the record type has no field that can carry them.',
|
|
341
645
|
].join('\n');
|
|
342
646
|
}
|
|
647
|
+
|
|
648
|
+
/** The structured fields the upstream intake form collects. */
|
|
649
|
+
export interface ParsedReport {
|
|
650
|
+
defect_class: string;
|
|
651
|
+
fingerprint: string;
|
|
652
|
+
occurrences: number;
|
|
653
|
+
evidence: string;
|
|
654
|
+
suggested_surface: string;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Inverse of `renderReport` for the structured fields — the round-trip that
|
|
659
|
+
* pins the rendered body to the upstream issue form: every field the form
|
|
660
|
+
* collects is recoverable from a rendered report, so a `release` body and a
|
|
661
|
+
* manually-filed form carry the same clustering keys (fingerprint above all).
|
|
662
|
+
*/
|
|
663
|
+
export function parseReport(body: string): ParsedReport | null {
|
|
664
|
+
const cls = /^## Self-repair report — `([^`]+)`$/m.exec(body)?.[1];
|
|
665
|
+
const fp = /^- \*\*Fingerprint:\*\* `([^`]+)`$/m.exec(body)?.[1];
|
|
666
|
+
const seen = /^- \*\*Seen:\*\* (.+)$/m.exec(body)?.[1];
|
|
667
|
+
const evidence = /^### Evidence\n\n> (.*)$/m.exec(body)?.[1];
|
|
668
|
+
const surface = /### What the fix should address\n\n([\s\S]*?)\n\n---/.exec(body)?.[1];
|
|
669
|
+
if (!cls || !fp || !seen || evidence === undefined || !surface) {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
const occurrences = seen.startsWith('once') ? 1 : Number(/^(\d+)×/.exec(seen)?.[1] ?? Number.NaN);
|
|
673
|
+
if (!Number.isFinite(occurrences)) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
defect_class: cls,
|
|
678
|
+
fingerprint: fp,
|
|
679
|
+
occurrences,
|
|
680
|
+
evidence,
|
|
681
|
+
suggested_surface: surface.trim(),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
@@ -94,7 +94,35 @@ export function markReleased(root: string, fp: string, now: string): DefectRecor
|
|
|
94
94
|
if (rec === null) {
|
|
95
95
|
return null;
|
|
96
96
|
}
|
|
97
|
+
// A successful release supersedes any earlier failed attempts.
|
|
97
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
|
+
};
|
|
98
126
|
writeRecord(root, next);
|
|
99
127
|
return next;
|
|
100
128
|
}
|