@event4u/agent-config 1.17.0 → 1.18.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/rules/context-hygiene.md +6 -0
- package/.agent-src/rules/direct-answers.md +17 -26
- package/.agent-src/rules/no-cheap-questions.md +14 -21
- package/.agent-src/rules/onboarding-gate.md +7 -0
- package/.agent-src/rules/roadmap-progress-sync.md +27 -0
- package/.agent-src/rules/rule-type-governance.md +28 -0
- package/.agent-src/templates/roadmaps.md +4 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +35 -0
- package/README.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/contracts/load-context-budget-model.md +80 -0
- package/docs/contracts/load-context-schema.md +20 -0
- package/docs/contracts/roadmap-complexity-standard.md +137 -0
- package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +134 -0
- package/docs/guidelines/agent-infra/direct-answers-demos.md +145 -0
- package/docs/guidelines/agent-infra/verify-before-complete-demos.md +128 -0
- package/package.json +1 -1
- package/scripts/agent-config +20 -0
- package/scripts/ai_council/one_off_archive/2026-05/README.md +45 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_budget_v2_audit.py +206 -0
- package/scripts/build_rule_trigger_matrix.py +360 -0
- package/scripts/check_always_budget.py +39 -0
- package/scripts/check_one_off_location.py +81 -0
- package/scripts/check_references.py +6 -0
- package/scripts/compress.py +5 -2
- package/scripts/context_hygiene_hook.py +173 -0
- package/scripts/hooks/augment-context-hygiene.sh +55 -0
- package/scripts/hooks/augment-onboarding-gate.sh +55 -0
- package/scripts/install.py +58 -19
- package/scripts/lint_examples.py +98 -0
- package/scripts/lint_roadmap_complexity.py +127 -0
- package/scripts/onboarding_gate_hook.py +137 -0
- package/scripts/schemas/rule.schema.json +5 -0
- /package/scripts/ai_council/{_one_off_2a4_acceptance.py → one_off_archive/2026-05/_one_off_2a4_acceptance.py} +0 -0
- /package/scripts/ai_council/{_one_off_context_layer_v1_estimate.py → one_off_archive/2026-05/_one_off_context_layer_v1_estimate.py} +0 -0
- /package/scripts/ai_council/{_one_off_context_layer_v1_review.py → one_off_archive/2026-05/_one_off_context_layer_v1_review.py} +0 -0
- /package/scripts/ai_council/{_one_off_followups_review.py → one_off_archive/2026-05/_one_off_followups_review.py} +0 -0
- /package/scripts/ai_council/{_one_off_nondestructive_inline_audit.py → one_off_archive/2026-05/_one_off_nondestructive_inline_audit.py} +0 -0
- /package/scripts/{_one_off_phase4_dispatch_latency.py → ai_council/one_off_archive/2026-05/_one_off_phase4_dispatch_latency.py} +0 -0
- /package/scripts/{_one_off_phase6_trigger_jaccard.py → ai_council/one_off_archive/2026-05/_one_off_phase6_trigger_jaccard.py} +0 -0
- /package/scripts/ai_council/{_one_off_phase_2a_budget_rebalance.py → one_off_archive/2026-05/_one_off_phase_2a_budget_rebalance.py} +0 -0
- /package/scripts/ai_council/{_one_off_phase_2a_post_revert.py → one_off_archive/2026-05/_one_off_phase_2a_post_revert.py} +0 -0
- /package/scripts/ai_council/{_one_off_rebalancing_audit.py → one_off_archive/2026-05/_one_off_rebalancing_audit.py} +0 -0
- /package/scripts/ai_council/{_one_off_roundtrip.py → one_off_archive/2026-05/_one_off_roundtrip.py} +0 -0
- /package/scripts/ai_council/{_one_off_rule_hardening_v1.py → one_off_archive/2026-05/_one_off_rule_hardening_v1.py} +0 -0
- /package/scripts/ai_council/{_one_off_structural_open_questions.py → one_off_archive/2026-05/_one_off_structural_open_questions.py} +0 -0
- /package/scripts/ai_council/{_one_off_structural_optimization.py → one_off_archive/2026-05/_one_off_structural_optimization.py} +0 -0
- /package/scripts/ai_council/{_one_off_structural_v3_gaps.py → one_off_archive/2026-05/_one_off_structural_v3_gaps.py} +0 -0
- /package/scripts/ai_council/{_one_off_structural_v3_review.py → one_off_archive/2026-05/_one_off_structural_v3_review.py} +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Phase 5.2 roadmap-complexity linter.
|
|
3
|
+
|
|
4
|
+
Enforces the measurable subset of
|
|
5
|
+
`docs/contracts/roadmap-complexity-standard.md`:
|
|
6
|
+
|
|
7
|
+
- every `agents/roadmaps/*.md` declares `complexity: lightweight`
|
|
8
|
+
or `complexity: structural` in frontmatter;
|
|
9
|
+
- lightweight roadmaps have ≤ 600 total lines and ≤ 6 `## Phase N`
|
|
10
|
+
headings, and contain no `## Council Round N` / `### Verdict`
|
|
11
|
+
sections;
|
|
12
|
+
- structural roadmaps have no upper cap, but the tag must be
|
|
13
|
+
declared.
|
|
14
|
+
|
|
15
|
+
Cap: ≤ 150 LOC, stdlib only. Hooked into `task ci` via
|
|
16
|
+
`task lint-roadmap-complexity`.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
ROADMAP_GLOB = "agents/roadmaps/*.md"
|
|
26
|
+
LIGHTWEIGHT_LINE_CAP = 600
|
|
27
|
+
LIGHTWEIGHT_PHASE_CAP = 6
|
|
28
|
+
|
|
29
|
+
PHASE_PAT = re.compile(r"^## Phase \d+\b", re.MULTILINE)
|
|
30
|
+
COUNCIL_PAT = re.compile(r"^## Council Round \d+\b", re.MULTILINE)
|
|
31
|
+
VERDICT_PAT = re.compile(r"^### Verdict\b", re.MULTILINE)
|
|
32
|
+
COMPLEXITY_PAT = re.compile(
|
|
33
|
+
r"^complexity:\s*(lightweight|structural)\s*$", re.MULTILINE
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _frontmatter(text: str) -> str:
|
|
38
|
+
if not text.startswith("---\n"):
|
|
39
|
+
return ""
|
|
40
|
+
end = text.find("\n---\n", 4)
|
|
41
|
+
return text[4:end] if end != -1 else ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _read_complexity(fm: str) -> str | None:
|
|
45
|
+
m = COMPLEXITY_PAT.search(fm)
|
|
46
|
+
return m.group(1) if m else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _check_lightweight(text: str, line_count: int, problems: list[str]) -> None:
|
|
50
|
+
if line_count > LIGHTWEIGHT_LINE_CAP:
|
|
51
|
+
problems.append(
|
|
52
|
+
f"lightweight cap exceeded: {line_count} lines "
|
|
53
|
+
f"(max {LIGHTWEIGHT_LINE_CAP}); consider tagging structural "
|
|
54
|
+
f"or trimming"
|
|
55
|
+
)
|
|
56
|
+
phases = len(PHASE_PAT.findall(text))
|
|
57
|
+
if phases > LIGHTWEIGHT_PHASE_CAP:
|
|
58
|
+
problems.append(
|
|
59
|
+
f"lightweight phase cap exceeded: {phases} phases "
|
|
60
|
+
f"(max {LIGHTWEIGHT_PHASE_CAP})"
|
|
61
|
+
)
|
|
62
|
+
if COUNCIL_PAT.search(text):
|
|
63
|
+
problems.append(
|
|
64
|
+
"lightweight roadmap contains '## Council Round N' "
|
|
65
|
+
"block — council debates belong in structural roadmaps"
|
|
66
|
+
)
|
|
67
|
+
if VERDICT_PAT.search(text):
|
|
68
|
+
problems.append(
|
|
69
|
+
"lightweight roadmap contains '### Verdict' block — "
|
|
70
|
+
"council verdicts belong in structural roadmaps"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def lint_roadmap(path: Path) -> list[str]:
|
|
75
|
+
text = path.read_text(encoding="utf-8")
|
|
76
|
+
line_count = text.count("\n") + (1 if text and not text.endswith("\n") else 0)
|
|
77
|
+
problems: list[str] = []
|
|
78
|
+
fm = _frontmatter(text)
|
|
79
|
+
complexity = _read_complexity(fm) if fm else None
|
|
80
|
+
if complexity is None:
|
|
81
|
+
problems.append(
|
|
82
|
+
"missing 'complexity:' frontmatter "
|
|
83
|
+
"(must declare 'lightweight' or 'structural')"
|
|
84
|
+
)
|
|
85
|
+
return problems
|
|
86
|
+
if complexity == "lightweight":
|
|
87
|
+
_check_lightweight(text, line_count, problems)
|
|
88
|
+
return problems
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main() -> int:
|
|
92
|
+
roadmaps = sorted(REPO_ROOT.glob(ROADMAP_GLOB))
|
|
93
|
+
if not roadmaps:
|
|
94
|
+
print(f"❌ no roadmaps matched {ROADMAP_GLOB}", file=sys.stderr)
|
|
95
|
+
return 1
|
|
96
|
+
failed = 0
|
|
97
|
+
summary: list[tuple[str, str]] = []
|
|
98
|
+
for roadmap in roadmaps:
|
|
99
|
+
rel = roadmap.relative_to(REPO_ROOT)
|
|
100
|
+
problems = lint_roadmap(roadmap)
|
|
101
|
+
text = roadmap.read_text(encoding="utf-8")
|
|
102
|
+
complexity = _read_complexity(_frontmatter(text)) or "untagged"
|
|
103
|
+
summary.append((str(rel), complexity))
|
|
104
|
+
if problems:
|
|
105
|
+
failed += 1
|
|
106
|
+
print(f"❌ {rel} [{complexity}]", file=sys.stderr)
|
|
107
|
+
for p in problems:
|
|
108
|
+
print(f" - {p}", file=sys.stderr)
|
|
109
|
+
else:
|
|
110
|
+
print(f"✅ {rel} [{complexity}]")
|
|
111
|
+
print()
|
|
112
|
+
light = sum(1 for _, c in summary if c == "lightweight")
|
|
113
|
+
structural = sum(1 for _, c in summary if c == "structural")
|
|
114
|
+
untagged = sum(1 for _, c in summary if c == "untagged")
|
|
115
|
+
print(
|
|
116
|
+
f"summary: {light} lightweight · {structural} structural · "
|
|
117
|
+
f"{untagged} untagged · {len(summary)} total"
|
|
118
|
+
)
|
|
119
|
+
if failed:
|
|
120
|
+
print(f"\n❌ {failed} roadmap(s) failed complexity lint", file=sys.stderr)
|
|
121
|
+
return 1
|
|
122
|
+
print(f"\n✅ {len(roadmaps)} roadmap(s) complexity-clean")
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
sys.exit(main())
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Platform-agnostic hook for the `onboarding-gate` rule.
|
|
3
|
+
|
|
4
|
+
Reads `.agent-settings.yml` from the consumer repo and writes a
|
|
5
|
+
deterministic state file the rule body can cite as the source of
|
|
6
|
+
truth for "do I need to prompt the user about /onboard?".
|
|
7
|
+
|
|
8
|
+
Output is written to `agents/state/onboarding-gate.json` with:
|
|
9
|
+
{
|
|
10
|
+
"required": <bool>, // true → rule fires on first turn
|
|
11
|
+
"reason": "<string>", // why this state was set
|
|
12
|
+
"checked_at": "<iso8601>", // last hook run
|
|
13
|
+
"settings_present": <bool> // .agent-settings.yml exists
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
Exit code is **always 0**. Hooks must never block the agent loop.
|
|
17
|
+
|
|
18
|
+
Output discipline:
|
|
19
|
+
- stdout: nothing (Augment surfaces stdout to the user)
|
|
20
|
+
- stderr: one short line in --verbose mode, otherwise silent
|
|
21
|
+
|
|
22
|
+
CLI:
|
|
23
|
+
python3 scripts/onboarding_gate_hook.py [--platform NAME] [--verbose]
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import datetime as _dt
|
|
29
|
+
import json
|
|
30
|
+
import re
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
SETTINGS_FILE = ".agent-settings.yml"
|
|
35
|
+
STATE_DIR = Path("agents") / "state"
|
|
36
|
+
STATE_FILE = STATE_DIR / "onboarding-gate.json"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _read_onboarded(settings_path: Path) -> tuple[bool, str]:
|
|
40
|
+
"""Return (required, reason) — minimal, dependency-free YAML parsing.
|
|
41
|
+
|
|
42
|
+
We only need a single key under the `onboarding:` block. Full YAML is
|
|
43
|
+
overkill (and would pull in a runtime dep). We scan line-by-line for
|
|
44
|
+
`onboarded: <bool>` inside the `onboarding:` section.
|
|
45
|
+
"""
|
|
46
|
+
if not settings_path.is_file():
|
|
47
|
+
return (False, "settings_file_missing") # legacy: do not block
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
text = settings_path.read_text(encoding="utf-8")
|
|
51
|
+
except OSError:
|
|
52
|
+
return (False, "settings_file_unreadable")
|
|
53
|
+
|
|
54
|
+
in_onboarding = False
|
|
55
|
+
onboarded_value: str | None = None
|
|
56
|
+
for raw in text.splitlines():
|
|
57
|
+
line = raw.rstrip()
|
|
58
|
+
if not line or line.lstrip().startswith("#"):
|
|
59
|
+
continue
|
|
60
|
+
if re.match(r"^onboarding\s*:\s*$", line):
|
|
61
|
+
in_onboarding = True
|
|
62
|
+
continue
|
|
63
|
+
if in_onboarding:
|
|
64
|
+
# Section ends when a top-level (non-indented) key starts.
|
|
65
|
+
if line and not line.startswith((" ", "\t")):
|
|
66
|
+
break
|
|
67
|
+
m = re.match(r"^\s+onboarded\s*:\s*(\S+)\s*(?:#.*)?$", line)
|
|
68
|
+
if m:
|
|
69
|
+
onboarded_value = m.group(1).strip().lower()
|
|
70
|
+
|
|
71
|
+
if onboarded_value is None:
|
|
72
|
+
return (False, "key_missing") # legacy / pre-rule project
|
|
73
|
+
if onboarded_value in ("true", "yes", "on"):
|
|
74
|
+
return (False, "already_onboarded")
|
|
75
|
+
if onboarded_value in ("false", "no", "off"):
|
|
76
|
+
return (True, "explicit_false")
|
|
77
|
+
return (False, f"unknown_value:{onboarded_value}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _write_state(consumer_root: Path, required: bool, reason: str,
|
|
81
|
+
settings_present: bool) -> None:
|
|
82
|
+
"""Write `agents/state/onboarding-gate.json` atomically."""
|
|
83
|
+
state_dir = consumer_root / STATE_DIR
|
|
84
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
payload = {
|
|
86
|
+
"required": required,
|
|
87
|
+
"reason": reason,
|
|
88
|
+
"checked_at": _dt.datetime.now(_dt.timezone.utc).isoformat(
|
|
89
|
+
timespec="seconds"),
|
|
90
|
+
"settings_present": settings_present,
|
|
91
|
+
}
|
|
92
|
+
target = consumer_root / STATE_FILE
|
|
93
|
+
tmp = target.with_suffix(".json.tmp")
|
|
94
|
+
tmp.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
95
|
+
tmp.replace(target)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def run(*, consumer_root: Path, verbose: bool = False) -> int:
|
|
99
|
+
settings_path = consumer_root / SETTINGS_FILE
|
|
100
|
+
settings_present = settings_path.is_file()
|
|
101
|
+
try:
|
|
102
|
+
required, reason = _read_onboarded(settings_path)
|
|
103
|
+
except Exception: # pragma: no cover — defensive
|
|
104
|
+
required, reason = (False, "hook_error")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
_write_state(consumer_root, required, reason, settings_present)
|
|
108
|
+
except OSError:
|
|
109
|
+
if verbose:
|
|
110
|
+
print("onboarding-gate-hook: state write failed",
|
|
111
|
+
file=sys.stderr)
|
|
112
|
+
return 0 # never block
|
|
113
|
+
|
|
114
|
+
if verbose:
|
|
115
|
+
print(f"onboarding-gate-hook: required={required} reason={reason}",
|
|
116
|
+
file=sys.stderr)
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main(argv: list[str] | None = None) -> int:
|
|
121
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
122
|
+
parser.add_argument("--platform", default="generic",
|
|
123
|
+
help="informational platform tag")
|
|
124
|
+
parser.add_argument("--verbose", action="store_true",
|
|
125
|
+
help="emit one stderr line per invocation")
|
|
126
|
+
args = parser.parse_args(argv)
|
|
127
|
+
# Drain stdin so callers piping JSON don't block on a SIGPIPE on
|
|
128
|
+
# platforms that strictly require stdin to be consumed.
|
|
129
|
+
try:
|
|
130
|
+
sys.stdin.read()
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
return run(consumer_root=Path.cwd(), verbose=args.verbose)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__": # pragma: no cover
|
|
137
|
+
sys.exit(main())
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
"type": "array",
|
|
34
34
|
"items": {"type": "string", "pattern": "\\.md$"},
|
|
35
35
|
"description": "Eager auto-loaded context references. Counts against the per-rule char budget; enforced by scripts/lint_load_context.py."
|
|
36
|
+
},
|
|
37
|
+
"tier": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"enum": ["1", "2a", "2b", "3", "safety-floor", "mechanical-already"],
|
|
40
|
+
"description": "Hardening tier per road-to-rule-hardening.md. Optional today, recommended for new rules. Tracked in agents/contexts/rule-trigger-matrix.md. Tier 3 rules also referenced in agents/contexts/tier-3-dispositions.md."
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/scripts/ai_council/{_one_off_roundtrip.py → one_off_archive/2026-05/_one_off_roundtrip.py}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|