@event4u/agent-config 9.2.0 → 9.4.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 +111 -0
- package/CONTRIBUTING.md +14 -0
- package/README.md +12 -4
- package/dist/agent-src/commands/contribution-precheck.md +100 -0
- package/dist/agent-src/commands/fix/route.md +49 -0
- package/dist/agent-src/commands/fix.md +4 -2
- package/dist/agent-src/commands/tdd/green.md +32 -0
- package/dist/agent-src/commands/tdd/red.md +33 -0
- package/dist/agent-src/commands/tdd/refactor.md +31 -0
- package/dist/agent-src/commands/tdd.md +50 -0
- package/dist/agent-src/commands/team.md +4 -0
- package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +81 -2
- package/dist/agent-src/contexts/model-recommendations.md +6 -0
- package/dist/agent-src/rules/architecture.md +7 -0
- package/dist/agent-src/rules/token-efficiency.md +7 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +5 -0
- package/dist/agent-src/skills/ai-council/SKILL.md +3 -0
- package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -0
- package/dist/agent-src/skills/decision-record/SKILL.md +35 -1
- package/dist/agent-src/skills/decision-record/evals/triggers.json +82 -12
- package/dist/agent-src/skills/decision-record/references/weighted-matrix.md +119 -0
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +142 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +1 -0
- package/dist/agent-src/skills/judge-code-quality/SKILL.md +1 -0
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +1 -0
- package/dist/agent-src/skills/learning-tutor/SKILL.md +215 -0
- package/dist/agent-src/skills/persona-improvement/SKILL.md +81 -0
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +10 -0
- package/dist/agent-src/skills/rice-prioritization/SKILL.md +1 -0
- package/dist/agent-src/skills/stakeholder-tradeoff/SKILL.md +3 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +14 -24
- 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 +253 -30
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +19 -10
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +15 -6
- package/dist/mcp/registry-manifest.json +3 -3
- package/docs/CLAIMS.md +16 -2
- package/docs/announcements/2026-07-honest-cost-sweep-launch.md +84 -0
- package/docs/anti-reskin-gate.md +60 -0
- package/docs/architecture.md +3 -3
- package/docs/catalog.md +303 -278
- package/docs/command-flows.md +10 -3
- package/docs/comparison.yaml +10 -0
- package/docs/contracts/adoption-signal-floor.md +17 -0
- package/docs/contracts/ai-council-config.md +25 -0
- package/docs/contracts/command-clusters.md +2 -1
- package/docs/contracts/mcp-tool-inventory.md +25 -22
- package/docs/contracts/tier-3-contrib-plugin.md +12 -4
- package/docs/decisions/ADR-122-adversarial-verification-council.md +1 -1
- package/docs/development.md +10 -1
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/component-oriented-and-oop-development.md +181 -0
- package/docs/proof.md +10 -6
- package/docs/self-review-gate.md +14 -0
- package/docs/setup/per-ide/antigravity.md +8 -3
- package/docs/us-vs-the-category.md +38 -0
- package/docs/wedge/production-validator/README.md +24 -0
- package/docs/wedge/production-validator/first-run-check.sh +42 -0
- package/package.json +3 -2
- package/src/scripts/_lib/catalog_score.ts +64 -0
- package/src/scripts/_lib/shingle_similarity.ts +146 -0
- package/src/scripts/ai_council/clients.ts +50 -1
- package/src/scripts/ai_council/config.ts +28 -3
- package/src/scripts/ai_council/low_impact.ts +16 -0
- package/src/scripts/ai_council/necessity.ts +11 -0
- package/src/scripts/ai_council/orchestrator.ts +78 -26
- package/src/scripts/ai_council/pricing.ts +59 -0
- package/src/scripts/ai_council/prompts.ts +7 -0
- package/src/scripts/ai_council/stance_tally.ts +42 -5
- package/src/scripts/build_catalog_index.ts +188 -0
- package/src/scripts/council_cli.ts +56 -1
- package/src/scripts/generate_index.ts +6 -3
- package/src/scripts/hook_manifest.yaml +17 -6
- package/src/scripts/install-hooks.sh +32 -17
- package/src/scripts/lint_originality.ts +464 -0
- package/src/scripts/mcp_server/catalog_tools.ts +114 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +46 -0
- package/src/scripts/pr_url_reminder_hook.ts +177 -0
- package/src/scripts/schemas/flow.schema.json +17 -0
- package/src/scripts/schemas/skill.schema.json +166 -32
- package/src/scripts/self_review_gate.test.ts +48 -0
- package/src/scripts/self_review_gate.ts +73 -5
- package/src/scripts/validate_flow_teams.ts +127 -0
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Antigravity (Google's agentic IDE) reads the Anthropic-shaped
|
|
4
4
|
markdown skill bundle from its user-scope anchor `~/.agents/`. The
|
|
5
|
-
package deploys
|
|
6
|
-
|
|
5
|
+
package deploys the universal skill bundle at global scope, the same
|
|
6
|
+
projection its other bundle hosts get (codex, continue, roocode and
|
|
7
|
+
the rest) — global-only by design, not a pending gap.
|
|
7
8
|
|
|
8
9
|
## Prerequisites
|
|
9
10
|
|
|
@@ -25,7 +26,11 @@ Populates:
|
|
|
25
26
|
- `~/.agents/personas/` — review-lens personas
|
|
26
27
|
|
|
27
28
|
(Project-scope `--tools=antigravity` is rejected with exit code 1
|
|
28
|
-
—
|
|
29
|
+
— every bundle host is global-only; the package writes the shared
|
|
30
|
+
bundle to a user-scope anchor, not into the project. Antigravity does
|
|
31
|
+
have a native project `.agents/` convention (`skills/`, `agents.md`,
|
|
32
|
+
`workflows/`), but a project-scope projection is not part of the
|
|
33
|
+
shipped surface.)
|
|
29
34
|
|
|
30
35
|
## How to use
|
|
31
36
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Us vs. the category — the honest version
|
|
2
|
+
|
|
3
|
+
<!-- claim:positioning-honest-nulls -->**The only agent layer that publishes
|
|
4
|
+
the runs where it changed nothing.**<!-- /claim --> That line is deliberately
|
|
5
|
+
falsifiable: find a comparable agent layer publishing its own honest-null
|
|
6
|
+
benchmark runs and it updates — that is the point. The category's growth
|
|
7
|
+
playbook is headline numbers nobody can re-run; ours is a claims ledger CI
|
|
8
|
+
refuses to let drift.
|
|
9
|
+
|
|
10
|
+
This page is the adoption-context frame around the CI-locked comparison table.
|
|
11
|
+
The table itself lives on the proof page — every "our evidence" pointer is
|
|
12
|
+
resolved by `task check-comparison` on every build, the category column only
|
|
13
|
+
ever describes what is publicly observable, and no competitor is named or
|
|
14
|
+
counter-claimed:
|
|
15
|
+
|
|
16
|
+
**→ [The comparison table, CI-checked on every build](proof.md#4-what-is-checkable--us-vs-the-category)**
|
|
17
|
+
|
|
18
|
+
## What the rows mean for an adopter
|
|
19
|
+
|
|
20
|
+
- **No runtime.** Nothing to keep alive, nothing that writes state behind
|
|
21
|
+
you — the whole layer compiles into your host agent's own config surface.
|
|
22
|
+
- **Published nulls.** The benchmark page keeps the runs where the package
|
|
23
|
+
changed *nothing* — including the persona-theater question: swapping the
|
|
24
|
+
advisor identities was measured at ~zero effect while the provider choice
|
|
25
|
+
mattered ~15× more, so persona identity was published as a placebo
|
|
26
|
+
(<!-- claim:persona-identity-placebo-null -->the persona-identity A/B is a
|
|
27
|
+
backed honest null<!-- /claim -->) instead of shipped as a feature. A
|
|
28
|
+
council implementation with falsifiable verdicts — including about itself.
|
|
29
|
+
- **A wedge, not a funnel trap.** The 30-second entry is one read-only
|
|
30
|
+
subagent with a scoped, published eval behind its promise
|
|
31
|
+
([what it catches](wedge/production-validator/README.md)) — the platform is
|
|
32
|
+
the second date, not the first.
|
|
33
|
+
|
|
34
|
+
## Verify it yourself
|
|
35
|
+
|
|
36
|
+
Every command on the [proof page § Verify it yourself](proof.md#5-verify-it-yourself)
|
|
37
|
+
runs on a fresh checkout. If a claim here does not resolve, CI is red before
|
|
38
|
+
you ever read it.
|
|
@@ -7,6 +7,14 @@ system — not just green tests over hollow code.
|
|
|
7
7
|
|
|
8
8
|
One file. No install of anything else. No runtime, no daemon, no account.
|
|
9
9
|
|
|
10
|
+
<!-- claim:wedge-hollow-detection -->On its published eval it returned the
|
|
11
|
+
correct verdict on both fixtures — `NOT READY` with the exact `file:line` on a
|
|
12
|
+
planted hollow implementation, `READY` with zero spurious findings on the clean
|
|
13
|
+
control — at ~45k fewer tokens per task than the same check run inline
|
|
14
|
+
([evidence](../../../internal/bench/orchestration/pv-a3-results.md)). Two
|
|
15
|
+
planted fixtures on a Claude Code host — a scoped, reproducible promise, not a
|
|
16
|
+
hit-rate.<!-- /claim -->
|
|
17
|
+
|
|
10
18
|
## What it does
|
|
11
19
|
|
|
12
20
|
Before you call a feature done, `@production-validator` greps the shipped path
|
|
@@ -46,6 +54,22 @@ production-validator
|
|
|
46
54
|
Final gate: NOT READY — refund() is a stub; validate against the real provider.
|
|
47
55
|
```
|
|
48
56
|
|
|
57
|
+
## Optional: record your first run (local-only, opt-in)
|
|
58
|
+
|
|
59
|
+
Nothing is measured unless you run it. After your first verdict, you can keep a
|
|
60
|
+
private local record — one aggregate line, written into YOUR repo, no network:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bash <(curl -fsSL https://raw.githubusercontent.com/event4u-app/agent-config/main/docs/wedge/production-validator/first-run-check.sh) ready
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Outcomes: `ready` · `not-ready` · `abandoned`. The log
|
|
67
|
+
(`.claude/wedge-first-run.local.log`) never leaves your machine — the package
|
|
68
|
+
runs zero telemetry by default
|
|
69
|
+
([contract](../../contracts/adoption-signal-floor.md)). The same outcome
|
|
70
|
+
vocabulary feeds the proctored install-friction study
|
|
71
|
+
(`agents/recruit-sessions/_install-friction-runbook.md`).
|
|
72
|
+
|
|
49
73
|
## Why this one
|
|
50
74
|
|
|
51
75
|
It carries an anti-hallucination discipline (Evidence-First): "done" is a claim
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Opt-in, local-only first-run check for the production-validator wedge.
|
|
3
|
+
#
|
|
4
|
+
# Default-off by construction: nothing runs unless YOU invoke this script.
|
|
5
|
+
# It writes ONE aggregate line to a local log in your repo and never opens a
|
|
6
|
+
# network connection — consistent with the package's no-telemetry posture
|
|
7
|
+
# (docs/contracts/adoption-signal-floor.md). Delete the log any time.
|
|
8
|
+
#
|
|
9
|
+
# Usage (after your first @production-validator verdict):
|
|
10
|
+
# bash first-run-check.sh ready # first verdict was READY
|
|
11
|
+
# bash first-run-check.sh not-ready # first verdict was NOT READY
|
|
12
|
+
# bash first-run-check.sh abandoned # you gave up before a verdict
|
|
13
|
+
#
|
|
14
|
+
# The same outcome vocabulary feeds the B9 install-friction study
|
|
15
|
+
# (agents/recruit-sessions/_install-friction-runbook.md) when a session is
|
|
16
|
+
# proctored; unproctored users simply keep a private local record.
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
OUTCOME="${1:-}"
|
|
20
|
+
case "$OUTCOME" in
|
|
21
|
+
ready|not-ready|abandoned) ;;
|
|
22
|
+
*)
|
|
23
|
+
echo "usage: bash first-run-check.sh <ready|not-ready|abandoned>" >&2
|
|
24
|
+
exit 2
|
|
25
|
+
;;
|
|
26
|
+
esac
|
|
27
|
+
|
|
28
|
+
AGENT_FILE=".claude/agents/production-validator.md"
|
|
29
|
+
LOG_FILE=".claude/wedge-first-run.local.log"
|
|
30
|
+
|
|
31
|
+
if [ ! -f "$AGENT_FILE" ]; then
|
|
32
|
+
echo "NOT INSTALLED — $AGENT_FILE missing. Install first:" >&2
|
|
33
|
+
echo " mkdir -p .claude/agents && curl -fsSL https://raw.githubusercontent.com/event4u-app/agent-config/main/docs/wedge/production-validator/production-validator.md -o $AGENT_FILE" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# One aggregate line: date + outcome. No user, host, repo, or content data.
|
|
38
|
+
printf '%s wedge=production-validator first_run=%s\n' \
|
|
39
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$OUTCOME" >> "$LOG_FILE"
|
|
40
|
+
|
|
41
|
+
echo "Recorded locally in $LOG_FILE (never leaves this machine)."
|
|
42
|
+
echo "Add it to .gitignore if you don't want it committed."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.4.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,
|
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
"lint:ts": "eslint 'src/**/*.ts'",
|
|
71
71
|
"test:ts": "vitest run",
|
|
72
72
|
"prepack": "npm run build && npm run build:discovery && npm run build:mcp-manifest && node src/scripts/prepack-check.mjs",
|
|
73
|
-
"prepublishOnly": "./scripts-run src/scripts/check_release_includes_discovery"
|
|
73
|
+
"prepublishOnly": "./scripts-run src/scripts/check_release_includes_discovery",
|
|
74
|
+
"prepare": "[ -d .git ] && bash src/scripts/install-hooks.sh || true"
|
|
74
75
|
},
|
|
75
76
|
"publishConfig": {
|
|
76
77
|
"access": "public",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog search scoring — a thin field-boosted wrapper over the shared
|
|
3
|
+
* `LexicalIndex` (BM25 + trigram prefilter).
|
|
4
|
+
*
|
|
5
|
+
* DECISION (road-to-persona-library-harvest.md Phase 2.3): the roadmap sketched
|
|
6
|
+
* porting the upstream Hermes `_score` (token overlap + name/description
|
|
7
|
+
* boosts). We do NOT — `_lib/lexical_index.ts` already ships a calibrated BM25
|
|
8
|
+
* core (ADR-061-sanctioned, dependency-free) with a coverage² multi-term
|
|
9
|
+
* weighting and a trigram candidate prefilter. Maintaining a second lexical
|
|
10
|
+
* scorer would be duplication. Instead we reuse `LexicalIndex` and get the
|
|
11
|
+
* field boost by REPEATING the name (×3) and description (×2) terms in each
|
|
12
|
+
* doc's indexed text, so a query term hitting the name outranks the same term
|
|
13
|
+
* buried in a tag. No new scorer.
|
|
14
|
+
*/
|
|
15
|
+
import type { CatalogIndexEntry } from '../build_catalog_index.js';
|
|
16
|
+
import { LexicalIndex } from './lexical_index.js';
|
|
17
|
+
|
|
18
|
+
export interface CatalogFilter {
|
|
19
|
+
cls?: CatalogIndexEntry['cls'];
|
|
20
|
+
/** Match against the entry's tags (packs/domain/tier). */
|
|
21
|
+
pack?: string;
|
|
22
|
+
limit?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CatalogHit {
|
|
26
|
+
entry: CatalogIndexEntry;
|
|
27
|
+
score: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Field-boosted searchable text: name ×3, description ×2, tags ×1. */
|
|
31
|
+
function _docText(e: CatalogIndexEntry): string {
|
|
32
|
+
return [e.name, e.name, e.name, e.description, e.description, ...e.tags].join(' ');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function _filter(entries: readonly CatalogIndexEntry[], f: CatalogFilter): CatalogIndexEntry[] {
|
|
36
|
+
return entries.filter((e) => {
|
|
37
|
+
if (f.cls && e.cls !== f.cls) return false;
|
|
38
|
+
if (f.pack && !e.tags.includes(f.pack)) return false;
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Rank catalog entries against a free-text query. The index is built over the
|
|
45
|
+
* (filtered) candidate set so IDF reflects the searched scope. Empty or
|
|
46
|
+
* whitespace query → no hits (a search tool must not dump the catalog).
|
|
47
|
+
*/
|
|
48
|
+
export function searchCatalog(
|
|
49
|
+
entries: readonly CatalogIndexEntry[],
|
|
50
|
+
query: string,
|
|
51
|
+
filter: CatalogFilter = {},
|
|
52
|
+
): CatalogHit[] {
|
|
53
|
+
const q = query.trim();
|
|
54
|
+
if (q.length === 0) return [];
|
|
55
|
+
const candidates = _filter(entries, filter);
|
|
56
|
+
if (candidates.length === 0) return [];
|
|
57
|
+
|
|
58
|
+
const index = new LexicalIndex(candidates.map((e) => ({ id: e.id, text: _docText(e) })));
|
|
59
|
+
const byId = new Map(candidates.map((e) => [e.id, e]));
|
|
60
|
+
const limit = filter.limit && filter.limit > 0 ? filter.limit : 8;
|
|
61
|
+
|
|
62
|
+
const ranked = index.rank([q]).filter((r) => r.score > 0);
|
|
63
|
+
return ranked.slice(0, limit).map((r) => ({ entry: byId.get(r.id) as CatalogIndexEntry, score: r.score }));
|
|
64
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity-neutralized shingle-overlap primitive — the anti-reskin engine behind
|
|
3
|
+
* `lint_originality.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Deterministic, no embeddings, no network. This is a DIFFERENT primitive from
|
|
6
|
+
* `text_similarity.ts` (token-set Jaccard, dedup) and `audit_skill_overlap.ts`
|
|
7
|
+
* (keyword-cosine, advisory) — both of which a find-replace re-skin
|
|
8
|
+
* ("Laravel" → "Symfony", "Vietnam" → "Korea") can defeat, because the swapped
|
|
9
|
+
* proper nouns shift the token/keyword vectors enough to drop the score.
|
|
10
|
+
*
|
|
11
|
+
* The defence is two-fold:
|
|
12
|
+
* 1. NEUTRALIZE entities — replace framework / vendor / language / region /
|
|
13
|
+
* currency proper nouns with a single placeholder BEFORE comparison, so a
|
|
14
|
+
* find-replace re-skin collapses to the original text.
|
|
15
|
+
* 2. SHINGLE — compare k-word shingles (default k = 8), not a bag of tokens,
|
|
16
|
+
* so re-skins that only swap scattered nouns still share long verbatim
|
|
17
|
+
* runs, and unrelated files that happen to share vocabulary do not.
|
|
18
|
+
*
|
|
19
|
+
* Overlap is CONTAINMENT (`|a∩b| / min(|a|,|b|)`), not Jaccard: a small file
|
|
20
|
+
* copied verbatim into a large padded one must still score high — the exact
|
|
21
|
+
* shape of an embedded re-skin.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately does NOT touch `text_similarity.ts` or `memory_signal.ts`
|
|
24
|
+
* (byte-faithful parity contracts). This module is a new, additional layer.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Proper-noun families that a find-replace re-skin swaps. Neutralized to a
|
|
29
|
+
* single placeholder so the swap does not move the score. Word-boundary
|
|
30
|
+
* matched, case-insensitive. **Extend via PR, never inline at a call site** —
|
|
31
|
+
* the list is the calibration surface, and a scattered entity list drifts.
|
|
32
|
+
*
|
|
33
|
+
* Seed the list from OUR corpus (the frameworks / vendors / regions the suite
|
|
34
|
+
* actually names), not from any external source.
|
|
35
|
+
*/
|
|
36
|
+
export const ENTITY_TERMS: readonly string[] = [
|
|
37
|
+
// Frameworks / libraries
|
|
38
|
+
'laravel', 'symfony', 'react', 'vue', 'preact', 'angular', 'svelte', 'nextjs',
|
|
39
|
+
'next', 'nuxt', 'livewire', 'flux', 'inertia', 'blade', 'tailwind', 'eloquent',
|
|
40
|
+
'doctrine', 'pest', 'phpunit', 'vitest', 'jest', 'playwright', 'express',
|
|
41
|
+
'fastify', 'django', 'flask', 'rails', 'spring',
|
|
42
|
+
// Languages
|
|
43
|
+
'php', 'typescript', 'javascript', 'python', 'ruby', 'golang', 'rust', 'java',
|
|
44
|
+
'kotlin', 'swift', 'csharp', 'elixir',
|
|
45
|
+
// Vendors / platforms / services
|
|
46
|
+
'aws', 'gcp', 'azure', 'hetzner', 'cloudflare', 'vercel', 'netlify', 'stripe',
|
|
47
|
+
'paddle', 'paypal', 'github', 'gitlab', 'bitbucket', 'linear', 'jira',
|
|
48
|
+
'confluence', 'slack', 'sentry', 'datadog', 'grafana', 'prometheus', 'redis',
|
|
49
|
+
'postgres', 'postgresql', 'mysql', 'mariadb', 'sqlite', 'mongodb', 'kafka',
|
|
50
|
+
'rabbitmq', 'terraform', 'terragrunt', 'kubernetes', 'docker', 'ansible',
|
|
51
|
+
'openai', 'anthropic', 'gemini', 'claude',
|
|
52
|
+
// Cloud regions / locales that re-skins swap
|
|
53
|
+
'us-east-1', 'us-west-2', 'eu-west-1', 'eu-central-1', 'ap-southeast-1',
|
|
54
|
+
// Currency codes / symbols
|
|
55
|
+
'usd', 'eur', 'gbp', 'chf', 'jpy',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const _ENTITY_RE = new RegExp(
|
|
59
|
+
`\\b(?:${ENTITY_TERMS.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})\\b`,
|
|
60
|
+
'gi',
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/** Placeholder every entity family collapses to. Distinct token so it shingles. */
|
|
64
|
+
export const ENTITY_PLACEHOLDER = 'entterm';
|
|
65
|
+
|
|
66
|
+
/** Word token — mirrors the upstream re-skin gate's word regex so a calibrated
|
|
67
|
+
* threshold ports across corpora. Digits, `+ . # _ -` kept (e.g. `c#`, `9.3.0`). */
|
|
68
|
+
const _WORD_RE = /[a-z0-9][a-z0-9+.#_-]*/g;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Lowercase, strip frontmatter + markdown syntax, then replace entity proper
|
|
72
|
+
* nouns with `ENTITY_PLACEHOLDER`. `extra` is an optional caller-supplied regex
|
|
73
|
+
* (global) of additional terms to neutralize for one call (e.g. a corpus-
|
|
74
|
+
* specific product name); it never mutates the shared list.
|
|
75
|
+
*/
|
|
76
|
+
export function neutralizeEntities(text: string, extra?: RegExp): string {
|
|
77
|
+
let t = text;
|
|
78
|
+
// Leading YAML frontmatter.
|
|
79
|
+
t = t.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
80
|
+
// Fenced code blocks (``` or ~~~).
|
|
81
|
+
t = t.replace(/^([`~]{3,}).*$[\s\S]*?^\1[ \t]*$/gm, ' ');
|
|
82
|
+
// HTML comments.
|
|
83
|
+
t = t.replace(/<!--[\s\S]*?-->/g, ' ');
|
|
84
|
+
// Inline code spans.
|
|
85
|
+
t = t.replace(/`[^`]*`/g, ' ');
|
|
86
|
+
// Links / images: keep the visible label, drop the target.
|
|
87
|
+
t = t.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1');
|
|
88
|
+
t = t.toLowerCase();
|
|
89
|
+
// Heading / emphasis / list / blockquote / table punctuation → spaces, so
|
|
90
|
+
// the same prose scores identically regardless of markdown decoration.
|
|
91
|
+
t = t.replace(/[#*_>|~]+/g, ' ');
|
|
92
|
+
if (extra) {
|
|
93
|
+
t = t.replace(extra, ENTITY_PLACEHOLDER);
|
|
94
|
+
}
|
|
95
|
+
t = t.replace(_ENTITY_RE, ENTITY_PLACEHOLDER);
|
|
96
|
+
return t;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Ordered word tokens of the neutralized text (no set — shingles need order). */
|
|
100
|
+
export function words(text: string): string[] {
|
|
101
|
+
return text.match(_WORD_RE) ?? [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Word-level k-shingles over the neutralized token stream. A file with fewer
|
|
106
|
+
* than `k` tokens yields the empty set (guarded downstream → score 0, never
|
|
107
|
+
* NaN). Shingles join on a single space so they are comparable across files.
|
|
108
|
+
*/
|
|
109
|
+
export function shingles(text: string, k = 8): Set<string> {
|
|
110
|
+
const toks = words(neutralizeEntities(text));
|
|
111
|
+
const out = new Set<string>();
|
|
112
|
+
if (toks.length < k) {
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
for (let i = 0; i + k <= toks.length; i++) {
|
|
116
|
+
out.add(toks.slice(i, i + k).join(' '));
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Containment overlap in percent: `100 * |a∩b| / min(|a|,|b|)`.
|
|
123
|
+
*
|
|
124
|
+
* Containment (not Jaccard) so a small file copied verbatim into a large padded
|
|
125
|
+
* one still scores high. Two empty shingle sets → 0 (a file too short to
|
|
126
|
+
* shingle carries no originality signal — never flag it). One empty → 0.
|
|
127
|
+
*/
|
|
128
|
+
export function overlapPercent(a: Set<string>, b: Set<string>): number {
|
|
129
|
+
const min = Math.min(a.size, b.size);
|
|
130
|
+
if (min === 0) {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
const [small, large] = a.size <= b.size ? [a, b] : [b, a];
|
|
134
|
+
let shared = 0;
|
|
135
|
+
for (const s of small) {
|
|
136
|
+
if (large.has(s)) {
|
|
137
|
+
shared++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return (100 * shared) / min;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Convenience: shingle both texts and score. */
|
|
144
|
+
export function shingleOverlap(a: string, b: string, k = 8): number {
|
|
145
|
+
return overlapPercent(shingles(a, k), shingles(b, k));
|
|
146
|
+
}
|
|
@@ -281,6 +281,27 @@ export abstract class ExternalAIClient {
|
|
|
281
281
|
user_prompt: string,
|
|
282
282
|
max_tokens?: number,
|
|
283
283
|
): CouncilResponse;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Cache-aware variant (A3 cross-round read unlock): the caller splits the
|
|
287
|
+
* user prompt into a byte-stable prefix (system + artefact — identical
|
|
288
|
+
* across rounds) and a volatile suffix (per-round critiques, stance
|
|
289
|
+
* contract). Clients with native prompt-cache support override this and
|
|
290
|
+
* place the cache breakpoint on the stable block only, so round N+1 READS
|
|
291
|
+
* the prefix instead of re-writing it. Default: plain concatenation —
|
|
292
|
+
* byte-identical to `ask()` for every transport without cache support.
|
|
293
|
+
*/
|
|
294
|
+
ask_split(
|
|
295
|
+
system_prompt: string,
|
|
296
|
+
stable_prompt: string,
|
|
297
|
+
volatile_suffix: string,
|
|
298
|
+
max_tokens?: number,
|
|
299
|
+
): CouncilResponse {
|
|
300
|
+
const user_prompt = volatile_suffix
|
|
301
|
+
? `${stable_prompt}${volatile_suffix}`
|
|
302
|
+
: stable_prompt;
|
|
303
|
+
return this.ask(system_prompt, user_prompt, max_tokens);
|
|
304
|
+
}
|
|
284
305
|
}
|
|
285
306
|
|
|
286
307
|
/** Shared ctor-options shape for the API clients. */
|
|
@@ -395,6 +416,27 @@ export class AnthropicClient extends ExternalAIClient {
|
|
|
395
416
|
user_prompt: string,
|
|
396
417
|
max_tokens: number = DEFAULT_MAX_TOKENS,
|
|
397
418
|
): CouncilResponse {
|
|
419
|
+
return this._ask_impl(system_prompt, user_prompt, '', max_tokens);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
override ask_split(
|
|
423
|
+
system_prompt: string,
|
|
424
|
+
stable_prompt: string,
|
|
425
|
+
volatile_suffix: string,
|
|
426
|
+
max_tokens: number = DEFAULT_MAX_TOKENS,
|
|
427
|
+
): CouncilResponse {
|
|
428
|
+
return this._ask_impl(system_prompt, stable_prompt, volatile_suffix, max_tokens);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private _ask_impl(
|
|
432
|
+
system_prompt: string,
|
|
433
|
+
stable_prompt: string,
|
|
434
|
+
volatile_suffix: string,
|
|
435
|
+
max_tokens: number,
|
|
436
|
+
): CouncilResponse {
|
|
437
|
+
const user_prompt = volatile_suffix
|
|
438
|
+
? `${stable_prompt}${volatile_suffix}`
|
|
439
|
+
: stable_prompt;
|
|
398
440
|
const t0 = _nowMs();
|
|
399
441
|
let response: unknown;
|
|
400
442
|
try {
|
|
@@ -426,12 +468,19 @@ export class AnthropicClient extends ExternalAIClient {
|
|
|
426
468
|
messages: [
|
|
427
469
|
{
|
|
428
470
|
role: 'user',
|
|
471
|
+
// Breakpoint on the STABLE block only — the
|
|
472
|
+
// volatile suffix (round critiques) rides after
|
|
473
|
+
// it, so round N+1 reads the cached prefix
|
|
474
|
+
// instead of re-writing it (A3 read unlock).
|
|
429
475
|
content: [
|
|
430
476
|
{
|
|
431
477
|
type: 'text',
|
|
432
|
-
text:
|
|
478
|
+
text: stable_prompt,
|
|
433
479
|
cache_control: { type: 'ephemeral' },
|
|
434
480
|
},
|
|
481
|
+
...(volatile_suffix
|
|
482
|
+
? [{ type: 'text', text: volatile_suffix }]
|
|
483
|
+
: []),
|
|
435
484
|
],
|
|
436
485
|
},
|
|
437
486
|
],
|
|
@@ -437,6 +437,13 @@ export interface NecessityClassifierConfig {
|
|
|
437
437
|
export interface ModelDowngradeConfig {
|
|
438
438
|
readonly enabled: boolean;
|
|
439
439
|
readonly auto_apply: boolean;
|
|
440
|
+
/**
|
|
441
|
+
* Per-run escape hatch (roadmap A3): member name → model id. A member
|
|
442
|
+
* listed here is pinned to that model for the run — the size classifier
|
|
443
|
+
* and the cache-coupling gate are both skipped for it. Set it in
|
|
444
|
+
* `.ai-council.yml` when a specific artefact needs a specific tier.
|
|
445
|
+
*/
|
|
446
|
+
readonly model_tier_override: Readonly<Record<string, string>>;
|
|
440
447
|
}
|
|
441
448
|
|
|
442
449
|
/** Pre-flight cost-disclosure toggle (Phase 8). */
|
|
@@ -908,11 +915,27 @@ function _build_model_downgrade(d: Dict): ModelDowngradeConfig {
|
|
|
908
915
|
if (!_isBool(enabled)) {
|
|
909
916
|
throw new CouncilConfigError('`model_downgrade.enabled` must be a bool.');
|
|
910
917
|
}
|
|
911
|
-
|
|
918
|
+
// A3 (2026-07-20): auto_apply default flipped to TRUE — auto-downgrade is
|
|
919
|
+
// the default and `auto_apply: false` (or `enabled: false`) is the opt-out.
|
|
920
|
+
// The cache-coupling gate in council_cli._size_fit_gate keeps it honest.
|
|
921
|
+
const auto_apply = _get(d, 'auto_apply', true);
|
|
912
922
|
if (!_isBool(auto_apply)) {
|
|
913
923
|
throw new CouncilConfigError('`model_downgrade.auto_apply` must be a bool.');
|
|
914
924
|
}
|
|
915
|
-
|
|
925
|
+
const override_raw = _get(d, 'model_tier_override', {});
|
|
926
|
+
if (!_isDict(override_raw)) {
|
|
927
|
+
throw new CouncilConfigError('`model_downgrade.model_tier_override` must be a mapping.');
|
|
928
|
+
}
|
|
929
|
+
const model_tier_override: Record<string, string> = {};
|
|
930
|
+
for (const [k, v] of Object.entries(override_raw as Dict)) {
|
|
931
|
+
if (!_isStr(v) || (v as string).length === 0) {
|
|
932
|
+
throw new CouncilConfigError(
|
|
933
|
+
`\`model_downgrade.model_tier_override.${k}\` must be a non-empty string model id.`,
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
model_tier_override[k] = v as string;
|
|
937
|
+
}
|
|
938
|
+
return { enabled: Boolean(enabled), auto_apply: Boolean(auto_apply), model_tier_override };
|
|
916
939
|
}
|
|
917
940
|
|
|
918
941
|
function _build_cost_disclosure(d: Dict, scope: string): CostDisclosureConfig {
|
|
@@ -1308,7 +1331,8 @@ function _build_lens_overrides(d: Dict): LensOverridesConfig {
|
|
|
1308
1331
|
`\`lenses.${lens_name}.model_downgrade.enabled\` must be a bool.`,
|
|
1309
1332
|
);
|
|
1310
1333
|
}
|
|
1311
|
-
|
|
1334
|
+
// Lens overrides inherit the A3 auto-by-default posture.
|
|
1335
|
+
const md_auto = _get(md_block, 'auto_apply', true);
|
|
1312
1336
|
if (!_isBool(md_auto)) {
|
|
1313
1337
|
throw new CouncilConfigError(
|
|
1314
1338
|
`\`lenses.${lens_name}.model_downgrade.auto_apply\` must be a bool.`,
|
|
@@ -1317,6 +1341,7 @@ function _build_lens_overrides(d: Dict): LensOverridesConfig {
|
|
|
1317
1341
|
md_overrides.set(lens_name, {
|
|
1318
1342
|
enabled: Boolean(md_enabled),
|
|
1319
1343
|
auto_apply: Boolean(md_auto),
|
|
1344
|
+
model_tier_override: {},
|
|
1320
1345
|
});
|
|
1321
1346
|
}
|
|
1322
1347
|
const cd_block = lens_cfg.cost_disclosure;
|
|
@@ -598,6 +598,10 @@ export function resolve_low_impact(
|
|
|
598
598
|
clients: ReadonlyMap<string, ExternalAIClient> | Record<string, ExternalAIClient>,
|
|
599
599
|
price_table: unknown = null,
|
|
600
600
|
now: (() => Date) | null = null,
|
|
601
|
+
model_downgrade: {
|
|
602
|
+
enabled: boolean;
|
|
603
|
+
model_tier_override?: Readonly<Record<string, string>>;
|
|
604
|
+
} | null = null,
|
|
601
605
|
): FastPathResolution {
|
|
602
606
|
if (!plan.is_resolvable) {
|
|
603
607
|
return new FastPathResolution({
|
|
@@ -618,6 +622,18 @@ export function resolve_low_impact(
|
|
|
618
622
|
|
|
619
623
|
for (const member of plan.members) {
|
|
620
624
|
const client = getClient(member.name);
|
|
625
|
+
// A3: the fast-path is one-shot (no same-model cache reads to lose),
|
|
626
|
+
// so a configured ladder downgrades to its CHEAPEST rung outright —
|
|
627
|
+
// low-impact questions are the definition of small/low-stakes. The
|
|
628
|
+
// per-run model_tier_override pin wins; enabled: false opts out.
|
|
629
|
+
if (client && model_downgrade?.enabled) {
|
|
630
|
+
const pinned = model_downgrade.model_tier_override?.[member.name];
|
|
631
|
+
const cheapest = member.model_ladder.length > 0 ? member.model_ladder[0] : null;
|
|
632
|
+
const target = pinned && pinned.length > 0 ? pinned : cheapest;
|
|
633
|
+
if (target && target !== client.model) {
|
|
634
|
+
client.model = target;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
621
637
|
if (client === undefined || client === null) {
|
|
622
638
|
answers.push(
|
|
623
639
|
new MemberAnswer({
|
|
@@ -402,6 +402,17 @@ function _length_tier(text: string): LengthTier {
|
|
|
402
402
|
* @param lens active lens; `debate` lens disables downgrade suggestions to
|
|
403
403
|
* keep dissent quality high.
|
|
404
404
|
* @returns A {@link SizeFitVerdict}.
|
|
405
|
+
*
|
|
406
|
+
* SIZE DEFINITION (A3 precondition, verified 2026-07-20): "small" is measured
|
|
407
|
+
* in Unicode code points (`_pyLen`, Python-`len()` parity) against the
|
|
408
|
+
* character thresholds `_SHORT_PROMPT_MAX`/`_MEDIUM_PROMPT_MAX` (200/800) plus
|
|
409
|
+
* a complexity-trigger heuristic — NOT bytes and NOT tokens. A token-based
|
|
410
|
+
* classifier was considered and rejected: it would add a tokenizer dependency
|
|
411
|
+
* (or a paid pre-flight count_tokens call) on the sequential-dispatch path
|
|
412
|
+
* (orchestrator.ts § "Members are called sequentially"), where every
|
|
413
|
+
* per-member pre-flight adds wall-clock latency before the first real call.
|
|
414
|
+
* The chars/4 heuristic in `pricing.estimate_input_tokens` remains the shared
|
|
415
|
+
* token approximation where cost math needs one.
|
|
405
416
|
*/
|
|
406
417
|
export function classify_size_fit(
|
|
407
418
|
prompt: string,
|