fix(context): require a name or trigger anchor for implicit admission

This commit is contained in:
montjeffrey
2026-10-07 16:16:57 +00:00
parent bd13428151
commit deeb27d87b
4 changed files with 20 additions and 2 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ A task input contains caller-assigned `sessionId`, `taskId`, positive integer `r
}
```
Auto uses explicit user IDs first, then a completed pinned decision, an unambiguous ranked match, one cited skill name, or admitted agent-proposed IDs. Ambiguous free text shortlists up to five candidates for a bounded proposal. Manual uses explicit IDs; suggest emits a proposal without bodies. `--load` returns selected UTF-8 instructions and declared required resources, capped at 32,000 bytes across at most eight skills. `--task-input -` accepts one UTF-8 JSON object on standard input, capped at 65,536 bytes. These byte caps are output and transport bounds, not native tokenizer results.
Auto uses explicit user IDs first, then a completed pinned decision, an unambiguous ranked match anchored by a term in the skill name or curated triggers, one cited skill name, or admitted agent-proposed IDs. Ambiguous free text shortlists up to five candidates for a bounded proposal. Manual uses explicit IDs; suggest emits a proposal without bodies. `--load` returns selected UTF-8 instructions and declared required resources, capped at 32,000 bytes across at most eight skills. `--task-input -` accepts one UTF-8 JSON object on standard input, capped at 65,536 bytes. These byte caps are output and transport bounds, not native tokenizer results.
Save the returned `selection.receipt` as a separate JSON document to use `--previous receipt.json`. `--expected-digest` can bind a load to a prior selection digest. Source, trigger content, routing-policy version, profile, mode, exclusions, session, task revision, phase, and a digest of the query invalidate stale reuse. A pending proposal cannot be reused as a completed decision. Receipts are integrity checks for local operation, not an authorization signature.
@@ -88,4 +88,13 @@ Current combined verification after recovery:
- The default sandbox checkout's 5,000-path capture limit truncated a real Tier 1 install diff and failed closed. The reviewed ECC-029 sandbox implementation raises the bounded cap to 50,000, passes its 26-case boundary suite, and produced both final reports. The driver receipt binds its 51-file implementation digest `a84e09ab848b8cd05f33792c13734f7aabe16bfe16d50d8f8292eb5261a93c3a`.
- No real AI outcome call ran because `CODEX_API_KEY` was absent. Host ChatGPT authentication was neither copied nor exposed to the disposable evaluator.
## Auto-admission anchor (routing policy v5)
The 77-prompt routing corpus from #2945 (52 direct, 25 paraphrased) found one wrong implicit load. "two services both think they own the same record" admitted `skill:santa-method` on four description-only words (`two`, `both`, `they`, `same`) with BM25 24.3 and a 1.94 margin, clearing every v4 bar.
- RED: the prompt and the corpus check fail against v4 (`skill:santa-method` loaded).
- GREEN: auto admission and the tier-2 fallback also require one matched term in the skill name or curated triggers. Exact directive citations are unchanged.
- Corpus result: zero loads outside the expected set; 19 implicit admissions become 17. The other dropped admission, `skill:cost-tracking`, now defers to the bounded proposal instead of loading.
- The existing nine-query auto/agent corpus, fallback, launch, evaluation and retrieval suites pass unchanged.
These boundaries keep the shipped behavior distinct from the M1 release gate. Authenticated outcome observations, a complete Tier 2 disk diff, live-install migration, other-provider activation, whole-context token truth and release defaults remain unverified until their explicit prerequisites are available.
+2
View File
@@ -171,12 +171,14 @@ function searchRetrieval(index, query, { limit = 5 } = {}) {
.slice(0, limit)
.map(([document, score]) => {
const matched = [...new Set(queryTokens)].filter(term => document.weighted.has(term));
const anchorTerms = matched.filter(term => document.fields.name.includes(term) || document.fields.triggers.includes(term));
const exact = anchored.has(document);
return { id: document.entry.id, score: Math.round(score * 10000) / 10000, exact,
exactAlias: exact ? anchored.get(document) : undefined,
dense: Math.round((dense.get(document) || 0) * 10000) / 10000,
bm25: Math.round((bm25.get(document) || 0) * 10000) / 10000,
matchedTerms: matched,
anchorTerms,
description: document.entry.description.slice(0, 2048),
descriptionTruncated: document.entry.description.length > 2048 };
});
+8 -1
View File
@@ -26,9 +26,14 @@ const AUTO_ADMIT_MARGIN = 1.5;
const FALLBACK_MIN_BM25 = 12;
const FALLBACK_MIN_TERMS = 2;
const FALLBACK_MARGIN = 1.1;
// Both tiers also require at least one matched term in the skill's name or
// curated triggers. Description-only overlap on common words (two, both, same)
// can clear the score bars without naming the skill's subject.
const MIN_ANCHOR_TERMS = 1;
// v4: an explicit empty proposal (decline) is honored; the tier-2 fallback no
// longer overrides declines at the launch/selection call sites.
const ROUTING_POLICY_VERSION = 4;
// v5: implicit admission requires a name or trigger anchor term.
const ROUTING_POLICY_VERSION = 5;
const TASK_KEYS = new Set(['sessionId', 'taskId', 'revision', 'phase', 'query', 'explicitIds', 'proposedIds', 'noWorkflow']);
function validateTask(task) {
@@ -210,6 +215,7 @@ function resolveTaskContext({ repoRoot = DEFAULT_REPO_ROOT, task, profileId = 'l
const top = candidates[0];
const second = candidates[1];
if (top.bm25 >= AUTO_ADMIT_MIN_BM25 && top.matchedTerms.length >= AUTO_ADMIT_MIN_TERMS
&& top.anchorTerms.length >= MIN_ANCHOR_TERMS
&& (!second || top.bm25 >= AUTO_ADMIT_MARGIN * (second.bm25 || 0))) {
autoSelection = { id: top.id, bm25: top.bm25, matchedTerms: top.matchedTerms.length, exact: false };
}
@@ -221,6 +227,7 @@ function resolveTaskContext({ repoRoot = DEFAULT_REPO_ROOT, task, profileId = 'l
const top = candidates[0];
const second = candidates[1];
if (top.bm25 >= FALLBACK_MIN_BM25 && top.matchedTerms.length >= FALLBACK_MIN_TERMS
&& top.anchorTerms.length >= MIN_ANCHOR_TERMS
&& (!second || top.bm25 >= FALLBACK_MARGIN * (second.bm25 || 0))) {
fallback = { id: top.id, bm25: top.bm25, matchedTerms: top.matchedTerms.length };
}