@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.
Files changed (64) hide show
  1. package/.agent-src/commands/memory/learn-low-impact.md +143 -0
  2. package/.agent-src/rules/ask-when-uncertain.md +10 -6
  3. package/.agent-src/rules/copilot-routing.md +1 -1
  4. package/.agent-src/rules/devcontainer-routing.md +1 -1
  5. package/.agent-src/rules/external-reference-deep-dive.md +1 -1
  6. package/.agent-src/rules/fast-path-marker-visibility.md +38 -0
  7. package/.agent-src/rules/low-impact-corpus-privacy-floor.md +74 -0
  8. package/.agent-src/rules/symfony-routing.md +1 -1
  9. package/.agent-src/skills/ai-council/SKILL.md +208 -8
  10. package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  11. package/.claude-plugin/marketplace.json +2 -1
  12. package/CHANGELOG.md +299 -124
  13. package/README.md +6 -6
  14. package/config/gitignore-block.txt +6 -0
  15. package/docs/architecture.md +12 -12
  16. package/docs/archive/CHANGELOG-pre-2.11.0.md +141 -0
  17. package/docs/catalog.md +10 -7
  18. package/docs/contracts/adr-architectural-consensus-mechanism.md +4 -3
  19. package/docs/contracts/adr-level-6-productization.md +7 -9
  20. package/docs/contracts/ai-council-config.md +492 -20
  21. package/docs/contracts/command-clusters.md +1 -1
  22. package/docs/contracts/command-surface-tiers.md +3 -2
  23. package/docs/contracts/cost-profile-defaults.md +5 -0
  24. package/docs/contracts/decision-engine-gates.md +5 -0
  25. package/docs/contracts/decision-trace-v1.md +2 -2
  26. package/docs/contracts/file-ownership-matrix.json +1735 -72
  27. package/docs/contracts/installed-tools-lockfile.md +2 -1
  28. package/docs/contracts/low-impact-corpus-format.md +95 -0
  29. package/docs/contracts/mcp-beta-criteria.md +6 -5
  30. package/docs/contracts/mcp-cloud-scope.md +5 -4
  31. package/docs/contracts/multi-tool-projection-fidelity.md +8 -2
  32. package/docs/contracts/release-trunk-sync.md +4 -3
  33. package/docs/contracts/tier-3-contrib-plugin.md +5 -6
  34. package/docs/getting-started.md +2 -2
  35. package/docs/guidelines/agent-infra/installed-tools-manifest.md +2 -1
  36. package/docs/installation.md +32 -0
  37. package/package.json +1 -1
  38. package/scripts/_cli/cmd_doctor.py +134 -0
  39. package/scripts/ai_council/airgap.py +165 -0
  40. package/scripts/ai_council/cli_hints.py +123 -0
  41. package/scripts/ai_council/clients.py +787 -5
  42. package/scripts/ai_council/compile_corpus.py +178 -0
  43. package/scripts/ai_council/confidence_gate.py +156 -0
  44. package/scripts/ai_council/config.py +1007 -11
  45. package/scripts/ai_council/consensus.py +41 -2
  46. package/scripts/ai_council/events_log.py +137 -0
  47. package/scripts/ai_council/learn_low_impact_preview.py +252 -0
  48. package/scripts/ai_council/low_impact.py +714 -0
  49. package/scripts/ai_council/low_impact_corpus.py +466 -0
  50. package/scripts/ai_council/low_impact_intake.py +163 -0
  51. package/scripts/ai_council/modes.py +6 -1
  52. package/scripts/ai_council/necessity.py +782 -0
  53. package/scripts/ai_council/orchestrator.py +252 -14
  54. package/scripts/ai_council/probation_gate.py +152 -0
  55. package/scripts/ai_council/redact_low_impact_entry.py +155 -0
  56. package/scripts/ai_council/replay.py +155 -0
  57. package/scripts/ai_council/session.py +19 -1
  58. package/scripts/ai_council/shadow_dispatch.py +235 -0
  59. package/scripts/ai_council/solo_dispatch.py +226 -0
  60. package/scripts/audit_cloud_compatibility.py +74 -0
  61. package/scripts/audit_command_surface.py +363 -0
  62. package/scripts/check_council_layout.py +11 -0
  63. package/scripts/council_cli.py +1046 -15
  64. 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
+ ]