@event4u/agent-config 8.5.0 → 8.6.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/CHANGELOG.md +25 -0
- package/dist/agent-src/commands/cost/report.md +20 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +30 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +15 -1
- package/dist/agent-src/contexts/execution/subagent-steering.md +54 -1
- package/dist/agent-src/contexts/model-recommendations.md +19 -0
- package/dist/agent-src/contexts/subagent-configuration.md +8 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +3 -3
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/mcp/registry-manifest.json +1 -1
- package/docs/CLAIMS.md +7 -0
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +125 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/proof.md +2 -1
- package/package.json +1 -1
- package/src/scripts/_lib/auto_dispatch.ts +63 -0
- package/src/scripts/_lib/subagent_routing.ts +24 -0
- package/src/scripts/_lib/subagent_steering.ts +213 -3
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Bootstrap shim for the event4u agent-config suite. This plugin ships ONLY the dispatcher hooks plus one install-pointer skill — ALL content (skills, rules, commands) is installed via `npx -y @event4u/agent-config init`, which writes the file projection AND registers the same hooks in ~/.claude/settings.json. DEPRECATED as a content channel; kept so the marketplace listing stays discoverable and existing installs keep working hooks.",
|
|
9
|
-
"version": "8.
|
|
9
|
+
"version": "8.6.0",
|
|
10
10
|
"keywords": [
|
|
11
11
|
"agent-config",
|
|
12
12
|
"skills",
|
package/CHANGELOG.md
CHANGED
|
@@ -989,6 +989,31 @@ our recommendation order, not its support status.
|
|
|
989
989
|
> that forces a new era split (`# Era: 8.2.x`, etc.) — see
|
|
990
990
|
> [`docs/contracts/CHANGELOG-conventions.md § Era splits`](docs/contracts/CHANGELOG-conventions.md).
|
|
991
991
|
|
|
992
|
+
## [8.6.0](https://github.com/event4u-app/agent-config/compare/8.5.0...8.6.0) (2026-07-08)
|
|
993
|
+
|
|
994
|
+
### Features
|
|
995
|
+
|
|
996
|
+
* **cost-routing:** tier-downshift layer — telemetry fields, category defaults, tripwires, slice inference, verify-fail escalation ([e9839f5](https://github.com/event4u-app/agent-config/commit/e9839f5a034c3a0882e8956a2a3f580b3978056a))
|
|
997
|
+
|
|
998
|
+
### Bug Fixes
|
|
999
|
+
|
|
1000
|
+
* **ci:** install task in the release workflow ([19072d9](https://github.com/event4u-app/agent-config/commit/19072d9bcbb6d30cd943804f287dd79c9c709167))
|
|
1001
|
+
|
|
1002
|
+
### Documentation
|
|
1003
|
+
|
|
1004
|
+
* **roadmaps:** add four frontier-prompt-harvest roadmaps ([e232e34](https://github.com/event4u-app/agent-config/commit/e232e342401bd6edafda79cd64ce8ed4ff6ddd93))
|
|
1005
|
+
* **proof:** regenerate proof.md with the backed downshift-cost-reduction claim ([6f28604](https://github.com/event4u-app/agent-config/commit/6f286048b609442be81ac60a8c64df544a369df0))
|
|
1006
|
+
* **roadmaps:** unify tripwire engine wording on the ADR-116 path ([f4ee375](https://github.com/event4u-app/agent-config/commit/f4ee37540ea6f9724014c63be05617e4abcdaa1b))
|
|
1007
|
+
* **adr:** pin the memory scale-tripwire activation path (ADR-116) ([7efe88a](https://github.com/event4u-app/agent-config/commit/7efe88a7cda064e659a830a2d6a70e506ab21408))
|
|
1008
|
+
* **claims:** back downshift-cost-reduction with the routing-downshift bench run ([38f3194](https://github.com/event4u-app/agent-config/commit/38f31942b16474fcc5977f0560da435ce84a81dd))
|
|
1009
|
+
|
|
1010
|
+
### Chores
|
|
1011
|
+
|
|
1012
|
+
* **condense:** recondense cost-routing context/command edits into dist ([33fe1a3](https://github.com/event4u-app/agent-config/commit/33fe1a353a3eef52d024079016b32b9fe02b0531))
|
|
1013
|
+
* **contexts:** durable verdict note for cost-aware model routing ([cbff1bd](https://github.com/event4u-app/agent-config/commit/cbff1bdc7f2b0b87843283443de534c48f5ddd54))
|
|
1014
|
+
|
|
1015
|
+
Tests: 6943 (+7 since 8.5.0)
|
|
1016
|
+
|
|
992
1017
|
## [8.5.0](https://github.com/event4u-app/agent-config/compare/8.4.1...8.5.0) (2026-07-08)
|
|
993
1018
|
|
|
994
1019
|
### Features
|
|
@@ -118,6 +118,26 @@ Orchestration telemetry (YYYY-MM, N dispatches):
|
|
|
118
118
|
guardrail breaches: [token_blowup | spawn_failure | verify_skip | none]
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
When orchestration lines carry the routing extension fields
|
|
122
|
+
(`task_class` / `tier_chosen` / `escalated_from` / `verify_result_by_tier` —
|
|
123
|
+
see `orchestration-telemetry.md`), append the per-tier routing view using
|
|
124
|
+
`readTierRoutingMetrics`, `escalationPromotionCandidates`, and
|
|
125
|
+
`verifyPassDrift` from the same module — the per-tier QUALITY view a
|
|
126
|
+
spend-only dashboard misses:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
Tier routing (YYYY-MM):
|
|
130
|
+
dispatches by tier: lite N · medium N · high N
|
|
131
|
+
escalation rate by class: <class>: N.N% (promotion tripwire: >40%)
|
|
132
|
+
verify-pass rate by tier: lite N.N% · medium N.N%
|
|
133
|
+
tripwires fired: [promote <class> | verify-pass drift on <tier> | none]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
A fired tripwire is a recommendation to surface, never an auto-flip: the
|
|
137
|
+
escalation tripwire suggests promoting the class's static default tier; the
|
|
138
|
+
drift tripwire flags verifier/model drift for investigation (per
|
|
139
|
+
`subagent-steering.md § Cost-routing tripwires`).
|
|
140
|
+
|
|
121
141
|
If no orchestration lines exist yet (telemetry capture not yet running or
|
|
122
142
|
no delegated dispatches this month), surface:
|
|
123
143
|
|
|
@@ -55,6 +55,36 @@ dwarfs the work). Below the floor → in-session.
|
|
|
55
55
|
| `parallelizable: files\|independent` / independent slices | `do-in-parallel` |
|
|
56
56
|
| change needing verification (any of the above) | implementer + cross-model judge per the `subagent-orchestration` Iron Law |
|
|
57
57
|
|
|
58
|
+
## Per-slice tier inference (v1.5 — deterministic, task-TYPE-keyed)
|
|
59
|
+
|
|
60
|
+
Once a slice is classified **delegable**, a second deterministic table infers
|
|
61
|
+
its `model_tier` (road-to-cost-aware-model-routing, council 2026-07-08).
|
|
62
|
+
Keyed **exclusively on the classifier's task-TYPE outputs** — never raw size
|
|
63
|
+
metrics; diff size anti-correlates with difficulty in refactoring domains.
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
UNKNOWN / AMBIGUOUS → inherit (SESSION TIER). NEVER GUESS DOWN.
|
|
67
|
+
SIZE SIGNALS ARE NEGATIVE GUARDS ONLY — THEY REVOKE A lite CANDIDACY,
|
|
68
|
+
THEY NEVER CREATE ONE.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| Slice type (classifier output) | Inferred tier |
|
|
72
|
+
|---|---|
|
|
73
|
+
| Delegable + read-only fan-out (grep / inventory / discovery targets) | `lite` |
|
|
74
|
+
| Delegable + mechanical / template-driven transform WITH test coverage | `lite` (verify-fail escalates to `medium` per the steering cascade) |
|
|
75
|
+
| Delegable + mutating WITHOUT test coverage | `medium` |
|
|
76
|
+
| Delegable + synthesis / judgment (review, analysis slice) | `medium` — judge one tier up per the orchestration Iron Law |
|
|
77
|
+
| Any other / ambiguous shape | `inherit` — session tier, no downshift |
|
|
78
|
+
|
|
79
|
+
**Negative size guard:** slice scope exceeding the mechanical envelope
|
|
80
|
+
(multi-file mutation, diff surface beyond single responsibility) loses `lite`
|
|
81
|
+
candidacy, resolves one row down — size never argues FOR a downshift, only
|
|
82
|
+
against one.
|
|
83
|
+
|
|
84
|
+
Every inferred decision records `tier_source: "inferred"` in orchestration
|
|
85
|
+
telemetry (static pins record `"static"`; session-tier runs `"inherit"`) so
|
|
86
|
+
the evidence gate scores inferred routing separately from static pinning.
|
|
87
|
+
|
|
58
88
|
## v2+ (deferred, gated on Phase 6 evidence)
|
|
59
89
|
|
|
60
90
|
LLM-based classification — only if the deterministic rules prove too coarse
|
|
@@ -19,7 +19,12 @@ v1 forward-compat rule on unknown fields).
|
|
|
19
19
|
"wall_clock_ms": 0,
|
|
20
20
|
"outcome": "DONE",
|
|
21
21
|
"verify_mode": "deterministic",
|
|
22
|
-
"verdict_changed_outcome": null
|
|
22
|
+
"verdict_changed_outcome": null,
|
|
23
|
+
"task_class": null,
|
|
24
|
+
"tier_chosen": null,
|
|
25
|
+
"tier_source": null,
|
|
26
|
+
"escalated_from": null,
|
|
27
|
+
"verify_result_by_tier": null
|
|
23
28
|
}
|
|
24
29
|
```
|
|
25
30
|
|
|
@@ -36,6 +41,15 @@ v1 forward-compat rule on unknown fields).
|
|
|
36
41
|
| `outcome` | enum | One of `DONE` · `DONE_WITH_CONCERNS` · `NEEDS_CONTEXT` · `BLOCKED` · `killed`. |
|
|
37
42
|
| `verify_mode` | enum | How output was verified: `deterministic` · `judge` · `none`. |
|
|
38
43
|
| `verdict_changed_outcome` | bool \| null | **A3 extension (ADR-109 Track A).** For a review/verdict subagent (e.g. `production-validator`): did the verdict change the outcome vs the in-session baseline? `true` = caught a real issue the baseline missed / flipped a false `READY`→`NOT READY`; `false` = no lift; `null` = not a verdict dispatch / not measured. **Negative-control (clean single-file) tasks MUST record `false`** — a `true` on a control = spurious findings, fails Gate A. Additive field on THIS object, not a second schema. Boolean/counts only, no bodies. |
|
|
44
|
+
| `task_class` | string \| null | **Routing extension (road-to-cost-aware-model-routing Phase 0).** Category id of the dispatched slice (e.g. `read-only-fanout`, `mechanical-edit`, `implementation`, `review-synthesis`). Enum-style id only, never free-form task text. `null` = pre-extension line / unclassified. |
|
|
45
|
+
| `tier_chosen` | string \| null | Tier the slice was dispatched on (`lite` \| `medium` \| `high`). `null` = pre-extension line. |
|
|
46
|
+
| `tier_source` | enum \| null | Where `tier_chosen` came from: `static` (frontmatter/category pin) · `inferred` (deterministic per-slice inference) · `inherit` (session tier — no downshift decision). Lets the evidence gate score inferred routing separately from static pinning. |
|
|
47
|
+
| `escalated_from` | string \| null | When the slice re-dispatched after a verification failure: tier of the FAILED attempt (e.g. `lite` when a lite return failed verify and the slice re-ran on `medium`). `null` = no escalation. |
|
|
48
|
+
| `verify_result_by_tier` | object \| null | Map tier → verification result per attempt of this slice (e.g. `{"lite":"fail","medium":"pass"}`). Values: `pass` \| `fail` \| `skipped`. Feeds the per-tier verify-pass-rate tripwire. Enums only, no verdict bodies. |
|
|
49
|
+
|
|
50
|
+
All five routing fields are additive and optional — a line without them is
|
|
51
|
+
still a valid orchestration line; readers ignore unknown fields per the v1
|
|
52
|
+
forward-compat rule.
|
|
39
53
|
|
|
40
54
|
## Privacy floor
|
|
41
55
|
|
|
@@ -42,6 +42,38 @@ Only automatic response to a transient failure; structural/semantic errors (e.g.
|
|
|
42
42
|
`BLOCKED` envelope) do **not** trigger a downshift — escalate to the orchestrator
|
|
43
43
|
immediately.
|
|
44
44
|
|
|
45
|
+
## Verify-fail escalation — the downshift cascade (M3)
|
|
46
|
+
|
|
47
|
+
Cheap-first cascade for DOWNSHIFTED slices only
|
|
48
|
+
(road-to-cost-aware-model-routing, council 2026-07-08). Transient failures
|
|
49
|
+
(above) and verify-fails are distinct: transient retries downward once; a
|
|
50
|
+
**verification failure escalates upward**.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
VERIFY-FAIL ON A DOWNSHIFTED RETURN → RE-DISPATCH ONE TIER UP.
|
|
54
|
+
COUNTS AGAINST THE EXISTING N=3 BUDGET — NEVER A NEW BUDGET.
|
|
55
|
+
ATTEMPT 1 lite → ATTEMPT 2 medium → ATTEMPT 3 = SLICE FAILED,
|
|
56
|
+
ORCHESTRATOR REPLANS AT SESSION TIER.
|
|
57
|
+
ESCALATION TRIGGER IS THE JUDGE VERDICT / DETERMINISTIC VERIFY RESULT —
|
|
58
|
+
NEVER THE SUBAGENT'S OWN CONFIDENCE.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **Scope:** slices with `tier_source: static | inferred` only. `inherit`
|
|
62
|
+
slices keep existing same-tier retry semantics — no behavior change outside
|
|
63
|
+
the downshift path.
|
|
64
|
+
- **Budget accounting:** each escalated attempt consumes one of the slice's
|
|
65
|
+
three attempts (`budgetHalt()` unchanged). Two failed attempts mark the
|
|
66
|
+
slice failed; the orchestrator replans — a slice's verify-fail never
|
|
67
|
+
poisons the orchestrator trajectory, only the final verified return (or
|
|
68
|
+
failure mark) enters its context.
|
|
69
|
+
- **Telemetry:** the re-dispatch records `escalated_from: <failed tier>` and
|
|
70
|
+
appends to `verify_result_by_tier`.
|
|
71
|
+
- **Economics guard:** a class escalating > 40% of the time is cheaper
|
|
72
|
+
started on the higher tier — the escalation-rate tripwire (below) promotes
|
|
73
|
+
its static default; the cascade is for the tail, not the norm.
|
|
74
|
+
|
|
75
|
+
Deterministic reference: `escalateOnVerifyFail()`.
|
|
76
|
+
|
|
45
77
|
## Failure-type stop — the N=3 budget applied per subagent type
|
|
46
78
|
|
|
47
79
|
Two consecutive **verification-failed** returns from one subagent type in a
|
|
@@ -88,6 +120,26 @@ The guardrail thresholds (`breachedGuardrails()`), read off the
|
|
|
88
120
|
|
|
89
121
|
A breach is a maintainer/user signal, surfaced — never an automatic flip.
|
|
90
122
|
|
|
123
|
+
## Cost-routing tripwires — steering policy for downshifted dispatch
|
|
124
|
+
|
|
125
|
+
Two tripwires guard the cost-aware downshift path
|
|
126
|
+
(road-to-cost-aware-model-routing, council 2026-07-08). Like the rollback
|
|
127
|
+
guardrails they are **surfaced, never auto-flipped** — steering policy over
|
|
128
|
+
the routing telemetry fields (`task_class`, `tier_chosen`, `escalated_from`,
|
|
129
|
+
`verify_result_by_tier` in [`orchestration-telemetry`](orchestration-telemetry.md)),
|
|
130
|
+
not a new mechanism:
|
|
131
|
+
|
|
132
|
+
| Tripwire | Threshold | Action when fired |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| **Escalation-rate promotion** | per-class escalation rate > 40% over the rolling window | Class's default tier is wrong — cascading it costs more than starting high (decision-theoretic escalation analysis). Promote the class's static default one tier, log the promotion; do not keep cascading. |
|
|
135
|
+
| **Verify-pass drift** | a tier's verify-pass rate drops below its trailing baseline | Verifier or model drift — a drifting verifier silently escalates everything (cost ~3x). Surface the drift with per-tier numbers; never silently absorb the extra escalations. |
|
|
136
|
+
|
|
137
|
+
Deterministic references: `escalationPromotionCandidates()` and
|
|
138
|
+
`verifyPassDrift()` over `readTierRoutingMetrics()` aggregates. Per-tier
|
|
139
|
+
quality view (spend by tier, escalation count by class, verify-pass rate by
|
|
140
|
+
tier) surfaces via `/cost:report` — the delayed-signal quality guard: cost
|
|
141
|
+
dashboards alone look fine while quality regresses.
|
|
142
|
+
|
|
91
143
|
## Kill-switch
|
|
92
144
|
|
|
93
145
|
```
|
|
@@ -101,7 +153,8 @@ This is the canonical disable — no code change, effective on the next run.
|
|
|
101
153
|
|
|
102
154
|
[`src/scripts/_lib/subagent_steering.ts`](../../../../src/scripts/_lib/subagent_steering.ts)
|
|
103
155
|
(`isLayerDisabled`, `budgetHalt`, `typeStop`, `sliceDispatchAllowed`,
|
|
104
|
-
`breachedGuardrails`
|
|
156
|
+
`breachedGuardrails`, `readTierRoutingMetrics`,
|
|
157
|
+
`escalationPromotionCandidates`, `verifyPassDrift`), covered by
|
|
105
158
|
[`tests/scripts/_lib_subagent_steering.test.ts`](../../../../tests/scripts/_lib_subagent_steering.test.ts).
|
|
106
159
|
|
|
107
160
|
## Related
|
|
@@ -39,6 +39,25 @@ tier is the reasoning band; `context: large` is the context-window need.
|
|
|
39
39
|
| **Implementation after a `high`-tier plan** | `medium` | Plan is done, execution is mechanical |
|
|
40
40
|
| **Trivial mechanical** (formatting, one-line config, rote rename) | `lite` | No reasoning required; fastest tier suffices |
|
|
41
41
|
|
|
42
|
+
## Subagent Category → Tier Defaults (dispatch-time downshift)
|
|
43
|
+
|
|
44
|
+
Normative defaults for DISPATCHED subagent slices (road-to-cost-aware-model-routing,
|
|
45
|
+
council 2026-07-08). The orchestrator's own session model is never switched;
|
|
46
|
+
these tiers apply to the sub-task the orchestrator spawns. Category-keyed —
|
|
47
|
+
immune to per-instance misrouting; unknown/ambiguous category → `inherit`.
|
|
48
|
+
|
|
49
|
+
| Subagent category | Tier | Why |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| Read-only fan-out (grep / inventory / file discovery) | `lite` | Returns get deterministic verification; quality risk ~0 by construction |
|
|
52
|
+
| Mechanical single-file regen / template-driven transform | `lite`→`medium` fallback | Pattern application, no open-ended reasoning; verify-fail escalates one tier |
|
|
53
|
+
| Implementation (bounded, planned slice) | `medium` | Plan exists; execution is pattern work |
|
|
54
|
+
| Review / judge / synthesis / architecture | `medium`\|`high` | Reasoning depth is the product |
|
|
55
|
+
|
|
56
|
+
**Judge asymmetry invariant:** the cross-model judge always runs one tier
|
|
57
|
+
ABOVE the implementer tier (ladder in
|
|
58
|
+
[`subagent-configuration`](subagent-configuration.md)). Downshifting an
|
|
59
|
+
implementer never downshifts its judge.
|
|
60
|
+
|
|
42
61
|
## Detection Heuristics
|
|
43
62
|
|
|
44
63
|
| Signal | Task Type | Tier |
|
|
@@ -25,6 +25,14 @@ If the session runs on **opus**, judge stays on opus (no higher tier
|
|
|
25
25
|
available). If the session runs on **sonnet**, judge defaults to opus.
|
|
26
26
|
If the session runs on **haiku**, judge defaults to sonnet.
|
|
27
27
|
|
|
28
|
+
**Downshift never touches the judge.** When cost-aware routing runs an
|
|
29
|
+
implementer on a downshifted tier (per the category → tier defaults in
|
|
30
|
+
[`model-recommendations § Subagent Category → Tier Defaults`](model-recommendations.md)),
|
|
31
|
+
the judge still resolves one tier above the IMPLEMENTER's resolved tier —
|
|
32
|
+
a `lite` implementer gets at least a `medium` judge. Downshifting an
|
|
33
|
+
implementer never downshifts its judge below what the verify contract
|
|
34
|
+
requires; a judge is never `lite`.
|
|
35
|
+
|
|
28
36
|
## Resolution order
|
|
29
37
|
|
|
30
38
|
For both implementer and judge:
|
|
@@ -39,7 +39,7 @@ schema_version: 1
|
|
|
39
39
|
# CI guard: a release bump of `package.json` must update this value
|
|
40
40
|
# in lockstep — see scripts/check_template_pin_drift.ts (road-to-
|
|
41
41
|
# portable-runtime-and-update-check P3.3).
|
|
42
|
-
agent_config_version: "8.
|
|
42
|
+
agent_config_version: "8.6.0"
|
|
43
43
|
|
|
44
44
|
# --- Project identity ---
|
|
45
45
|
project:
|
|
@@ -2925,7 +2925,7 @@
|
|
|
2925
2925
|
},
|
|
2926
2926
|
{
|
|
2927
2927
|
"category": "command",
|
|
2928
|
-
"checksum": "sha256:
|
|
2928
|
+
"checksum": "sha256:90b89f5241ecad5adb6b6de797a2b71e0a87898b8e3f5a0958be4df293246a3a",
|
|
2929
2929
|
"install": {
|
|
2930
2930
|
"default": true,
|
|
2931
2931
|
"removable": false
|
|
@@ -12769,7 +12769,7 @@
|
|
|
12769
12769
|
"workspaces": []
|
|
12770
12770
|
}
|
|
12771
12771
|
],
|
|
12772
|
-
"checksum": "sha256:
|
|
12772
|
+
"checksum": "sha256:373d4519debcdb5a39c065b83e496247480fc44a91aeb6f495ba5024f2a7192b",
|
|
12773
12773
|
"deprecations": [
|
|
12774
12774
|
{
|
|
12775
12775
|
"key": "tier",
|
|
@@ -12912,7 +12912,7 @@
|
|
|
12912
12912
|
"reason": "scaffold template for ticket-bundle tickets (ADR-101); carries no runtime frontmatter — emit-tickets fills it in per ticket"
|
|
12913
12913
|
}
|
|
12914
12914
|
],
|
|
12915
|
-
"generated_at": "2026-07-
|
|
12915
|
+
"generated_at": "2026-07-08T14:10:55Z",
|
|
12916
12916
|
"packs": [
|
|
12917
12917
|
{
|
|
12918
12918
|
"artefact_count": 95,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
01ac916a948ce2e4232ae06f46e3c626f31a3e328c66904dbfd61ef731a13b1c discovery-manifest.json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"checksum": "sha256:
|
|
3
|
-
"generated_at": "2026-07-
|
|
2
|
+
"checksum": "sha256:373d4519debcdb5a39c065b83e496247480fc44a91aeb6f495ba5024f2a7192b",
|
|
3
|
+
"generated_at": "2026-07-08T14:10:55Z",
|
|
4
4
|
"packs": [
|
|
5
5
|
{
|
|
6
6
|
"always_on": false,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"checksum": "sha256:
|
|
3
|
-
"generated_at": "2026-07-
|
|
2
|
+
"checksum": "sha256:373d4519debcdb5a39c065b83e496247480fc44a91aeb6f495ba5024f2a7192b",
|
|
3
|
+
"generated_at": "2026-07-08T14:10:55Z",
|
|
4
4
|
"scanner_version": "f36b5a348348",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
{
|
package/docs/CLAIMS.md
CHANGED
|
@@ -70,6 +70,13 @@
|
|
|
70
70
|
- status: backed
|
|
71
71
|
- last_verified: 2026-07-07
|
|
72
72
|
|
|
73
|
+
### claim: downshift-cost-reduction
|
|
74
|
+
- claim: On the READ-ONLY FAN-OUT slice family, tier-downshifted subagent dispatch (lite/haiku vs session-tier-proxy sonnet) nets a ≥30% USD-weighted token-cost reduction at held quality — measured 2026-07-08 (n=10 paired live dispatches, 20 telemetry lines): 10/10 exact-match on BOTH arms, 29.4% fewer raw tokens, 76.5% USD-weighted cost reduction at the 3x haiku↔sonnet price ratio. FAMILY-SCOPED — the mechanical-edit family is unmeasured and its downshift (incl. the deferred tier downgrades of existing units) stays gated. Negative control held: an open-ended synthesis/unknown slice never resolves below the session tier (inferSliceTier → medium/inherit, never lite).
|
|
75
|
+
- kind: quant
|
|
76
|
+
- evidence: internal/bench/routing-downshift/results-2026-07-08.md#FAMILY-SCOPED PROVE
|
|
77
|
+
- status: backed
|
|
78
|
+
- last_verified: 2026-07-08
|
|
79
|
+
|
|
73
80
|
---
|
|
74
81
|
|
|
75
82
|
## Unbacked inventory (documented debt — not yet markered in prose)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
adr: 116
|
|
3
|
+
status: accepted
|
|
4
|
+
date: 2026-07-08
|
|
5
|
+
decision: memory-tripwire-activation-path
|
|
6
|
+
supersedes: —
|
|
7
|
+
superseded_by: —
|
|
8
|
+
phase: road-to-memory-retrieval-economy · Phase 6
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# ADR-116 — Memory scale-tripwire activation path: one engine, measured at fire time
|
|
12
|
+
|
|
13
|
+
- **Deciders:** maintainer + AI council (anthropic/claude-sonnet-4-5 +
|
|
14
|
+
openai/gpt-4o; disposition debate 2 rounds + engine tie-break 2 rounds,
|
|
15
|
+
2026-07-08 — tie-break converged after the in-repo `node:sqlite`
|
|
16
|
+
precedent was surfaced)
|
|
17
|
+
|
|
18
|
+
## Context
|
|
19
|
+
|
|
20
|
+
An external second-opinion review (round 2 of the Obsidian-reference
|
|
21
|
+
comparison) surfaced a real documentation conflict: the memory-scale
|
|
22
|
+
tripwire (>200 files in one memory/knowledge type, or >500 files total —
|
|
23
|
+
guarded by `lint_knowledge_scale.ts`) had **three different pre-decided
|
|
24
|
+
activation paths** across three artifacts, written by two councils one day
|
|
25
|
+
apart:
|
|
26
|
+
|
|
27
|
+
1. `agents/settings/contexts/second-brain-delta-verdict.md` Q4
|
|
28
|
+
(2026-07-07): "file-first in-memory BM25 (minisearch-class, re-index at
|
|
29
|
+
session-start, no persistence)".
|
|
30
|
+
2. `agents/roadmaps/road-to-memory-retrieval-economy.md` Phase 6
|
|
31
|
+
(2026-07-07/08): "SQLite FTS5 over the memory files … replacing
|
|
32
|
+
`_score()`'s substring pass".
|
|
33
|
+
3. `docs/decisions/ADR-061-corpus-grounding-layer.md`: "a new domain ships
|
|
34
|
+
a manifest + data + a named owner — **not a forked engine**", with a
|
|
35
|
+
tested in-house BM25 (`src/skills/corpus-grounding/scripts/bm25_search.ts`,
|
|
36
|
+
generic `class BM25` over `Row[]`) as the implied reuse candidate.
|
|
37
|
+
|
|
38
|
+
The whole point of a pre-decided path is that the tripwire firing needs no
|
|
39
|
+
new debate; in this state, firing would have required one.
|
|
40
|
+
|
|
41
|
+
## Decision
|
|
42
|
+
|
|
43
|
+
1. **One engine: SQLite FTS5 via Node's built-in `node:sqlite`.**
|
|
44
|
+
- Zero npm dependency — the repo already uses `node:sqlite` in
|
|
45
|
+
production code (`src/scripts/mcp_telemetry_store.ts` /
|
|
46
|
+
`mcp_telemetry_query.ts`) with the established pattern: lazy import +
|
|
47
|
+
runtime guard for Node < 22.5 (`src/scripts/_lib/node_sqlite.d.ts`).
|
|
48
|
+
`better-sqlite3` and a minisearch dependency are both rejected.
|
|
49
|
+
- **Persistence economics** decided the tie-break: at tripwire scale the
|
|
50
|
+
in-memory re-index per session-start is a startup tax that grows with
|
|
51
|
+
the corpus; FTS5 gives a persisted index plus incremental updates.
|
|
52
|
+
The in-house `class BM25` lacks a persistence layer; building one
|
|
53
|
+
(serialization, incremental updates, corruption handling) recreates
|
|
54
|
+
what SQLite ships.
|
|
55
|
+
- Index location: `agents/runtime/state/` (gitignored, never tracked).
|
|
56
|
+
- **Re-index is batch/lazy** — post-session or on first lookup, never
|
|
57
|
+
inline per write: during active sessions files churn (drafts,
|
|
58
|
+
rollbacks), and inline indexing would put fsync latency and lock
|
|
59
|
+
contention inside the write path (council round-2 caveat).
|
|
60
|
+
- FTS5 availability in the bundled SQLite is verified at implementation
|
|
61
|
+
time; on absence or Node < 22.5 the runtime guard falls back to the
|
|
62
|
+
current grep/substring path.
|
|
63
|
+
|
|
64
|
+
2. **Measurement at activation, no speculative harness.** At today's
|
|
65
|
+
corpus (~85 auto-memory files / 412 KB) the ranked-retrieval lift over
|
|
66
|
+
grep is zero by construction — the honest-zero is already recorded in
|
|
67
|
+
the verdict. No grep-vs-ranked benchmark harness is built now. Instead,
|
|
68
|
+
the pre-decided path carries a clause: when the tripwire fires, reuse
|
|
69
|
+
the retrieval-economy Phase-0 replay set to measure the grep baseline
|
|
70
|
+
vs the FTS5 candidate on the then-current corpus BEFORE building; ship
|
|
71
|
+
only on measured retrieval lift, else record the honest-null and keep
|
|
72
|
+
grep.
|
|
73
|
+
|
|
74
|
+
3. **ADR-061 boundary restated.** Memory lookup is a **Reference**
|
|
75
|
+
operation in ADR-061's four-operation model — memory is never
|
|
76
|
+
registered as a grounding corpus ("grounding theater"), and
|
|
77
|
+
`corpus-grounding`'s engine is not forked per domain. This ADR does not
|
|
78
|
+
amend ADR-061; it confirms the tripwire path lives outside the
|
|
79
|
+
grounding layer.
|
|
80
|
+
|
|
81
|
+
4. **Wording migration.** The verdict Q4 "minisearch-class" sentence and
|
|
82
|
+
the tripwire-table row are amended in the same change; a grep sweep for
|
|
83
|
+
`minisearch` / "pre-decided BM25" catches stragglers in stable
|
|
84
|
+
artifacts.
|
|
85
|
+
|
|
86
|
+
## Consequences
|
|
87
|
+
|
|
88
|
+
- Firing the tripwire is now a wiring-plus-measurement task, not a design
|
|
89
|
+
debate: guard fires → replay-set comparison → on lift, ~30-line
|
|
90
|
+
`node:sqlite` FTS5 schema + query wrapper replacing `_score()`'s
|
|
91
|
+
substring pass.
|
|
92
|
+
- No new npm dependency at any point on this path; Layer-2 sunset stands
|
|
93
|
+
(no vectors, no services, no always-on workers).
|
|
94
|
+
- The rejected review items are recorded here so they are not relitigated:
|
|
95
|
+
a pre-built grep-vs-ranked harness (speculative — measures a known
|
|
96
|
+
zero), and a deterministic "next step" hot-context field (feature envy
|
|
97
|
+
absent a real resume-context gap; the LLM-summary ban from the verdict's
|
|
98
|
+
REJECT-list is untouched).
|
|
99
|
+
|
|
100
|
+
## Alternatives
|
|
101
|
+
|
|
102
|
+
- **In-house `bm25_search.ts` as a library** — rejected in the tie-break:
|
|
103
|
+
no persistence layer; the "zero new deps" advantage evaporated once
|
|
104
|
+
`node:sqlite` (also zero-dep) was on the table; retrofitting
|
|
105
|
+
persistence ≈ 200 LOC of database-shaped code SQLite already provides.
|
|
106
|
+
- **minisearch dependency** (verdict Q4's original wording) — rejected:
|
|
107
|
+
new npm dependency for a problem the built-in module covers; same
|
|
108
|
+
missing-persistence shape.
|
|
109
|
+
- **Two-step (in-house now, FTS5 escalation later)** — rejected: a
|
|
110
|
+
pre-decided path must be single and unambiguous; a conditional two-step
|
|
111
|
+
re-imports the debate the tripwire exists to avoid.
|
|
112
|
+
- **Pre-building the measurement harness now** — rejected: honest expected
|
|
113
|
+
lift at the current corpus is zero; the replay set from Phase 0 will
|
|
114
|
+
exist independently and is reused at fire time.
|
|
115
|
+
|
|
116
|
+
## References
|
|
117
|
+
|
|
118
|
+
- `agents/roadmaps/road-to-memory-retrieval-economy.md` § Phase 6 (engine
|
|
119
|
+
note + measurement-at-activation clause).
|
|
120
|
+
- `agents/settings/contexts/second-brain-delta-verdict.md` Q4 (amended
|
|
121
|
+
wording + tripwire table).
|
|
122
|
+
- `docs/decisions/ADR-061-corpus-grounding-layer.md` (four-operation
|
|
123
|
+
model; engine-fork ban).
|
|
124
|
+
- `src/scripts/mcp_telemetry_store.ts`, `src/scripts/_lib/node_sqlite.d.ts`
|
|
125
|
+
(in-repo `node:sqlite` precedent).
|
package/docs/decisions/INDEX.md
CHANGED
|
@@ -119,6 +119,7 @@ _Auto-generated by `scripts/adr/regenerate_index.py`. Do not edit._
|
|
|
119
119
|
| [ADR-113](ADR-113-ci-native-release-label-trigger.md) | Ci Native Release Label Trigger | accepted | 2026-07-08 | — |
|
|
120
120
|
| [ADR-114](ADR-114-commit-policy-iron-law-override.md) | Commit Policy Iron Law Override | — | — | — |
|
|
121
121
|
| [ADR-115](ADR-115-command-cluster-consolidation-phase-4.md) | Command Cluster Consolidation Phase 4 | accepted | 2026-07-08 | — |
|
|
122
|
+
| [ADR-116](ADR-116-memory-tripwire-activation-path.md) | Memory Tripwire Activation Path | accepted | 2026-07-08 | — |
|
|
122
123
|
| [ADR-200](ADR-200-python-to-typescript-migration.md) | Python To Typescript Migration | accepted | 2026-06-11 | — |
|
|
123
124
|
|
|
124
125
|
## Unnumbered (legacy)
|
package/docs/proof.md
CHANGED
|
@@ -28,13 +28,14 @@ evidence pointer, or `task check-claims` fails the build.
|
|
|
28
28
|
|---|---|---|---|
|
|
29
29
|
| 172 commands. | quant | `src/scripts/check_artefact_count_messaging.ts#Artefact-count messaging gate` | ✅ |
|
|
30
30
|
| On a weak host (claude-haiku-4-5) the package produces a significant, placebo-controlled discipline lift on scope/downstream traps; on a strong host the same measurement is a published null — the package transplants discipline a weak model lacks, not model intelligence. | quant | `docs/benchmark.md#weak-host-specific` | ✅ |
|
|
31
|
+
| On the READ-ONLY FAN-OUT slice family, tier-downshifted subagent dispatch (lite/haiku vs session-tier-proxy sonnet) nets a ≥30% USD-weighted token-cost reduction at held quality — measured 2026-07-08 (n=10 paired live dispatches, 20 telemetry lines): 10/10 exact-match on BOTH arms, 29.4% fewer raw tokens, 76.5% USD-weighted cost reduction at the 3x haiku↔sonnet price ratio. FAMILY-SCOPED — the mechanical-edit family is unmeasured and its downshift (incl. the deferred tier downgrades of existing units) stays gated. Negative control held: an open-ended synthesis/unknown slice never resolves below the session tier (inferSliceTier → medium/inherit, never lite). | quant | `internal/bench/routing-downshift/results-2026-07-08.md#FAMILY-SCOPED PROVE` | ✅ |
|
|
31
32
|
| The lift-carrying essential cut (kernel + downstream-changes) keeps a significant weak-host discipline lift at a fraction of the full load's tokens, and the lift is FAMILY- and HOST-SCOPED — measured on three hosts: claude-haiku-4-5 (weak) shows the family-scoped lift (trapE 0.533→1.000, 7/7 discordant, corpus cost 1.71x); claude-sonnet-4-6 (strong) is a ceiling null; gpt-5-mini (non-Claude weak, codex prompt-prepend surface) FAILED replication with headroom (corpus Δ=+0.024 p=0.70, capability trend n.s. — no harm claimed, injection-surface confound documented). Therefore discipline_profile: auto enables the lift only where measured (vendor-granular unknown_defaults). Non-claims — the balanced router profile was removed after a NULL measurement (p=0.81, n=24); no full-tier recommendation exists; no cross-vendor lift is claimed. | quant | `docs/benchmark.md#REPLICATION FAILED` | ✅ |
|
|
32
33
|
| The whole layer is compiled into host agents with zero runtime daemon. | qual | `docs/contracts/no-runtime-boundary.md#file-first, no-runtime suite` | ✅ |
|
|
33
34
|
| 95 governed rules. | quant | `src/scripts/check_artefact_count_messaging.ts#Artefact-count messaging gate` | ✅ |
|
|
34
35
|
| 264 skills (README hero + feature list). | quant | `src/scripts/check_artefact_count_messaging.ts#Artefact-count messaging gate` | ✅ |
|
|
35
36
|
| Removes only its own keys from a shared host config (matched by JSON-pointer + SHA-256), never a neighbour tool's entries. | qual | `docs/contracts/install-layout.md#JSON-pointer` | ✅ |
|
|
36
37
|
|
|
37
|
-
**
|
|
38
|
+
**8 backed claim(s)** — all evidence pointers resolve in CI.
|
|
38
39
|
|
|
39
40
|
Artefact counts in public prose (skills, commands, governed rules,
|
|
40
41
|
guidelines, personas) are **generated from source and CI-drift-checked**:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.6.0",
|
|
4
4
|
"description": "Universal AI Agent OS — audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -93,3 +93,66 @@ export function classifyTask(signals: TaskSignals, activation: ActivationInputs)
|
|
|
93
93
|
const action: DispatchAction = activation.auto === 'on' ? 'dispatch' : 'ask';
|
|
94
94
|
return { delegable: true, action, mode, reason };
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
// ── Per-slice tier inference (v1.5 — road-to-cost-aware-model-routing) ──
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Task-TYPE outputs the tier inference keys on. NEVER raw size metrics —
|
|
101
|
+
* size enters only as the negative guard below. Contract:
|
|
102
|
+
* `auto-dispatch-classification.md § Per-slice tier inference`.
|
|
103
|
+
*/
|
|
104
|
+
export type SliceType =
|
|
105
|
+
| 'read-only-fanout' // grep / inventory / discovery targets
|
|
106
|
+
| 'mechanical-covered' // template-driven transform WITH test coverage
|
|
107
|
+
| 'mutating-uncovered' // mutation without test coverage
|
|
108
|
+
| 'synthesis' // review / analysis / judgment slice
|
|
109
|
+
| 'unknown';
|
|
110
|
+
|
|
111
|
+
export interface SliceTierSignals {
|
|
112
|
+
slice_type: SliceType;
|
|
113
|
+
/**
|
|
114
|
+
* Negative size guard: slice scope exceeds the mechanical envelope
|
|
115
|
+
* (multi-file mutation / beyond single responsibility). Revokes a `lite`
|
|
116
|
+
* candidacy — never creates one.
|
|
117
|
+
*/
|
|
118
|
+
exceeds_mechanical_envelope?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type InferredTier = 'lite' | 'medium' | 'inherit';
|
|
122
|
+
|
|
123
|
+
export interface TierInference {
|
|
124
|
+
tier: InferredTier;
|
|
125
|
+
/** 'inferred' for a real downshift decision; 'inherit' when no inference fired. */
|
|
126
|
+
tier_source: 'inferred' | 'inherit';
|
|
127
|
+
reason: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Infer a delegable slice's model tier from its task TYPE. Deterministic,
|
|
132
|
+
* no LLM meta-call. Unknown/ambiguous → inherit (session tier) — never
|
|
133
|
+
* guess down.
|
|
134
|
+
*/
|
|
135
|
+
export function inferSliceTier(signals: SliceTierSignals): TierInference {
|
|
136
|
+
const guard = signals.exceeds_mechanical_envelope === true;
|
|
137
|
+
|
|
138
|
+
switch (signals.slice_type) {
|
|
139
|
+
case 'read-only-fanout':
|
|
140
|
+
case 'mechanical-covered': {
|
|
141
|
+
if (guard) {
|
|
142
|
+
return {
|
|
143
|
+
tier: 'medium',
|
|
144
|
+
tier_source: 'inferred',
|
|
145
|
+
reason: `${signals.slice_type} slice, but scope exceeds the mechanical envelope — lite candidacy revoked`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const cascade = signals.slice_type === 'mechanical-covered' ? ' (verify-fail escalates to medium)' : '';
|
|
149
|
+
return { tier: 'lite', tier_source: 'inferred', reason: `${signals.slice_type} slice${cascade}` };
|
|
150
|
+
}
|
|
151
|
+
case 'mutating-uncovered':
|
|
152
|
+
return { tier: 'medium', tier_source: 'inferred', reason: 'mutating slice without test coverage' };
|
|
153
|
+
case 'synthesis':
|
|
154
|
+
return { tier: 'medium', tier_source: 'inferred', reason: 'synthesis/judgment slice — judge one tier up' };
|
|
155
|
+
default:
|
|
156
|
+
return { tier: 'inherit', tier_source: 'inherit', reason: 'unknown/ambiguous slice type — session tier, never guess down' };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -18,6 +18,16 @@
|
|
|
18
18
|
export type Tier = 'lite' | 'medium' | 'high' | 'inherit';
|
|
19
19
|
export type QuotaPool = 'separate' | 'shared';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Where a resolved tier came from (road-to-cost-aware-model-routing Phase 0):
|
|
23
|
+
* - 'static' — declared by frontmatter / category pin,
|
|
24
|
+
* - 'inferred' — produced by the deterministic per-slice tier inference,
|
|
25
|
+
* - 'inherit' — session tier; no downshift decision was made.
|
|
26
|
+
* Mirrors the `tier_source` telemetry field in
|
|
27
|
+
* `src/agent-src/contexts/execution/orchestration-telemetry.md`.
|
|
28
|
+
*/
|
|
29
|
+
export type TierSource = 'static' | 'inferred' | 'inherit';
|
|
30
|
+
|
|
21
31
|
/** Per-tier model alias overrides (empty string = use the tier's runtime default). */
|
|
22
32
|
export interface ModelMap {
|
|
23
33
|
lite?: string;
|
|
@@ -35,6 +45,12 @@ export interface RoutingInputs {
|
|
|
35
45
|
model_map: ModelMap;
|
|
36
46
|
/** From the host-capability manifest. */
|
|
37
47
|
separate_quota_pool: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* How `task_tier` was produced: 'static' (frontmatter/category pin) or
|
|
50
|
+
* 'inferred' (deterministic per-slice inference). Optional — absent
|
|
51
|
+
* defaults to 'static'. Ignored when the slice runs on the session tier.
|
|
52
|
+
*/
|
|
53
|
+
task_tier_origin?: Exclude<TierSource, 'inherit'>;
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
export interface RoutingDecision {
|
|
@@ -44,6 +60,8 @@ export interface RoutingDecision {
|
|
|
44
60
|
model: string;
|
|
45
61
|
/** Whether a separate quota pool is used (bonus) or the shared one. */
|
|
46
62
|
quota_pool: QuotaPool;
|
|
63
|
+
/** Telemetry value for `tier_source`: 'inherit' when no downshift decision applied. */
|
|
64
|
+
tier_source: TierSource;
|
|
47
65
|
reason: string;
|
|
48
66
|
}
|
|
49
67
|
|
|
@@ -64,6 +82,11 @@ export function resolveSubagentRouting(inp: RoutingInputs): RoutingDecision {
|
|
|
64
82
|
const tier: Tier = !inp.downshift || inp.task_tier === 'inherit' ? inp.session_tier : inp.task_tier;
|
|
65
83
|
const downshifted = inp.downshift && inp.task_tier !== 'inherit' && tier !== inp.session_tier;
|
|
66
84
|
|
|
85
|
+
// Telemetry provenance: only a real downshift decision carries its origin;
|
|
86
|
+
// session-tier runs (inherit task, downshift off, or tier == session) are 'inherit'.
|
|
87
|
+
const applied = inp.downshift && inp.task_tier !== 'inherit';
|
|
88
|
+
const tier_source: TierSource = applied ? (inp.task_tier_origin ?? 'static') : 'inherit';
|
|
89
|
+
|
|
67
90
|
// Quota arbitrage: a bonus, only when BOTH the setting and the host allow it.
|
|
68
91
|
const useSeparate = inp.quota_arbitrage && inp.separate_quota_pool;
|
|
69
92
|
const quota_pool: QuotaPool = useSeparate ? 'separate' : 'shared';
|
|
@@ -76,6 +99,7 @@ export function resolveSubagentRouting(inp: RoutingInputs): RoutingDecision {
|
|
|
76
99
|
tier,
|
|
77
100
|
model: modelForTier(tier, inp.model_map),
|
|
78
101
|
quota_pool,
|
|
102
|
+
tier_source,
|
|
79
103
|
reason: reasonParts.join(' · '),
|
|
80
104
|
};
|
|
81
105
|
}
|
|
@@ -117,6 +117,12 @@ export interface OrchEntry {
|
|
|
117
117
|
token_delta: number;
|
|
118
118
|
outcome: string;
|
|
119
119
|
verify_mode: string;
|
|
120
|
+
/** Routing extension (road-to-cost-aware-model-routing) — null on pre-extension lines. */
|
|
121
|
+
task_class: string | null;
|
|
122
|
+
tier_chosen: string | null;
|
|
123
|
+
tier_source: string | null;
|
|
124
|
+
escalated_from: string | null;
|
|
125
|
+
verify_result_by_tier: Record<string, string> | null;
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
/**
|
|
@@ -129,9 +135,8 @@ export interface OrchEntry {
|
|
|
129
135
|
*
|
|
130
136
|
* user_override_rate cannot be measured from telemetry alone — returns 0.
|
|
131
137
|
*/
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
): GuardrailMetrics {
|
|
138
|
+
/** Parse orchestration entries from audit-log-v1 JSONL lines (shared by all aggregators). */
|
|
139
|
+
export function parseOrchEntries(lines: string[]): OrchEntry[] {
|
|
135
140
|
const entries: OrchEntry[] = [];
|
|
136
141
|
for (const line of lines) {
|
|
137
142
|
const trimmed = line.trim();
|
|
@@ -141,17 +146,33 @@ export function readOrchestrationMetrics(
|
|
|
141
146
|
if (parsed['input_kind'] !== 'orchestration') continue;
|
|
142
147
|
const orch = parsed['orchestration'] as Record<string, unknown> | undefined;
|
|
143
148
|
if (!orch) continue;
|
|
149
|
+
const vbt = orch['verify_result_by_tier'];
|
|
144
150
|
entries.push({
|
|
145
151
|
task_size_estimate: Number(orch['task_size_estimate'] ?? 0),
|
|
146
152
|
spawn_count: Number(orch['spawn_count'] ?? 0),
|
|
147
153
|
token_delta: Number(orch['token_delta'] ?? 0),
|
|
148
154
|
outcome: String(orch['outcome'] ?? ''),
|
|
149
155
|
verify_mode: String(orch['verify_mode'] ?? ''),
|
|
156
|
+
task_class: orch['task_class'] == null ? null : String(orch['task_class']),
|
|
157
|
+
tier_chosen: orch['tier_chosen'] == null ? null : String(orch['tier_chosen']),
|
|
158
|
+
tier_source: orch['tier_source'] == null ? null : String(orch['tier_source']),
|
|
159
|
+
escalated_from: orch['escalated_from'] == null ? null : String(orch['escalated_from']),
|
|
160
|
+
verify_result_by_tier:
|
|
161
|
+
vbt != null && typeof vbt === 'object' && !Array.isArray(vbt)
|
|
162
|
+
? Object.fromEntries(Object.entries(vbt as Record<string, unknown>).map(([k, v]) => [k, String(v)]))
|
|
163
|
+
: null,
|
|
150
164
|
});
|
|
151
165
|
} catch {
|
|
152
166
|
// skip malformed lines
|
|
153
167
|
}
|
|
154
168
|
}
|
|
169
|
+
return entries;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function readOrchestrationMetrics(
|
|
173
|
+
lines: string[],
|
|
174
|
+
): GuardrailMetrics {
|
|
175
|
+
const entries = parseOrchEntries(lines);
|
|
155
176
|
|
|
156
177
|
if (entries.length === 0) {
|
|
157
178
|
return { token_ratio: 1, spawn_failure_rate: 0, verify_skip_rate: 0, user_override_rate: 0 };
|
|
@@ -174,3 +195,192 @@ export function readOrchestrationMetrics(
|
|
|
174
195
|
user_override_rate: 0, // cannot measure from telemetry alone
|
|
175
196
|
};
|
|
176
197
|
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Per-tier / per-class routing aggregates (road-to-cost-aware-model-routing
|
|
201
|
+
* Phase 0). Pure aggregation over the routing telemetry fields — feeds the
|
|
202
|
+
* `/cost:report` per-tier view and the Phase-2 tripwires. Pre-extension lines
|
|
203
|
+
* (null routing fields) are ignored per aggregate.
|
|
204
|
+
*/
|
|
205
|
+
export interface TierRoutingMetrics {
|
|
206
|
+
/** Dispatch count per tier_chosen. */
|
|
207
|
+
dispatches_by_tier: Record<string, number>;
|
|
208
|
+
/** Dispatch count per task_class. */
|
|
209
|
+
dispatches_by_class: Record<string, number>;
|
|
210
|
+
/** Escalation count per task_class (entries with escalated_from set). */
|
|
211
|
+
escalations_by_class: Record<string, number>;
|
|
212
|
+
/** escalations_by_class / dispatches_by_class, per class present in both. */
|
|
213
|
+
escalation_rate_by_class: Record<string, number>;
|
|
214
|
+
/** Per tier: pass / (pass + fail) over verify_result_by_tier attempts. */
|
|
215
|
+
verify_pass_rate_by_tier: Record<string, number>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function readTierRoutingMetrics(lines: string[]): TierRoutingMetrics {
|
|
219
|
+
const entries = parseOrchEntries(lines);
|
|
220
|
+
|
|
221
|
+
const dispatches_by_tier: Record<string, number> = {};
|
|
222
|
+
const dispatches_by_class: Record<string, number> = {};
|
|
223
|
+
const escalations_by_class: Record<string, number> = {};
|
|
224
|
+
const verifyCounts: Record<string, { pass: number; fail: number }> = {};
|
|
225
|
+
|
|
226
|
+
for (const e of entries) {
|
|
227
|
+
if (e.tier_chosen !== null) {
|
|
228
|
+
dispatches_by_tier[e.tier_chosen] = (dispatches_by_tier[e.tier_chosen] ?? 0) + 1;
|
|
229
|
+
}
|
|
230
|
+
if (e.task_class !== null) {
|
|
231
|
+
dispatches_by_class[e.task_class] = (dispatches_by_class[e.task_class] ?? 0) + 1;
|
|
232
|
+
if (e.escalated_from !== null) {
|
|
233
|
+
escalations_by_class[e.task_class] = (escalations_by_class[e.task_class] ?? 0) + 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (e.verify_result_by_tier !== null) {
|
|
237
|
+
for (const [tier, result] of Object.entries(e.verify_result_by_tier)) {
|
|
238
|
+
const c = (verifyCounts[tier] ??= { pass: 0, fail: 0 });
|
|
239
|
+
if (result === 'pass') c.pass += 1;
|
|
240
|
+
else if (result === 'fail') c.fail += 1;
|
|
241
|
+
// 'skipped' counts toward neither pass nor fail.
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const escalation_rate_by_class: Record<string, number> = {};
|
|
247
|
+
for (const [cls, n] of Object.entries(dispatches_by_class)) {
|
|
248
|
+
escalation_rate_by_class[cls] = (escalations_by_class[cls] ?? 0) / n;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const verify_pass_rate_by_tier: Record<string, number> = {};
|
|
252
|
+
for (const [tier, c] of Object.entries(verifyCounts)) {
|
|
253
|
+
const attempts = c.pass + c.fail;
|
|
254
|
+
if (attempts > 0) verify_pass_rate_by_tier[tier] = c.pass / attempts;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
dispatches_by_tier,
|
|
259
|
+
dispatches_by_class,
|
|
260
|
+
escalations_by_class,
|
|
261
|
+
escalation_rate_by_class,
|
|
262
|
+
verify_pass_rate_by_tier,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Cost-routing tripwires (road-to-cost-aware-model-routing Phase 2) —
|
|
268
|
+
* steering policy over the routing telemetry, surfaced never auto-flipped.
|
|
269
|
+
*/
|
|
270
|
+
export const ROUTING_TRIPWIRES = {
|
|
271
|
+
/**
|
|
272
|
+
* Per-class escalation rate above which the class's static default tier
|
|
273
|
+
* should be promoted: cascading a class that escalates this often costs
|
|
274
|
+
* more than starting on the higher tier.
|
|
275
|
+
*/
|
|
276
|
+
escalation_promotion_rate: 0.4,
|
|
277
|
+
/** Verify-pass rate drop (absolute) below the trailing baseline that counts as drift. */
|
|
278
|
+
verify_pass_drift_tolerance: 0.1,
|
|
279
|
+
/** Minimum dispatches per class before the escalation tripwire may fire (noise floor). */
|
|
280
|
+
min_class_dispatches: 5,
|
|
281
|
+
} as const;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Tripwire (i): classes whose escalation rate exceeds the promotion
|
|
285
|
+
* threshold — their static default tier is wrong; promote it, stop cascading.
|
|
286
|
+
* Classes below the dispatch noise floor never fire.
|
|
287
|
+
*/
|
|
288
|
+
export function escalationPromotionCandidates(m: TierRoutingMetrics): string[] {
|
|
289
|
+
const candidates: string[] = [];
|
|
290
|
+
for (const [cls, rate] of Object.entries(m.escalation_rate_by_class)) {
|
|
291
|
+
const dispatches = m.dispatches_by_class[cls] ?? 0;
|
|
292
|
+
if (dispatches < ROUTING_TRIPWIRES.min_class_dispatches) continue;
|
|
293
|
+
if (rate > ROUTING_TRIPWIRES.escalation_promotion_rate) candidates.push(cls);
|
|
294
|
+
}
|
|
295
|
+
return candidates.sort();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Tripwire (ii): tiers whose current verify-pass rate dropped below the
|
|
300
|
+
* trailing baseline by more than the tolerance — verifier or model drift.
|
|
301
|
+
* Tiers absent from either side are skipped (no baseline → no drift claim).
|
|
302
|
+
*/
|
|
303
|
+
export function verifyPassDrift(
|
|
304
|
+
current: Record<string, number>,
|
|
305
|
+
trailingBaseline: Record<string, number>,
|
|
306
|
+
): string[] {
|
|
307
|
+
const drifting: string[] = [];
|
|
308
|
+
for (const [tier, baseline] of Object.entries(trailingBaseline)) {
|
|
309
|
+
const now = current[tier];
|
|
310
|
+
if (now === undefined) continue;
|
|
311
|
+
if (now < baseline - ROUTING_TRIPWIRES.verify_pass_drift_tolerance) drifting.push(tier);
|
|
312
|
+
}
|
|
313
|
+
return drifting.sort();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Verify-fail escalation (road-to-cost-aware-model-routing Phase 4 / M3).
|
|
318
|
+
*
|
|
319
|
+
* A verification FAILURE on a downshifted return re-dispatches the slice one
|
|
320
|
+
* tier up, consuming the existing N=3 budget — never a new budget. Confined
|
|
321
|
+
* to tier_source static|inferred; inherit slices keep same-tier retry.
|
|
322
|
+
* The trigger is the judge verdict / deterministic verify result — never the
|
|
323
|
+
* subagent's self-reported confidence.
|
|
324
|
+
*/
|
|
325
|
+
export type EscalationTier = 'lite' | 'medium' | 'high';
|
|
326
|
+
|
|
327
|
+
export interface VerifyFailInputs {
|
|
328
|
+
/** Tier of the attempt that just failed verification. */
|
|
329
|
+
failed_tier: EscalationTier;
|
|
330
|
+
/** Provenance of the slice's tier (telemetry `tier_source`). */
|
|
331
|
+
tier_source: 'static' | 'inferred' | 'inherit';
|
|
332
|
+
/** Consecutive failed attempts on this slice INCLUDING the one that just failed. */
|
|
333
|
+
consecutive_failures: number;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export interface VerifyFailDecision {
|
|
337
|
+
action: 'escalate' | 'retry-same-tier' | 'slice-failed';
|
|
338
|
+
/** Tier for the next attempt; null when the slice is failed. */
|
|
339
|
+
next_tier: EscalationTier | null;
|
|
340
|
+
reason: string;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const TIER_ABOVE: Record<EscalationTier, EscalationTier | null> = {
|
|
344
|
+
lite: 'medium',
|
|
345
|
+
medium: 'high',
|
|
346
|
+
high: null,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export function escalateOnVerifyFail(inp: VerifyFailInputs): VerifyFailDecision {
|
|
350
|
+
// Outside the downshift path → existing same-tier retry semantics, budget unchanged.
|
|
351
|
+
if (inp.tier_source === 'inherit') {
|
|
352
|
+
if (budgetHalt(inp.consecutive_failures)) {
|
|
353
|
+
return { action: 'slice-failed', next_tier: null, reason: 'N=3 budget exhausted on session tier' };
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
action: 'retry-same-tier',
|
|
357
|
+
next_tier: inp.failed_tier,
|
|
358
|
+
reason: 'inherit slice — no downshift decision, same-tier retry semantics unchanged',
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Downshifted slice: two failed attempts mark the slice failed — the third
|
|
363
|
+
// "attempt" of the N=3 budget is the orchestrator's replan at session tier.
|
|
364
|
+
if (inp.consecutive_failures >= 2) {
|
|
365
|
+
return {
|
|
366
|
+
action: 'slice-failed',
|
|
367
|
+
next_tier: null,
|
|
368
|
+
reason: 'two failed attempts on a downshifted slice — mark failed, orchestrator replans at session tier',
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const above = TIER_ABOVE[inp.failed_tier];
|
|
373
|
+
if (above === null) {
|
|
374
|
+
return {
|
|
375
|
+
action: 'slice-failed',
|
|
376
|
+
next_tier: null,
|
|
377
|
+
reason: 'verify-fail on the highest tier — no tier above, mark failed and replan',
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
action: 'escalate',
|
|
383
|
+
next_tier: above,
|
|
384
|
+
reason: `verify-fail on ${inp.failed_tier} → re-dispatch one tier up (${above}); consumes the N=3 budget`,
|
|
385
|
+
};
|
|
386
|
+
}
|