@event4u/agent-config 14.0.0 → 14.2.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 +95 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/cmd_conformance.js +112 -8
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/hooks/dispatch.js +199 -18
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +114 -9
- package/package.json +1 -1
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/hook-token-budget.json +6 -0
- package/src/scripts/_lib/gate_ledger.ts +5 -1
- package/src/scripts/_lib/skill_catalogue_series.ts +91 -0
- package/src/scripts/build_archive_index.ts +62 -3
- package/src/scripts/check_gate_coverage.ts +93 -0
- package/src/scripts/check_pr_ci_current.ts +86 -3
- package/src/scripts/dispatch_r2_reviewer.ts +169 -15
- package/src/scripts/git_authorization_hook.ts +186 -1
- package/src/scripts/hooks/block_unauthorized_git.ts +62 -6
- package/src/scripts/hooks/skill_route_hook.ts +172 -11
- package/src/scripts/install-hooks.sh +26 -0
|
@@ -43,6 +43,66 @@
|
|
|
43
43
|
* need an invalidation story for a tree that changes under the session, and
|
|
44
44
|
* 12.3 ms does not buy one.
|
|
45
45
|
*
|
|
46
|
+
* HOST-HONEST, AND FAIL-OPEN BY CONSTRUCTION (road-to-catalogue-host-fit Phase
|
|
47
|
+
* 3). The ranker reads the on-disk tree; the host does not necessarily deliver
|
|
48
|
+
* it. A skill the host truncated is still rankable and still pointable, and the
|
|
49
|
+
* pointer then names a skill whose description the model never received — worse
|
|
50
|
+
* than silence, because a pointer reads as a delivered capability. That is the
|
|
51
|
+
* roadmap's D-4, and it is MEASURED rather than assumed: 16 of 16 bare entries
|
|
52
|
+
* in the 2026-08-12 claude observation are still in this ranker's catalogue
|
|
53
|
+
* (`capture_skill_catalogue --pointable-bare`), among them `design-review`,
|
|
54
|
+
* `design-intelligence` and `fe-design` — skills this estate's own rules route
|
|
55
|
+
* to. So the pointer set is filtered against the latest per-entry observation
|
|
56
|
+
* for the CURRENT host.
|
|
57
|
+
*
|
|
58
|
+
* The filter only ever narrows on a positive reading, and every uncertain state
|
|
59
|
+
* resolves to no filtering at all: no observation log, no record for this host,
|
|
60
|
+
* a host that publishes no per-entry list, a malformed line, an unknown host, or
|
|
61
|
+
* any throw. `knownBareNames` returns `null` for each of them and
|
|
62
|
+
* `filterKnownBare` treats `null` as a pass-through, so behaviour is
|
|
63
|
+
* byte-identical to the unfiltered line. A filter that quietly hides skills on
|
|
64
|
+
* missing data is worse than the divergence it treats — the roadmap's own words,
|
|
65
|
+
* and the reason fail-open is a construction here rather than a convention.
|
|
66
|
+
*
|
|
67
|
+
* IT COSTS ZERO ON THE PATH THE HEADER ABOVE PROMISES IS FREE, AND 0.015 ms ON
|
|
68
|
+
* THE ONE THAT PAYS. The log read is passed in as a thunk, not a value, and
|
|
69
|
+
* `routePointers` calls it only after the term floor AND the rank both pass. A
|
|
70
|
+
* prompt below `MIN_TASK_TERMS` therefore still reads no files at all, which is
|
|
71
|
+
* the claim the 0 ms paragraph makes and an eagerly-read log would have quietly
|
|
72
|
+
* falsified.
|
|
73
|
+
*
|
|
74
|
+
* The FIRING path was measured rather than assumed, because R2 finding 10 is
|
|
75
|
+
* right that the paragraph above describes a path this branch changed. Measured
|
|
76
|
+
* 2026-08-18 on this tree, same method as the 12.3 ms figure: `knownBareForHost`
|
|
77
|
+
* alone is **0.015 ms median / 0.022 ms p95** (n=2000, warm) against a ranked
|
|
78
|
+
* pass of 8.3 ms median — three orders of magnitude below the ranker it rides
|
|
79
|
+
* on, and under 0.01 % of the slot's 250 ms p95 budget. The log is seven lines;
|
|
80
|
+
* a cache would need an invalidation story and 0.015 ms does not buy one, which
|
|
81
|
+
* is the same reasoning the ranker's own no-cache decision records.
|
|
82
|
+
*
|
|
83
|
+
* Note what the existing bench does NOT cover, so nobody reads it as coverage:
|
|
84
|
+
* `bench_hook_latency`'s synthetic payload carries neither `prompt` nor
|
|
85
|
+
* `platform`, so this concern returns before the ranker on every bench
|
|
86
|
+
* iteration. The figures above come from a direct probe, not from that harness.
|
|
87
|
+
*
|
|
88
|
+
* THE FLOOR APPLIES TO WHAT IS DELIVERED, NOT TO WHAT WAS RANKED. Filtering
|
|
89
|
+
* happens BEFORE `MIN_TOP_SCORE` and before the `TOP_K` slice, so the confidence
|
|
90
|
+
* question is asked of the best pointer the model can actually use. One rule, in
|
|
91
|
+
* one order: rank → drop the undeliverable → apply the floor → take the top
|
|
92
|
+
* three.
|
|
93
|
+
*
|
|
94
|
+
* That ordering makes silence a real outcome, and the number says so rather than
|
|
95
|
+
* the prose guessing: on `"review the authorization policy and tenant scope for
|
|
96
|
+
* this endpoint"` the ranker returns `authz-review` at 47 and the next entry at
|
|
97
|
+
* 23, against a floor of 31. Suppress the top-1 and NOTHING clears the floor —
|
|
98
|
+
* the line goes silent instead of naming a 23/100 pointer because the 47 one was
|
|
99
|
+
* undeliverable. That is the intended reading of a floor calibrated for
|
|
100
|
+
* confidence, and promoting a sub-floor pointer to fill the gap would be the
|
|
101
|
+
* "advisory worse than silence" failure this file's risk paragraph ranks first.
|
|
102
|
+
* `suppressed` is what distinguishes that silence from an unranked prompt, and
|
|
103
|
+
* `tests/hooks/skill_route_hook.test.ts` pins both cases against the real
|
|
104
|
+
* corpus rather than a constructed one.
|
|
105
|
+
*
|
|
46
106
|
* NO ADOPTION THRESHOLD IS COMMITTED HERE. Whether the line changes behaviour
|
|
47
107
|
* is a question for data, not for this header: `skill_route_pointer_rate` is
|
|
48
108
|
* registered in `hook-token-budget.json` under the same owner/review discipline
|
|
@@ -51,6 +111,13 @@
|
|
|
51
111
|
* removed. The score floor above is a TRIGGER calibration and is measured; the
|
|
52
112
|
* adoption threshold is an OUTCOME claim and is deliberately absent.
|
|
53
113
|
*
|
|
114
|
+
* `skill_route_bare_suppression_rate` is registered beside it under the same
|
|
115
|
+
* owner, review date and kill discipline, and inherits the same refusal: how
|
|
116
|
+
* often the filter fires is a number to collect, not one to promise. Its
|
|
117
|
+
* numerator is carried in the warn `reason` below — `N suppressed` — because no
|
|
118
|
+
* dedicated counter exists, and that gap is registered as a gap rather than
|
|
119
|
+
* implied to be automatic.
|
|
120
|
+
*
|
|
54
121
|
* DELIVERY PATH — the verified one, copied rather than re-derived. A
|
|
55
122
|
* `user_prompt_submit` concern returning `{decision:"allow", context:…}` at
|
|
56
123
|
* exit 0 produces zero stdout: `host_semantics.emitFor` drops the field for
|
|
@@ -73,7 +140,8 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
73
140
|
|
|
74
141
|
import { readHookStdin } from "./hook_stdin.js";
|
|
75
142
|
import { isSyntheticPrompt } from "../_lib/prompt_shape.js";
|
|
76
|
-
import { resolveSkillsRoot } from "../_lib/skill_catalogue.js";
|
|
143
|
+
import { OBSERVATION_LOG, readObservationLog, resolveSkillsRoot } from "../_lib/skill_catalogue.js";
|
|
144
|
+
import { knownBareNames } from "../_lib/skill_catalogue_series.js";
|
|
77
145
|
import { _tokenize, rank, type RankRow } from "../skill_tools/score_skill_relevance.js";
|
|
78
146
|
|
|
79
147
|
const EXIT_ALLOW = 0;
|
|
@@ -119,31 +187,115 @@ function _isObject(v: unknown): v is JsonObject {
|
|
|
119
187
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
120
188
|
}
|
|
121
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Names this host is known to have delivered bare, or `null` for no filtering.
|
|
192
|
+
*
|
|
193
|
+
* Every failure mode collapses to `null` — an unknown host, a missing log, an
|
|
194
|
+
* unreadable one, a host that enumerates nothing, a throw from anywhere in the
|
|
195
|
+
* read. The three-way distinction `knownBareNames` draws is preserved: an empty
|
|
196
|
+
* set means *measured clean*, `null` means *never measured*, and only the second
|
|
197
|
+
* one is produced here by accident.
|
|
198
|
+
*/
|
|
199
|
+
export function knownBareForHost(workspaceRoot: string, host: string | null): Set<string> | null {
|
|
200
|
+
if (host === null || host === "") return null;
|
|
201
|
+
try {
|
|
202
|
+
return knownBareNames(readObservationLog(path.join(workspaceRoot, OBSERVATION_LOG)), host);
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Drop the ranked rows this host is known to have delivered bare.
|
|
210
|
+
*
|
|
211
|
+
* `null` is a pass-through and returns the input order untouched, which is the
|
|
212
|
+
* fail-open half of the header's contract and what makes an absent observation
|
|
213
|
+
* byte-identical to today.
|
|
214
|
+
*/
|
|
215
|
+
export function filterKnownBare(
|
|
216
|
+
rows: readonly RankRow[],
|
|
217
|
+
bare: Set<string> | null,
|
|
218
|
+
): RankRow[] {
|
|
219
|
+
if (bare === null || bare.size === 0) return [...rows];
|
|
220
|
+
return rows.filter(([name]) => !bare.has(name));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** What one routing pass decided, including what it refused to name. */
|
|
224
|
+
export interface RouteDecision {
|
|
225
|
+
rows: RankRow[];
|
|
226
|
+
/**
|
|
227
|
+
* Would-be POINTERS dropped as known-bare — the suppression metric's numerator.
|
|
228
|
+
*
|
|
229
|
+
* Scoped to the pointer window (`TOP_K` of the unfiltered ranking), not to the
|
|
230
|
+
* whole ranked list. R2 finding 1: `rank` returns every non-zero-scoring skill
|
|
231
|
+
* and this catalogue has hundreds, so counting drops across all of them would
|
|
232
|
+
* let a bare name at rank 40 — never pointable, never a loss — bump the count
|
|
233
|
+
* on almost every fire. The registered metric defines this as skills the
|
|
234
|
+
* ranker wanted to POINT AT, and its upper falsifier would otherwise fire on a
|
|
235
|
+
* perfectly healthy join.
|
|
236
|
+
*/
|
|
237
|
+
suppressed: number;
|
|
238
|
+
}
|
|
239
|
+
|
|
122
240
|
/**
|
|
123
241
|
* Rank a prompt and return the pointers worth injecting, or `[]` for silence.
|
|
124
242
|
*
|
|
125
243
|
* Total by construction: a missing root, an unreadable catalogue, or a ranker
|
|
126
244
|
* error all resolve to `[]`. A routing advisory that can crash a turn is worse
|
|
127
245
|
* than one that says nothing.
|
|
246
|
+
*
|
|
247
|
+
* `bareProvider` is a THUNK, deliberately — see the header's zero-cost
|
|
248
|
+
* paragraph. It is called only once both the term floor and the rank have
|
|
249
|
+
* passed, so the short-prompt path still touches no files. Omitting it disables
|
|
250
|
+
* filtering entirely, which is what keeps every caller that predates the filter
|
|
251
|
+
* behaviourally unchanged.
|
|
128
252
|
*/
|
|
129
|
-
export function
|
|
130
|
-
|
|
253
|
+
export function routeDecision(
|
|
254
|
+
prompt: string,
|
|
255
|
+
skillsDir: string | null,
|
|
256
|
+
bareProvider?: () => Set<string> | null,
|
|
257
|
+
): RouteDecision {
|
|
258
|
+
const silent: RouteDecision = { rows: [], suppressed: 0 };
|
|
259
|
+
if (skillsDir === null) return silent;
|
|
131
260
|
// Denominator floor BEFORE the catalogue read: a prompt too short to score
|
|
132
261
|
// meaningfully cannot produce a pointer worth 337 file reads either.
|
|
133
262
|
try {
|
|
134
|
-
if (_tokenize(prompt).size < MIN_TASK_TERMS) return
|
|
263
|
+
if (_tokenize(prompt).size < MIN_TASK_TERMS) return silent;
|
|
135
264
|
} catch {
|
|
136
|
-
return
|
|
265
|
+
return silent;
|
|
137
266
|
}
|
|
138
267
|
let rows: RankRow[];
|
|
139
268
|
try {
|
|
140
269
|
rows = rank(prompt, skillsDir);
|
|
141
270
|
} catch {
|
|
142
|
-
return
|
|
271
|
+
return silent;
|
|
272
|
+
}
|
|
273
|
+
let bare: Set<string> | null = null;
|
|
274
|
+
if (bareProvider !== undefined) {
|
|
275
|
+
try {
|
|
276
|
+
bare = bareProvider();
|
|
277
|
+
} catch {
|
|
278
|
+
bare = null; // fail open, never silent-narrow
|
|
279
|
+
}
|
|
143
280
|
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
281
|
+
const deliverable = filterKnownBare(rows, bare);
|
|
282
|
+
// The pointer WINDOW, not the ranked list — see `RouteDecision.suppressed`.
|
|
283
|
+
const suppressed =
|
|
284
|
+
bare === null ? 0 : rows.slice(0, TOP_K).filter(([name]) => bare.has(name)).length;
|
|
285
|
+
// The floor asks its question of the best DELIVERABLE pointer, not of a
|
|
286
|
+
// pointer the model cannot use. See the header's one-rule-one-order note.
|
|
287
|
+
const top = deliverable[0];
|
|
288
|
+
if (top === undefined || top[1] < MIN_TOP_SCORE) return { rows: [], suppressed };
|
|
289
|
+
return { rows: deliverable.slice(0, TOP_K), suppressed };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** The rows alone. Kept because most callers and fixtures want only these. */
|
|
293
|
+
export function routePointers(
|
|
294
|
+
prompt: string,
|
|
295
|
+
skillsDir: string | null,
|
|
296
|
+
bareProvider?: () => Set<string> | null,
|
|
297
|
+
): RankRow[] {
|
|
298
|
+
return routeDecision(prompt, skillsDir, bareProvider).rows;
|
|
147
299
|
}
|
|
148
300
|
|
|
149
301
|
/**
|
|
@@ -191,13 +343,22 @@ export function main(): number {
|
|
|
191
343
|
if (!prompt) return EXIT_ALLOW;
|
|
192
344
|
if (isSyntheticPrompt(prompt)) return EXIT_ALLOW; // harness turn, not a task
|
|
193
345
|
|
|
194
|
-
const
|
|
346
|
+
const root = _workspaceRoot(env);
|
|
347
|
+
const platform = env["platform"];
|
|
348
|
+
const host = typeof platform === "string" && platform ? platform : null;
|
|
349
|
+
const { rows, suppressed } = routeDecision(prompt, resolveSkillsRoot(root), () =>
|
|
350
|
+
knownBareForHost(root, host),
|
|
351
|
+
);
|
|
195
352
|
if (rows.length === 0) return EXIT_ALLOW; // silence is the default
|
|
196
353
|
|
|
197
354
|
process.stdout.write(
|
|
198
355
|
`${JSON.stringify({
|
|
199
356
|
decision: "warn",
|
|
200
|
-
reason:
|
|
357
|
+
reason:
|
|
358
|
+
`skill-route: ${rows.length} pointer(s), top ${rows[0]![1]}/100` +
|
|
359
|
+
// The suppression metric's numerator. Named only when non-zero, so the
|
|
360
|
+
// common line keeps its shape and its registered byte budget.
|
|
361
|
+
(suppressed > 0 ? `, ${suppressed} suppressed as host-bare` : ""),
|
|
201
362
|
additional_context: buildRouteLine(rows),
|
|
202
363
|
})}\n`,
|
|
203
364
|
);
|
|
@@ -35,6 +35,32 @@ cat > "$HOOKS_DIR/pre-push" << 'EOF'
|
|
|
35
35
|
# so any derived-output drift blocks the push here. Runtime ~15-40s (it
|
|
36
36
|
# regenerates the tool trees) — cheaper than a red CI run and a fixup re-push.
|
|
37
37
|
|
|
38
|
+
# A delete-only push has no content to gate, so every check below is answering
|
|
39
|
+
# a question nobody asked. Measured 2026-08-18 during the 14.0.0 release:
|
|
40
|
+
# `git push origin --delete release/14.0.0` was refused by the branch-freshness
|
|
41
|
+
# gate because the CHECKED-OUT branch was behind main — a fact with no bearing
|
|
42
|
+
# on removing a remote ref. The deletion had to go through the GitHub API to
|
|
43
|
+
# happen at all, which routes around the whole hook rather than around one gate.
|
|
44
|
+
#
|
|
45
|
+
# Git feeds pre-push one line per ref on stdin:
|
|
46
|
+
# <local ref> <local sha> <remote ref> <remote sha>
|
|
47
|
+
# and a deletion carries the all-zero local sha. Reading stdin is safe here:
|
|
48
|
+
# nothing below consumes it.
|
|
49
|
+
delete_only=1
|
|
50
|
+
saw_ref=0
|
|
51
|
+
while read -r _local_ref local_sha _remote_ref _remote_sha; do
|
|
52
|
+
[ -z "${local_sha:-}" ] && continue
|
|
53
|
+
saw_ref=1
|
|
54
|
+
# Any non-zero character means a real object is being pushed.
|
|
55
|
+
case "$local_sha" in
|
|
56
|
+
*[!0]*) delete_only=0 ;;
|
|
57
|
+
esac
|
|
58
|
+
done
|
|
59
|
+
if [ "$saw_ref" = "1" ] && [ "$delete_only" = "1" ]; then
|
|
60
|
+
echo "🗑️ Delete-only push — no ref advances, so the content gates have nothing to check. Skipping."
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
38
64
|
fail=0
|
|
39
65
|
|
|
40
66
|
if ! command -v task >/dev/null 2>&1; then
|