@event4u/agent-config 2.13.0 → 2.14.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/.agent-src/commands/memory/learn-low-impact.md +143 -0
- package/.agent-src/rules/ask-when-uncertain.md +10 -6
- package/.agent-src/rules/copilot-routing.md +1 -1
- package/.agent-src/rules/devcontainer-routing.md +1 -1
- package/.agent-src/rules/external-reference-deep-dive.md +1 -1
- package/.agent-src/rules/fast-path-marker-visibility.md +38 -0
- package/.agent-src/rules/low-impact-corpus-privacy-floor.md +74 -0
- package/.agent-src/rules/symfony-routing.md +1 -1
- package/.agent-src/skills/ai-council/SKILL.md +208 -8
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.claude-plugin/marketplace.json +2 -1
- package/CHANGELOG.md +299 -124
- package/README.md +6 -6
- package/config/gitignore-block.txt +6 -0
- package/docs/architecture.md +12 -12
- package/docs/archive/CHANGELOG-pre-2.11.0.md +141 -0
- package/docs/catalog.md +10 -7
- package/docs/contracts/adr-architectural-consensus-mechanism.md +4 -3
- package/docs/contracts/adr-level-6-productization.md +7 -9
- package/docs/contracts/ai-council-config.md +492 -20
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/command-surface-tiers.md +3 -2
- package/docs/contracts/cost-profile-defaults.md +5 -0
- package/docs/contracts/decision-engine-gates.md +5 -0
- package/docs/contracts/decision-trace-v1.md +2 -2
- package/docs/contracts/file-ownership-matrix.json +1735 -72
- package/docs/contracts/installed-tools-lockfile.md +2 -1
- package/docs/contracts/low-impact-corpus-format.md +95 -0
- package/docs/contracts/mcp-beta-criteria.md +6 -5
- package/docs/contracts/mcp-cloud-scope.md +5 -4
- package/docs/contracts/multi-tool-projection-fidelity.md +8 -2
- package/docs/contracts/release-trunk-sync.md +4 -3
- package/docs/contracts/tier-3-contrib-plugin.md +5 -6
- package/docs/getting-started.md +2 -2
- package/docs/guidelines/agent-infra/installed-tools-manifest.md +2 -1
- package/docs/installation.md +32 -0
- package/package.json +1 -1
- package/scripts/_cli/cmd_doctor.py +134 -0
- package/scripts/ai_council/airgap.py +165 -0
- package/scripts/ai_council/cli_hints.py +123 -0
- package/scripts/ai_council/clients.py +787 -5
- package/scripts/ai_council/compile_corpus.py +178 -0
- package/scripts/ai_council/confidence_gate.py +156 -0
- package/scripts/ai_council/config.py +1007 -11
- package/scripts/ai_council/consensus.py +41 -2
- package/scripts/ai_council/events_log.py +137 -0
- package/scripts/ai_council/learn_low_impact_preview.py +252 -0
- package/scripts/ai_council/low_impact.py +714 -0
- package/scripts/ai_council/low_impact_corpus.py +466 -0
- package/scripts/ai_council/low_impact_intake.py +163 -0
- package/scripts/ai_council/modes.py +6 -1
- package/scripts/ai_council/necessity.py +782 -0
- package/scripts/ai_council/orchestrator.py +252 -14
- package/scripts/ai_council/probation_gate.py +152 -0
- package/scripts/ai_council/redact_low_impact_entry.py +155 -0
- package/scripts/ai_council/replay.py +155 -0
- package/scripts/ai_council/session.py +19 -1
- package/scripts/ai_council/shadow_dispatch.py +235 -0
- package/scripts/ai_council/solo_dispatch.py +226 -0
- package/scripts/audit_cloud_compatibility.py +74 -0
- package/scripts/audit_command_surface.py +363 -0
- package/scripts/check_council_layout.py +11 -0
- package/scripts/council_cli.py +1046 -15
- package/scripts/install.sh +12 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Compile the human-edited low-impact corpus Markdown to a YAML lockfile.
|
|
2
|
+
|
|
3
|
+
Step-10 — see ``agents/roadmaps/step-10-corpus-yaml-lockfile.md``.
|
|
4
|
+
|
|
5
|
+
Markdown (`agents/low-impact-decisions.md`) stays the human-authored
|
|
6
|
+
source-of-truth for PR review. This script reads it through the
|
|
7
|
+
hardened :func:`scripts.ai_council.low_impact_corpus.parse_corpus_strict`
|
|
8
|
+
parser and writes a YAML lockfile that becomes the **runtime**
|
|
9
|
+
source-of-truth. The pattern mirrors `.agent-src/` vs
|
|
10
|
+
`.agent-src.uncompressed/`: human edits Markdown, `task consistency`
|
|
11
|
+
enforces lockfile parity via the same ``git diff --quiet`` gate.
|
|
12
|
+
|
|
13
|
+
YAML schema (`schema_version: 1`)::
|
|
14
|
+
|
|
15
|
+
schema_version: 1
|
|
16
|
+
provenance:
|
|
17
|
+
source_path: agents/low-impact-decisions.md
|
|
18
|
+
source_sha256: <hex> # SHA-256 of the parsed Markdown bytes
|
|
19
|
+
last_upstreamed: <40-hex sha> # mirrored from the Markdown footer
|
|
20
|
+
validated:
|
|
21
|
+
- phrase: "raw bullet text"
|
|
22
|
+
normalised: "raw bullet text"
|
|
23
|
+
line_no: 42
|
|
24
|
+
trailing_metadata: "validated 2025-01-15"
|
|
25
|
+
probation: [...]
|
|
26
|
+
anti_examples: [...]
|
|
27
|
+
|
|
28
|
+
Determinism: sorted keys disabled (preserve schema order), entries
|
|
29
|
+
ordered by ``line_no``, single trailing newline. PyYAML ``safe_dump``
|
|
30
|
+
with ``allow_unicode=True`` so phrases with non-ASCII characters
|
|
31
|
+
round-trip unchanged.
|
|
32
|
+
|
|
33
|
+
Failure-mode contract:
|
|
34
|
+
|
|
35
|
+
- Parser raises ``CorpusParseError`` -> compiler exits non-zero, does
|
|
36
|
+
NOT write a partial lockfile.
|
|
37
|
+
- ``--check`` mode compares the freshly compiled output against the
|
|
38
|
+
committed lockfile and exits non-zero on drift (CI gate).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import argparse
|
|
44
|
+
import hashlib
|
|
45
|
+
import re
|
|
46
|
+
import sys
|
|
47
|
+
from dataclasses import asdict
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from typing import Any
|
|
50
|
+
|
|
51
|
+
import yaml
|
|
52
|
+
|
|
53
|
+
from scripts.ai_council.low_impact_corpus import (
|
|
54
|
+
CorpusEntry,
|
|
55
|
+
CorpusParseError,
|
|
56
|
+
CorpusParseResult,
|
|
57
|
+
parse_corpus_strict,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
SCHEMA_VERSION = 1
|
|
61
|
+
|
|
62
|
+
_LAST_UPSTREAMED_RE = re.compile(r"^last-upstreamed:\s*([0-9a-f]{40})\s*$", re.MULTILINE)
|
|
63
|
+
|
|
64
|
+
_DEFAULT_SOURCE = Path("agents/low-impact-decisions.md")
|
|
65
|
+
_DEFAULT_OUT = Path("agents/low-impact-decisions.lock.yaml")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _entry_to_dict(entry: CorpusEntry) -> dict[str, Any]:
|
|
69
|
+
"""Serialise a :class:`CorpusEntry` to schema-stable mapping."""
|
|
70
|
+
data = asdict(entry)
|
|
71
|
+
# ``section`` is implicit by parent key; drop to keep the YAML lean.
|
|
72
|
+
data.pop("section", None)
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _extract_last_upstreamed(text: str) -> str:
|
|
77
|
+
"""Read the provenance SHA from the Markdown footer; ``"" `` if absent."""
|
|
78
|
+
m = _LAST_UPSTREAMED_RE.search(text)
|
|
79
|
+
return m.group(1) if m else ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _normalise_source_path(path: Path) -> str:
|
|
83
|
+
"""Return ``path`` as a POSIX string relative to cwd when possible.
|
|
84
|
+
|
|
85
|
+
Absolute paths inside the current working directory are stripped to
|
|
86
|
+
their relative form so the committed lockfile carries
|
|
87
|
+
``agents/low-impact-decisions.md`` regardless of how the compiler
|
|
88
|
+
was invoked (CLI default, absolute path from a test, etc.).
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
rel = path.resolve().relative_to(Path.cwd().resolve())
|
|
92
|
+
except (ValueError, OSError):
|
|
93
|
+
rel = Path(path.name)
|
|
94
|
+
return str(rel).replace("\\", "/")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_lock_document(
|
|
98
|
+
source_path: Path,
|
|
99
|
+
parse_result: CorpusParseResult,
|
|
100
|
+
source_text: str,
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
"""Return the schema-v1 mapping for ``parse_result``."""
|
|
103
|
+
sha256 = hashlib.sha256(source_text.encode("utf-8")).hexdigest()
|
|
104
|
+
return {
|
|
105
|
+
"schema_version": SCHEMA_VERSION,
|
|
106
|
+
"provenance": {
|
|
107
|
+
"source_path": _normalise_source_path(source_path),
|
|
108
|
+
"source_sha256": sha256,
|
|
109
|
+
"last_upstreamed": _extract_last_upstreamed(source_text),
|
|
110
|
+
},
|
|
111
|
+
"validated": [_entry_to_dict(e) for e in parse_result.validated],
|
|
112
|
+
"probation": [_entry_to_dict(e) for e in parse_result.probation],
|
|
113
|
+
"anti_examples": [_entry_to_dict(e) for e in parse_result.anti_examples],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def dump_lock_yaml(document: dict[str, Any]) -> str:
|
|
118
|
+
"""Serialise ``document`` deterministically to YAML text."""
|
|
119
|
+
return yaml.safe_dump(
|
|
120
|
+
document,
|
|
121
|
+
sort_keys=False,
|
|
122
|
+
allow_unicode=True,
|
|
123
|
+
default_flow_style=False,
|
|
124
|
+
width=10_000,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def compile_corpus(source_path: Path, out_path: Path) -> str:
|
|
129
|
+
"""Read ``source_path`` Markdown, write YAML lockfile to ``out_path``.
|
|
130
|
+
|
|
131
|
+
Returns the YAML text that was written. Raises
|
|
132
|
+
:class:`CorpusParseError` if the Markdown source has structural
|
|
133
|
+
drift — caller is responsible for surfacing the error; no partial
|
|
134
|
+
lockfile is written.
|
|
135
|
+
"""
|
|
136
|
+
source_text = source_path.read_text(encoding="utf-8") if source_path.exists() else ""
|
|
137
|
+
parse_result = parse_corpus_strict(source_path)
|
|
138
|
+
document = build_lock_document(source_path, parse_result, source_text)
|
|
139
|
+
yaml_text = dump_lock_yaml(document)
|
|
140
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
out_path.write_text(yaml_text, encoding="utf-8")
|
|
142
|
+
return yaml_text
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _main(argv: list[str]) -> int:
|
|
146
|
+
parser = argparse.ArgumentParser(
|
|
147
|
+
prog="compile_corpus",
|
|
148
|
+
description="Compile low-impact-decisions.md to YAML lockfile.",
|
|
149
|
+
)
|
|
150
|
+
parser.add_argument("--source", type=Path, default=_DEFAULT_SOURCE)
|
|
151
|
+
parser.add_argument("--out", type=Path, default=_DEFAULT_OUT)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--check", action="store_true",
|
|
154
|
+
help="Exit non-zero if the lockfile is stale (CI gate).",
|
|
155
|
+
)
|
|
156
|
+
args = parser.parse_args(argv)
|
|
157
|
+
try:
|
|
158
|
+
fresh = compile_corpus(args.source, args.out) if not args.check else None
|
|
159
|
+
if args.check:
|
|
160
|
+
source_text = args.source.read_text(encoding="utf-8") if args.source.exists() else ""
|
|
161
|
+
parse_result = parse_corpus_strict(args.source)
|
|
162
|
+
document = build_lock_document(args.source, parse_result, source_text)
|
|
163
|
+
fresh = dump_lock_yaml(document)
|
|
164
|
+
existing = args.out.read_text(encoding="utf-8") if args.out.exists() else ""
|
|
165
|
+
if fresh != existing:
|
|
166
|
+
sys.stderr.write(
|
|
167
|
+
f"low-impact corpus lockfile is stale: {args.out}\n"
|
|
168
|
+
f" run: python3 -m scripts.ai_council.compile_corpus\n",
|
|
169
|
+
)
|
|
170
|
+
return 1
|
|
171
|
+
except CorpusParseError as exc:
|
|
172
|
+
sys.stderr.write(f"corpus parse failed: {exc}\n")
|
|
173
|
+
return 2
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
raise SystemExit(_main(sys.argv[1:]))
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Confidence gate for solo-member dispatch (step-9 P13).
|
|
2
|
+
|
|
3
|
+
Defense-in-depth on top of shadow-mode SLO: when a single member's
|
|
4
|
+
response signals uncertainty, presents unresolved alternatives, or
|
|
5
|
+
refuses, the dispatcher escalates to the full council on the current
|
|
6
|
+
invocation — independent of shadow sampling. The shadow log records
|
|
7
|
+
the escalation so the SLO can distinguish "silent disagreement" from
|
|
8
|
+
"auto-escalation".
|
|
9
|
+
|
|
10
|
+
Heuristics are intentionally stdlib-only (regex + length). No second
|
|
11
|
+
LLM judge pass; no external dependency. False positives escalate
|
|
12
|
+
(cheap, safe); false negatives are caught downstream by shadow-mode
|
|
13
|
+
disagreement.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
#: Below this character count a response is treated as too thin to
|
|
22
|
+
#: trust on its own — escalates as `short_response`.
|
|
23
|
+
_SHORT_RESPONSE_CHARS = 40
|
|
24
|
+
|
|
25
|
+
#: Hedge-word density (matches per 100 chars) above which the
|
|
26
|
+
#: response is treated as low-confidence. Calibrated against the
|
|
27
|
+
#: shadow-log fixtures; can be tuned without breaking the API.
|
|
28
|
+
_HEDGE_DENSITY_THRESHOLD = 0.04
|
|
29
|
+
|
|
30
|
+
_HEDGE_WORDS = (
|
|
31
|
+
"maybe", "perhaps", "possibly", "probably", "not sure",
|
|
32
|
+
"unsure", "i think", "i guess", "i'd say", "tend to",
|
|
33
|
+
"vielleicht", "eventuell", "möglicherweise", "wahrscheinlich",
|
|
34
|
+
"nicht sicher", "denke ich", "vermutlich",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_REFUSAL_PATTERNS = (
|
|
38
|
+
r"\bi (?:can(?:'?| no)t|cannot|won'?t|am unable)\b",
|
|
39
|
+
r"\bi don'?t know\b",
|
|
40
|
+
r"\bunclear\b",
|
|
41
|
+
r"\binsufficient (?:context|information|data)\b",
|
|
42
|
+
r"\bneed more (?:context|information|details)\b",
|
|
43
|
+
r"\bkann (?:ich )?nicht (?:entscheiden|sagen|beantworten)\b",
|
|
44
|
+
r"\bweiß ich nicht\b",
|
|
45
|
+
r"\bzu wenig (?:kontext|information)\b",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
_SPLIT_PATTERNS = (
|
|
49
|
+
r"\boption a\b.*?\boption b\b",
|
|
50
|
+
r"\bvariante 1\b.*?\bvariante 2\b",
|
|
51
|
+
r"\beither\b.*?\bor\b.*?\b(?:would|could|might)\b",
|
|
52
|
+
r"\bentweder\b.*?\boder\b.*?\b(?:wäre|könnte|würde)\b",
|
|
53
|
+
r"^\s*verdict:.*?^\s*verdict:", # two Verdict: blocks
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
_CONFIDENCE_MARKER_RE = re.compile(
|
|
57
|
+
r"confidence\s*[:=]\s*([01](?:\.\d+)?|\d{1,3}\s*%)",
|
|
58
|
+
re.IGNORECASE,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class EscalationDecision:
|
|
64
|
+
"""Verdict from :func:`should_escalate`."""
|
|
65
|
+
|
|
66
|
+
escalate: bool
|
|
67
|
+
reason: str # 'low_confidence' | 'split' | 'refusal' | 'short_response' | 'ok'
|
|
68
|
+
confidence: float | None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def extract_confidence(response: str) -> float | None:
|
|
72
|
+
"""Best-effort confidence score from a member response.
|
|
73
|
+
|
|
74
|
+
Returns the explicit ``Confidence: 0.X`` marker when present
|
|
75
|
+
(percent values normalised to 0–1). Otherwise derives a score
|
|
76
|
+
from hedge-word density: ``1.0 - clamp(density / threshold, 0, 1)``.
|
|
77
|
+
Returns ``None`` for empty input — caller treats as escalate.
|
|
78
|
+
"""
|
|
79
|
+
if not response or not response.strip():
|
|
80
|
+
return None
|
|
81
|
+
m = _CONFIDENCE_MARKER_RE.search(response)
|
|
82
|
+
if m:
|
|
83
|
+
raw = m.group(1).strip()
|
|
84
|
+
if raw.endswith("%"):
|
|
85
|
+
try:
|
|
86
|
+
return max(0.0, min(1.0, float(raw[:-1].strip()) / 100.0))
|
|
87
|
+
except ValueError:
|
|
88
|
+
pass
|
|
89
|
+
else:
|
|
90
|
+
try:
|
|
91
|
+
return max(0.0, min(1.0, float(raw)))
|
|
92
|
+
except ValueError:
|
|
93
|
+
pass
|
|
94
|
+
low = response.lower()
|
|
95
|
+
hits = sum(low.count(w) for w in _HEDGE_WORDS)
|
|
96
|
+
if hits == 0:
|
|
97
|
+
return 1.0
|
|
98
|
+
density = hits / max(1, len(response) / 100.0)
|
|
99
|
+
ratio = min(1.0, density / _HEDGE_DENSITY_THRESHOLD)
|
|
100
|
+
return max(0.0, 1.0 - ratio)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def is_split_response(response: str) -> bool:
|
|
104
|
+
"""True when the response presents unresolved alternatives.
|
|
105
|
+
|
|
106
|
+
Picks up `option A … option B`, two `Verdict:` blocks, `either … or
|
|
107
|
+
would/could`, and German equivalents. Conservative — escalating on
|
|
108
|
+
a split is cheap, missing one is caught by shadow disagreement.
|
|
109
|
+
"""
|
|
110
|
+
if not response:
|
|
111
|
+
return False
|
|
112
|
+
low = response.lower()
|
|
113
|
+
for pattern in _SPLIT_PATTERNS:
|
|
114
|
+
if re.search(pattern, low, re.DOTALL | re.MULTILINE):
|
|
115
|
+
return True
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def is_refusal(response: str) -> bool:
|
|
120
|
+
"""True when the response signals 'I can't / don't know / unclear'."""
|
|
121
|
+
if not response:
|
|
122
|
+
return True
|
|
123
|
+
low = response.lower()
|
|
124
|
+
return any(re.search(p, low) for p in _REFUSAL_PATTERNS)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def should_escalate(
|
|
128
|
+
response: str,
|
|
129
|
+
*,
|
|
130
|
+
floor: float,
|
|
131
|
+
) -> EscalationDecision:
|
|
132
|
+
"""Compose the gate. Order: refusal → split → short → low-conf → ok.
|
|
133
|
+
|
|
134
|
+
``floor`` is :class:`LowImpactConfig.solo_confidence_floor`.
|
|
135
|
+
"""
|
|
136
|
+
if response is None or not response.strip():
|
|
137
|
+
return EscalationDecision(True, "refusal", None)
|
|
138
|
+
if is_refusal(response):
|
|
139
|
+
return EscalationDecision(True, "refusal", None)
|
|
140
|
+
if is_split_response(response):
|
|
141
|
+
return EscalationDecision(True, "split", None)
|
|
142
|
+
if len(response.strip()) < _SHORT_RESPONSE_CHARS:
|
|
143
|
+
return EscalationDecision(True, "short_response", None)
|
|
144
|
+
conf = extract_confidence(response)
|
|
145
|
+
if conf is None or conf < floor:
|
|
146
|
+
return EscalationDecision(True, "low_confidence", conf)
|
|
147
|
+
return EscalationDecision(False, "ok", conf)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
__all__ = [
|
|
151
|
+
"EscalationDecision",
|
|
152
|
+
"extract_confidence",
|
|
153
|
+
"is_split_response",
|
|
154
|
+
"is_refusal",
|
|
155
|
+
"should_escalate",
|
|
156
|
+
]
|