@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.
Files changed (50) hide show
  1. package/.agent-src/rules/context-hygiene.md +6 -0
  2. package/.agent-src/rules/direct-answers.md +17 -26
  3. package/.agent-src/rules/no-cheap-questions.md +14 -21
  4. package/.agent-src/rules/onboarding-gate.md +7 -0
  5. package/.agent-src/rules/roadmap-progress-sync.md +27 -0
  6. package/.agent-src/rules/rule-type-governance.md +28 -0
  7. package/.agent-src/templates/roadmaps.md +4 -0
  8. package/.claude-plugin/marketplace.json +1 -1
  9. package/CHANGELOG.md +35 -0
  10. package/README.md +1 -1
  11. package/docs/architecture.md +1 -1
  12. package/docs/contracts/load-context-budget-model.md +80 -0
  13. package/docs/contracts/load-context-schema.md +20 -0
  14. package/docs/contracts/roadmap-complexity-standard.md +137 -0
  15. package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +134 -0
  16. package/docs/guidelines/agent-infra/direct-answers-demos.md +145 -0
  17. package/docs/guidelines/agent-infra/verify-before-complete-demos.md +128 -0
  18. package/package.json +1 -1
  19. package/scripts/agent-config +20 -0
  20. package/scripts/ai_council/one_off_archive/2026-05/README.md +45 -0
  21. package/scripts/ai_council/one_off_archive/2026-05/_one_off_budget_v2_audit.py +206 -0
  22. package/scripts/build_rule_trigger_matrix.py +360 -0
  23. package/scripts/check_always_budget.py +39 -0
  24. package/scripts/check_one_off_location.py +81 -0
  25. package/scripts/check_references.py +6 -0
  26. package/scripts/compress.py +5 -2
  27. package/scripts/context_hygiene_hook.py +173 -0
  28. package/scripts/hooks/augment-context-hygiene.sh +55 -0
  29. package/scripts/hooks/augment-onboarding-gate.sh +55 -0
  30. package/scripts/install.py +58 -19
  31. package/scripts/lint_examples.py +98 -0
  32. package/scripts/lint_roadmap_complexity.py +127 -0
  33. package/scripts/onboarding_gate_hook.py +137 -0
  34. package/scripts/schemas/rule.schema.json +5 -0
  35. /package/scripts/ai_council/{_one_off_2a4_acceptance.py → one_off_archive/2026-05/_one_off_2a4_acceptance.py} +0 -0
  36. /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
  37. /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
  38. /package/scripts/ai_council/{_one_off_followups_review.py → one_off_archive/2026-05/_one_off_followups_review.py} +0 -0
  39. /package/scripts/ai_council/{_one_off_nondestructive_inline_audit.py → one_off_archive/2026-05/_one_off_nondestructive_inline_audit.py} +0 -0
  40. /package/scripts/{_one_off_phase4_dispatch_latency.py → ai_council/one_off_archive/2026-05/_one_off_phase4_dispatch_latency.py} +0 -0
  41. /package/scripts/{_one_off_phase6_trigger_jaccard.py → ai_council/one_off_archive/2026-05/_one_off_phase6_trigger_jaccard.py} +0 -0
  42. /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
  43. /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
  44. /package/scripts/ai_council/{_one_off_rebalancing_audit.py → one_off_archive/2026-05/_one_off_rebalancing_audit.py} +0 -0
  45. /package/scripts/ai_council/{_one_off_roundtrip.py → one_off_archive/2026-05/_one_off_roundtrip.py} +0 -0
  46. /package/scripts/ai_council/{_one_off_rule_hardening_v1.py → one_off_archive/2026-05/_one_off_rule_hardening_v1.py} +0 -0
  47. /package/scripts/ai_council/{_one_off_structural_open_questions.py → one_off_archive/2026-05/_one_off_structural_open_questions.py} +0 -0
  48. /package/scripts/ai_council/{_one_off_structural_optimization.py → one_off_archive/2026-05/_one_off_structural_optimization.py} +0 -0
  49. /package/scripts/ai_council/{_one_off_structural_v3_gaps.py → one_off_archive/2026-05/_one_off_structural_v3_gaps.py} +0 -0
  50. /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
  }