@event4u/agent-config 2.10.0 → 2.12.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 (94) hide show
  1. package/.agent-src/commands/agents.md +1 -0
  2. package/.agent-src/commands/challenge-me.md +1 -0
  3. package/.agent-src/commands/chat-history.md +1 -0
  4. package/.agent-src/commands/context.md +1 -0
  5. package/.agent-src/commands/council.md +1 -0
  6. package/.agent-src/commands/feature.md +1 -0
  7. package/.agent-src/commands/fix.md +1 -0
  8. package/.agent-src/commands/grill-me.md +1 -0
  9. package/.agent-src/commands/judge.md +1 -0
  10. package/.agent-src/commands/memory.md +1 -0
  11. package/.agent-src/commands/module.md +1 -0
  12. package/.agent-src/commands/onboard.md +32 -4
  13. package/.agent-src/commands/optimize.md +1 -0
  14. package/.agent-src/commands/override.md +1 -0
  15. package/.agent-src/commands/roadmap.md +1 -0
  16. package/.agent-src/commands/tests.md +1 -0
  17. package/.agent-src/skills/canvas-design/SKILL.md +132 -0
  18. package/.agent-src/skills/canvas-design/evals/triggers.json +16 -0
  19. package/.agent-src/skills/doc-coauthoring/SKILL.md +129 -0
  20. package/.agent-src/skills/doc-coauthoring/evals/triggers.json +16 -0
  21. package/.agent-src/skills/nextjs-patterns/SKILL.md +203 -0
  22. package/.agent-src/skills/skill-writing/SKILL.md +101 -16
  23. package/.agent-src/skills/sql-writing/SKILL.md +1 -1
  24. package/.agent-src/skills/symfony-workflow/SKILL.md +173 -0
  25. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +4 -0
  26. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +3 -0
  27. package/.agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.py +162 -0
  28. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +24 -6
  29. package/.agent-src/templates/scripts/work_engine/scoring/decision_engine.py +351 -0
  30. package/.claude-plugin/marketplace.json +5 -1
  31. package/CHANGELOG.md +68 -0
  32. package/README.md +37 -8
  33. package/config/agent-settings.template.yml +66 -0
  34. package/docs/architecture.md +1 -1
  35. package/docs/contracts/STABILITY.md +16 -0
  36. package/docs/contracts/adr-chat-history-split.md +1 -0
  37. package/docs/contracts/adr-forecast-construction-shape.md +1 -0
  38. package/docs/contracts/adr-gtm-context-spine.md +1 -0
  39. package/docs/contracts/adr-level-6-productization.md +147 -0
  40. package/docs/contracts/adr-settings-sync-engine.md +1 -0
  41. package/docs/contracts/adr-wing4-context-spine.md +1 -0
  42. package/docs/contracts/agent-memory-contract.md +1 -0
  43. package/docs/contracts/agents-md-tech-stack.md +1 -0
  44. package/docs/contracts/audit-log-v1.md +1 -0
  45. package/docs/contracts/command-clusters.md +1 -0
  46. package/docs/contracts/command-surface-tiers.md +1 -0
  47. package/docs/contracts/context-paths.md +1 -0
  48. package/docs/contracts/cost-profile-defaults.md +105 -0
  49. package/docs/contracts/cross-wing-handoff.md +1 -0
  50. package/docs/contracts/decision-engine-gates.md +115 -0
  51. package/docs/contracts/decision-trace-v1.md +1 -0
  52. package/docs/contracts/file-ownership-matrix.md +1 -0
  53. package/docs/contracts/hook-architecture-v1.md +1 -0
  54. package/docs/contracts/implement-ticket-flow.md +1 -0
  55. package/docs/contracts/installed-tools-lockfile.md +1 -0
  56. package/docs/contracts/kernel-membership.md +1 -0
  57. package/docs/contracts/linear-ai-rules-inclusion.md +1 -0
  58. package/docs/contracts/linear-ai-three-layers.md +1 -0
  59. package/docs/contracts/linter-structural-model.md +1 -0
  60. package/docs/contracts/load-context-budget-model.md +1 -0
  61. package/docs/contracts/load-context-schema.md +1 -0
  62. package/docs/contracts/memory-visibility-v1.md +1 -0
  63. package/docs/contracts/one-off-script-lifecycle.md +1 -0
  64. package/docs/contracts/orchestration-dsl-v1.md +1 -0
  65. package/docs/contracts/package-self-orientation.md +1 -0
  66. package/docs/contracts/persona-schema.md +1 -0
  67. package/docs/contracts/release-trunk-sync.md +104 -0
  68. package/docs/contracts/roadmap-complexity-standard.md +1 -0
  69. package/docs/contracts/rule-classification.md +1 -0
  70. package/docs/contracts/rule-interactions.md +26 -0
  71. package/docs/contracts/rule-priority-hierarchy.md +1 -0
  72. package/docs/contracts/rule-router.md +1 -0
  73. package/docs/contracts/settings-sync-yaml-subset.md +1 -0
  74. package/docs/contracts/skill-domains.md +1 -0
  75. package/docs/contracts/tier-3-contrib-plugin.md +1 -0
  76. package/docs/contracts/ui-stack-extension.md +1 -0
  77. package/docs/contracts/ui-track-flow.md +1 -0
  78. package/docs/customization.md +1 -1
  79. package/docs/getting-started.md +3 -1
  80. package/docs/installation.md +8 -6
  81. package/package.json +1 -1
  82. package/scripts/ai_council/clients.py +17 -4
  83. package/scripts/ai_council/orchestrator.py +6 -2
  84. package/scripts/check_beta_review_markers.py +127 -0
  85. package/scripts/check_references.py +25 -0
  86. package/scripts/check_release_trunk_sync.py +152 -0
  87. package/scripts/council_cli.py +36 -5
  88. package/scripts/install.py +3 -3
  89. package/scripts/run_skill_evals.py +185 -0
  90. package/scripts/schemas/command.schema.json +5 -0
  91. package/scripts/schemas/skill.schema.json +4 -0
  92. package/scripts/skill_linter.py +82 -3
  93. package/scripts/smoke_quickstart.py +134 -0
  94. package/scripts/validate_decision_engine.py +124 -0
@@ -775,8 +775,14 @@ def lint_skill(path: Path, text: str) -> LintResult:
775
775
  # is *both* large AND prose-dominant OR ships ≥ 2 independently invocable
776
776
  # procedures. Reference catalogues (quality-tools 411 L / density 0.83)
777
777
  # pass; multi-procedure skills are flagged for split.
778
+ #
779
+ # Frontmatter opt-out: `meta_skill: true` exempts a skill from the size
780
+ # warn when the skill's purpose *is* breadth (skill-writing, agent-docs-
781
+ # writing, skill-reviewer, etc.). Meta-skills inherently bundle multiple
782
+ # procedures and inline examples.
778
783
  total_lines = len(text.splitlines())
779
- if total_lines > 400:
784
+ is_meta_skill = bool(fm) and re.search(r"^meta_skill:\s*true\s*$", fm, re.MULTILINE)
785
+ if total_lines > 400 and not is_meta_skill:
780
786
  density = _density_score(text)
781
787
  procedures = _count_procedure_sections(text)
782
788
  if density < 0.6 or procedures >= 2:
@@ -832,6 +838,12 @@ def lint_skill(path: Path, text: str) -> LintResult:
832
838
  f"{meaningful_steps} steps) — may lack its own executable workflow"))
833
839
  suggestions.append("Expand the skill so it remains executable without opening a guideline")
834
840
 
841
+ # --- evals.json schema validator ---
842
+ # When a skill ships sibling `evals/evals.json` (quantitative behavior
843
+ # eval per skill-writing § 7), validate its shape. Triggers.json is a
844
+ # separate concern handled elsewhere. All issues here are WARN.
845
+ issues.extend(validate_evals_json(path))
846
+
835
847
  return LintResult(
836
848
  file=str(path),
837
849
  artifact_type="skill",
@@ -841,6 +853,64 @@ def lint_skill(path: Path, text: str) -> LintResult:
841
853
  )
842
854
 
843
855
 
856
+ def validate_evals_json(skill_path: Path) -> list[Issue]:
857
+ """Validate `{skill_dir}/evals/evals.json` against the schema declared
858
+ in `skill-writing` § 7. Returns WARN-level issues only; never blocks.
859
+ Skipped entirely when the file is absent."""
860
+ evals_path = skill_path.parent / "evals" / "evals.json"
861
+ if not evals_path.is_file():
862
+ return []
863
+ issues: list[Issue] = []
864
+ try:
865
+ data = json.loads(evals_path.read_text(encoding="utf-8"))
866
+ except (OSError, json.JSONDecodeError) as exc:
867
+ return [Issue("warning", "evals_json_unreadable",
868
+ f"evals/evals.json could not be parsed: {exc}")]
869
+ if not isinstance(data, dict):
870
+ return [Issue("warning", "evals_json_shape",
871
+ "evals/evals.json root must be an object")]
872
+ if "skill" not in data or not isinstance(data["skill"], str):
873
+ issues.append(Issue("warning", "evals_json_missing_skill",
874
+ "evals/evals.json must declare top-level 'skill' (string)"))
875
+ scenarios = data.get("scenarios")
876
+ if not isinstance(scenarios, list) or len(scenarios) < 1:
877
+ issues.append(Issue("warning", "evals_json_no_scenarios",
878
+ "evals/evals.json must declare 'scenarios' (non-empty array)"))
879
+ return issues
880
+ valid_kinds = {"contains", "file_exists", "rubric"}
881
+ for idx, scenario in enumerate(scenarios):
882
+ loc = f"scenarios[{idx}]"
883
+ if not isinstance(scenario, dict):
884
+ issues.append(Issue("warning", "evals_json_scenario_shape",
885
+ f"{loc} must be an object"))
886
+ continue
887
+ for key in ("id", "prompt"):
888
+ if key not in scenario or not isinstance(scenario[key], str) or not scenario[key].strip():
889
+ issues.append(Issue("warning", "evals_json_scenario_missing_field",
890
+ f"{loc} missing required string field '{key}'"))
891
+ assertions = scenario.get("assertions")
892
+ if not isinstance(assertions, list) or len(assertions) < 1:
893
+ issues.append(Issue("warning", "evals_json_scenario_no_assertions",
894
+ f"{loc}.assertions must be a non-empty array"))
895
+ continue
896
+ for a_idx, assertion in enumerate(assertions):
897
+ a_loc = f"{loc}.assertions[{a_idx}]"
898
+ if not isinstance(assertion, dict):
899
+ issues.append(Issue("warning", "evals_json_assertion_shape",
900
+ f"{a_loc} must be an object"))
901
+ continue
902
+ kind = assertion.get("kind")
903
+ if kind not in valid_kinds:
904
+ issues.append(Issue("warning", "evals_json_assertion_kind",
905
+ f"{a_loc}.kind must be one of {sorted(valid_kinds)}, got {kind!r}"))
906
+ continue
907
+ required_field = {"contains": "value", "file_exists": "path", "rubric": "criterion"}[kind]
908
+ if required_field not in assertion or not isinstance(assertion[required_field], str):
909
+ issues.append(Issue("warning", "evals_json_assertion_missing_field",
910
+ f"{a_loc} (kind={kind}) missing required string field '{required_field}'"))
911
+ return issues
912
+
913
+
844
914
  def extract_frontmatter(text: str) -> Optional[str]:
845
915
  match = FRONTMATTER_PATTERN.search(text)
846
916
  return match.group(1) if match else None
@@ -2233,17 +2303,26 @@ def lint_type_boundaries(path: Path, text: str, artifact_type: str) -> List[Issu
2233
2303
  # Check frontmatter skills field
2234
2304
  frontmatter = extract_frontmatter(text)
2235
2305
  has_skills_field = False
2306
+ # Commands tagged `type: orchestrator` aggregate other commands /
2307
+ # routers — they intentionally do not declare a `skills:` list and
2308
+ # are exempt from the no-skill-reference check. The tag is the
2309
+ # contract; no hard-coded path list.
2310
+ is_orchestrator = False
2236
2311
  if frontmatter:
2237
2312
  skills_match = re.search(r'skills:\s*\[(.+)\]', frontmatter)
2238
2313
  has_skills_field = bool(skills_match and skills_match.group(1).strip())
2314
+ type_match = re.search(r'^type:\s*[\'"]?orchestrator[\'"]?\s*$',
2315
+ frontmatter, re.MULTILINE)
2316
+ is_orchestrator = bool(type_match)
2239
2317
 
2240
2318
  # Also check body for skill references
2241
2319
  has_skill_ref = bool(re.search(r'skill|SKILL\.md', text))
2242
2320
 
2243
- if not has_skills_field and not has_skill_ref:
2321
+ if not has_skills_field and not has_skill_ref and not is_orchestrator:
2244
2322
  issues.append(Issue("warning", "command_missing_skill_references",
2245
2323
  "Command does not reference any skills — "
2246
- "commands should orchestrate skills, not contain domain logic"))
2324
+ "commands should orchestrate skills, not contain domain logic "
2325
+ "(use `type: orchestrator` in frontmatter to exempt routers)"))
2247
2326
 
2248
2327
  # --- Skill: validation should be concrete, not vague ---
2249
2328
  if artifact_type == "skill":
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ """Structural smoke-test for the README Quickstart path.
3
+
4
+ Verifies the 3-step Quickstart from a fresh-project perspective:
5
+
6
+ 1. `scripts/install.py --project <tmpdir>` produces a usable
7
+ `.agent-settings.yml` with the documented default `cost_profile`.
8
+ 2. The decision_engine block (P2.x of road-to-productization) parses
9
+ cleanly through the same engine parser the runtime uses.
10
+ 3. The work-engine state-file format (`agents/state/<id>.json`) is
11
+ emit-ready — schema for `decision_result` matches the contract.
12
+
13
+ What it does NOT do:
14
+ - Invoke a real LLM agent (CI doesn't run a model). The end-to-end
15
+ `/onboard → /work → decision_result` chain still requires the host
16
+ agent. This smoke test asserts the *mechanics* the agent depends
17
+ on, so a Quickstart break is caught before the agent ever runs.
18
+
19
+ Exit codes: 0 = green; 1 = one or more checks failed; 2 = setup error.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import shutil
24
+ import subprocess
25
+ import sys
26
+ import tempfile
27
+ from pathlib import Path
28
+
29
+ ROOT = Path(__file__).resolve().parent.parent
30
+ INSTALLER = ROOT / "scripts" / "install.py"
31
+ TEMPLATE = ROOT / "config" / "agent-settings.template.yml"
32
+
33
+ EXPECTED_DEFAULT_PROFILE = "balanced"
34
+
35
+
36
+ def _fail(msg: str) -> int:
37
+ print(f"::error::{msg}", file=sys.stderr)
38
+ return 1
39
+
40
+
41
+ def _check_installer_runs(tmpdir: Path) -> tuple[int, Path | None]:
42
+ """Step 1 — run installer against a fresh tmpdir."""
43
+ cmd = [
44
+ sys.executable,
45
+ str(INSTALLER),
46
+ "--project",
47
+ str(tmpdir),
48
+ "--package",
49
+ str(ROOT),
50
+ "--skip-bridges",
51
+ ]
52
+ try:
53
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
54
+ except subprocess.TimeoutExpired:
55
+ return _fail("installer timed out after 60s"), None
56
+ if result.returncode != 0:
57
+ return (
58
+ _fail(f"installer exited {result.returncode}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"),
59
+ None,
60
+ )
61
+ settings = tmpdir / ".agent-settings.yml"
62
+ if not settings.exists():
63
+ return _fail(".agent-settings.yml not written by installer"), None
64
+ return 0, settings
65
+
66
+
67
+ def _check_default_profile(settings: Path) -> int:
68
+ """Step 2 — assert default cost_profile matches the contract."""
69
+ import yaml
70
+
71
+ parsed = yaml.safe_load(settings.read_text(encoding="utf-8"))
72
+ if not isinstance(parsed, dict):
73
+ return _fail(f"{settings.name}: top-level is not a YAML mapping")
74
+ profile = parsed.get("cost_profile")
75
+ if profile != EXPECTED_DEFAULT_PROFILE:
76
+ return _fail(
77
+ f"cost_profile drift: docs/contracts/cost-profile-defaults.md "
78
+ f"declares '{EXPECTED_DEFAULT_PROFILE}', settings has '{profile!r}'"
79
+ )
80
+ return 0
81
+
82
+
83
+ def _check_decision_engine_block(settings: Path) -> int:
84
+ """Step 3 — decision_engine block parses through the engine parser."""
85
+ sys.path.insert(0, str(ROOT / ".agent-src.uncompressed" / "templates" / "scripts"))
86
+ try:
87
+ from work_engine.scoring.decision_engine import ( # type: ignore[import-not-found]
88
+ DecisionEngineSettings,
89
+ parse as parse_decision_engine,
90
+ )
91
+ except ImportError as exc:
92
+ return _fail(f"decision_engine module not importable: {exc}")
93
+
94
+ import yaml
95
+
96
+ parsed = yaml.safe_load(settings.read_text(encoding="utf-8"))
97
+ block = parsed.get("decision_engine") if isinstance(parsed, dict) else None
98
+ try:
99
+ settings_obj = parse_decision_engine(block)
100
+ except Exception as exc: # noqa: BLE001 — surface the schema error
101
+ return _fail(f"decision_engine block rejected by parser: {exc}")
102
+ if not isinstance(settings_obj, DecisionEngineSettings):
103
+ return _fail("parser returned non-DecisionEngineSettings instance")
104
+ return 0
105
+
106
+
107
+ def main() -> int:
108
+ if not INSTALLER.exists():
109
+ print(f"::error::installer not found at {INSTALLER}", file=sys.stderr)
110
+ return 2
111
+ if not TEMPLATE.exists():
112
+ print(f"::error::template not found at {TEMPLATE}", file=sys.stderr)
113
+ return 2
114
+
115
+ failures = 0
116
+ tmpdir = Path(tempfile.mkdtemp(prefix="agent-config-quickstart-"))
117
+ try:
118
+ rc, settings = _check_installer_runs(tmpdir)
119
+ failures += rc
120
+ if settings is not None:
121
+ failures += _check_default_profile(settings)
122
+ failures += _check_decision_engine_block(settings)
123
+ finally:
124
+ shutil.rmtree(tmpdir, ignore_errors=True)
125
+
126
+ if failures:
127
+ print(f"\n❌ smoke-quickstart: {failures} check(s) failed", file=sys.stderr)
128
+ return 1
129
+ print("✅ smoke-quickstart: install → settings → decision_engine green")
130
+ return 0
131
+
132
+
133
+ if __name__ == "__main__":
134
+ sys.exit(main())
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Decision-engine settings validator (road-to-productization P2).
4
+
5
+ Walks every ``agent-settings.yml`` / ``agent-settings.template.yml``
6
+ under the repo, parses any ``decision_engine`` block via the canonical
7
+ ``work_engine.scoring.decision_engine.parse`` schema, and surfaces:
8
+
9
+ - hard errors → exit 1 (unknown keys, invalid enum values, bad types).
10
+ - warnings → exit 0 with a ``::warning::`` line per finding
11
+ (gates active but ``hooks.enabled`` is false → gates won't fire).
12
+
13
+ Contract: ``docs/contracts/decision-engine-gates.md``. Wired into
14
+ ``task ci`` via ``taskfiles/ci-fast.yml`` so configuration drift is
15
+ caught before a Decision Engine surprise lands in main.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ try:
24
+ import yaml
25
+ except ImportError: # pragma: no cover — bootstrap guard
26
+ print("::error::PyYAML not installed; cannot validate decision_engine block")
27
+ sys.exit(3)
28
+
29
+ REPO_ROOT = Path(__file__).resolve().parent.parent
30
+ TEMPLATE_SCRIPTS = REPO_ROOT / ".agent-src.uncompressed" / "templates" / "scripts"
31
+ if str(TEMPLATE_SCRIPTS) not in sys.path:
32
+ sys.path.insert(0, str(TEMPLATE_SCRIPTS))
33
+
34
+ from work_engine.scoring.decision_engine import ( # noqa: E402
35
+ DecisionEngineConfigError,
36
+ parse,
37
+ )
38
+
39
+ # Files we always validate, even if they don't exist (template is
40
+ # canonical — its absence is itself a regression).
41
+ TEMPLATE_PATH = REPO_ROOT / "config" / "agent-settings.template.yml"
42
+ # Project-level overrides developers may have on disk locally.
43
+ LOCAL_PATHS = [REPO_ROOT / ".agent-settings.yml"]
44
+
45
+
46
+ def _load_yaml(path: Path) -> dict | None:
47
+ if not path.is_file():
48
+ return None
49
+ try:
50
+ raw = yaml.safe_load(path.read_text(encoding="utf-8"))
51
+ except yaml.YAMLError as exc:
52
+ print(f"::error file={path}::malformed YAML: {exc}")
53
+ return {}
54
+ if raw is None:
55
+ return {}
56
+ if not isinstance(raw, dict):
57
+ print(f"::error file={path}::top-level must be a mapping")
58
+ return {}
59
+ return raw
60
+
61
+
62
+ def _validate(path: Path, doc: dict) -> tuple[int, int]:
63
+ """Return ``(errors, warnings)`` counts for ``doc``."""
64
+ errors = 0
65
+ warnings = 0
66
+ block = doc.get("decision_engine")
67
+ if block is None:
68
+ return 0, 0
69
+ try:
70
+ settings = parse(block)
71
+ except DecisionEngineConfigError as exc:
72
+ rel = path.relative_to(REPO_ROOT)
73
+ print(f"::error file={rel}::decision_engine: {exc}")
74
+ return 1, 0
75
+ if settings.any_gate_active:
76
+ hooks_block = doc.get("hooks") or {}
77
+ if isinstance(hooks_block, dict) and hooks_block.get("enabled") is False:
78
+ rel = path.relative_to(REPO_ROOT)
79
+ print(
80
+ f"::warning file={rel}::decision_engine gates configured "
81
+ "(min_confidence/block_on_risk/require_memory_hits) but "
82
+ "hooks.enabled=false — gates will not fire. Either enable "
83
+ "hooks or remove the gate keys."
84
+ )
85
+ warnings += 1
86
+ return errors, warnings
87
+
88
+
89
+ def main() -> int:
90
+ total_errors = 0
91
+ total_warnings = 0
92
+ paths: list[Path] = []
93
+ if TEMPLATE_PATH.is_file():
94
+ paths.append(TEMPLATE_PATH)
95
+ else:
96
+ print(f"::error file={TEMPLATE_PATH}::template missing")
97
+ return 1
98
+ for candidate in LOCAL_PATHS:
99
+ if candidate.is_file():
100
+ paths.append(candidate)
101
+ for path in paths:
102
+ doc = _load_yaml(path)
103
+ if doc is None:
104
+ continue
105
+ errors, warnings = _validate(path, doc)
106
+ total_errors += errors
107
+ total_warnings += warnings
108
+ if total_errors:
109
+ return 1
110
+ if total_warnings:
111
+ # Warnings already printed; CI treats exit 0 + ::warning:: as
112
+ # green-with-note. Surface a summary for human readers.
113
+ print(
114
+ f"decision_engine: {total_warnings} warning(s); see ::warning:: lines above"
115
+ )
116
+ return 0
117
+
118
+
119
+ if __name__ == "__main__":
120
+ try:
121
+ sys.exit(main())
122
+ except Exception as exc: # noqa: BLE001
123
+ print(f"::error::validate_decision_engine internal error: {exc}")
124
+ sys.exit(3)