@event4u/agent-config 2.17.0 → 2.19.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 (32) hide show
  1. package/.agent-src/commands/refine-ticket.md +3 -0
  2. package/.agent-src/personas/README.md +8 -0
  3. package/.agent-src/skills/refine-ticket/SKILL.md +3 -0
  4. package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  5. package/.agent-src/user-types/README.md +124 -0
  6. package/.agent-src/user-types/_template/user-type.md +95 -0
  7. package/.agent-src/user-types/galabau-field-crew.md +100 -0
  8. package/.agent-src/user-types/metalworking-shop.md +105 -0
  9. package/.agent-src/user-types/truck-driver.md +113 -0
  10. package/.claude-plugin/marketplace.json +1 -1
  11. package/CHANGELOG.md +68 -0
  12. package/config/agent-settings.template.yml +7 -0
  13. package/docs/catalog.md +1 -1
  14. package/docs/contracts/adr-install-user-type-axis.md +107 -0
  15. package/docs/contracts/adr-mcp-runtime.md +128 -0
  16. package/docs/contracts/adr-user-types-axis.md +127 -0
  17. package/docs/contracts/init-telemetry.md +2 -3
  18. package/docs/contracts/user-type-schema.md +146 -0
  19. package/docs/getting-started-by-role.md +1 -1
  20. package/docs/recruits/_template.md +81 -0
  21. package/package.json +1 -1
  22. package/scripts/audit_user_type_axis.py +140 -0
  23. package/scripts/compress.py +48 -2
  24. package/scripts/install +9 -1
  25. package/scripts/install.py +81 -7
  26. package/scripts/install.sh +7 -0
  27. package/scripts/mcp_server/prompts.py +134 -2
  28. package/scripts/schemas/user-type-axis.schema.json +56 -0
  29. package/scripts/schemas/user-type.schema.json +35 -0
  30. package/scripts/skill_linter.py +139 -4
  31. package/scripts/skill_tools/audit_user_type_coverage.py +148 -0
  32. package/scripts/sync_agent_settings.py +6 -0
@@ -0,0 +1,56 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/event4u-app/agent-config/scripts/schemas/user-type-axis.schema.json",
4
+ "title": "User-type axis (install filter)",
5
+ "$comment": "Source: user-types/README.md § Schema. One file per user-type under user-types/<id>.yml. `id` MUST equal the filename stem. Owning roadmap: agents/roadmaps/step-9-user-types-axis.md.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["id", "description"],
9
+ "properties": {
10
+ "id": {
11
+ "type": "string",
12
+ "pattern": "^[a-z][a-z0-9-]*$",
13
+ "minLength": 2,
14
+ "maxLength": 40,
15
+ "description": "Stable slug. MUST equal the filename stem and the value used in skills' `recommended_for_user_types` frontmatter."
16
+ },
17
+ "description": {
18
+ "type": "string",
19
+ "minLength": 1,
20
+ "maxLength": 240,
21
+ "description": "One-line audience description shown in install.sh --help and the user-types/README.md index."
22
+ },
23
+ "primary_workflows": {
24
+ "type": "array",
25
+ "minItems": 1,
26
+ "maxItems": 12,
27
+ "uniqueItems": true,
28
+ "items": {
29
+ "type": "string",
30
+ "pattern": "^[a-z][a-z0-9_]*$",
31
+ "minLength": 2,
32
+ "maxLength": 60
33
+ },
34
+ "description": "Snake-case verb-phrases describing what this user-type does most days. Informs which skills surface first."
35
+ },
36
+ "default_skill_priority": {
37
+ "type": "array",
38
+ "minItems": 1,
39
+ "maxItems": 12,
40
+ "uniqueItems": true,
41
+ "items": {
42
+ "type": "string",
43
+ "pattern": "^[a-z][a-z0-9-]*$",
44
+ "minLength": 2,
45
+ "maxLength": 60
46
+ },
47
+ "description": "Hand-curated short-list of 3–6 skill stems that should auto-load on `install.sh --user-type=<id>`."
48
+ },
49
+ "notes": {
50
+ "type": "string",
51
+ "minLength": 1,
52
+ "maxLength": 480,
53
+ "description": "Optional one-line caveat (overlap with sibling user-types, legacy semantics, etc.)."
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/event4u-app/agent-config/scripts/schemas/user-type.schema.json",
4
+ "title": "User-type frontmatter",
5
+ "$comment": "Source: docs/contracts/user-type-schema.md. The runtime review-lens axis seeded under .agent-src.uncompressed/user-types/. Distinct from the install-time user-type-axis YAMLs in user-types/ (root) which carry their own user-type-axis.schema.json.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["id", "kind", "description", "version", "source"],
9
+ "properties": {
10
+ "id": {
11
+ "type": "string",
12
+ "pattern": "^[a-z][a-z0-9-]*$",
13
+ "description": "Must match the user-type filename stem."
14
+ },
15
+ "kind": {
16
+ "type": "string",
17
+ "const": "user-type",
18
+ "description": "Discriminator — locks this file as a review-lens user-type, distinct from the install-time user-type-axis YAMLs."
19
+ },
20
+ "description": {
21
+ "type": "string",
22
+ "minLength": 1,
23
+ "maxLength": 240
24
+ },
25
+ "version": {
26
+ "type": "string",
27
+ "pattern": "^[0-9]+(\\.[0-9]+){0,2}$"
28
+ },
29
+ "source": {
30
+ "type": "string",
31
+ "enum": ["package", "project"],
32
+ "description": "Project-specific the typical case — most end-user simulations are consumer-domain, not package-defaults."
33
+ }
34
+ }
35
+ }
@@ -39,7 +39,7 @@ from validate_frontmatter import ( # noqa: E402
39
39
  )
40
40
 
41
41
  Severity = Literal["error", "warning", "info"]
42
- ArtifactType = Literal["skill", "rule", "command", "guideline", "persona", "unknown"]
42
+ ArtifactType = Literal["skill", "rule", "command", "guideline", "persona", "user-type", "unknown"]
43
43
 
44
44
  REQUIRED_PERSONA_SECTIONS_CORE = [
45
45
  "Focus",
@@ -57,6 +57,20 @@ REQUIRED_PERSONA_SECTIONS = REQUIRED_PERSONA_SECTIONS_CORE
57
57
  VALID_PERSONA_TIERS = {"core", "specialist"}
58
58
  # Locked in docs/contracts/persona-schema.md § 4: core ≤ 120, specialist ≤ 100.
59
59
  PERSONA_LINE_BUDGETS = {"core": 120, "specialist": 100}
60
+
61
+ # User-type spine — locked in docs/contracts/user-type-schema.md § 3.
62
+ # Runtime end-user simulation lens (sister axis to personas — methodology vs
63
+ # end-user). Single tier in v1 (no core/specialist split).
64
+ REQUIRED_USERTYPE_SECTIONS = [
65
+ "Focus",
66
+ "Daily Workflow",
67
+ "Vocabulary",
68
+ "Operational Constraints",
69
+ "Unique Questions",
70
+ "Ticket Red Flags",
71
+ "Anti-Patterns",
72
+ ]
73
+ USERTYPE_LINE_BUDGET = 120
60
74
  # Wing-scoped overrides — Wing-3 (GTM) and Wing-4 (Money/Strategy/Ops) carry
61
75
  # denser cognition (funnel × channel × lifecycle, or finance × org × strategy)
62
76
  # than Wing-1/2 specialists, so the line cap rises to keep the seven-section
@@ -514,6 +528,8 @@ def detect_artifact_type(path: Path, text: str) -> ArtifactType:
514
528
  return "guideline"
515
529
  if "/personas/" in path_str:
516
530
  return "persona"
531
+ if "/user-types/" in path_str:
532
+ return "user-type"
517
533
  if has_skill_heading:
518
534
  return "skill"
519
535
  return "unknown"
@@ -1849,6 +1865,106 @@ def lint_persona(path: Path, text: str) -> LintResult:
1849
1865
  )
1850
1866
 
1851
1867
 
1868
+ def lint_usertype(path: Path, text: str) -> LintResult:
1869
+ """Lint a user-type .md file (frontmatter schema + required sections + size).
1870
+
1871
+ User-types are the runtime end-user simulation lens (sister axis to
1872
+ personas — methodology vs end-user). Contract:
1873
+ docs/contracts/user-type-schema.md.
1874
+ """
1875
+ issues: List[Issue] = []
1876
+
1877
+ frontmatter = extract_frontmatter(text)
1878
+ if not frontmatter:
1879
+ issues.append(Issue("error", "missing_frontmatter", "User-type requires YAML frontmatter"))
1880
+ return LintResult(
1881
+ file=str(path),
1882
+ artifact_type="user-type",
1883
+ status="fail",
1884
+ issues=issues,
1885
+ suggestions=[".agent-src.uncompressed/user-types/_template/user-type.md"],
1886
+ )
1887
+
1888
+ # Required keys per docs/contracts/user-type-schema.md § 1.
1889
+ required = {
1890
+ "id": re.compile(r'^id:\s*"?([\w-]+)"?\s*$', re.MULTILINE),
1891
+ "kind": re.compile(r'^kind:\s*"?([\w-]+)"?\s*$', re.MULTILINE),
1892
+ "description": re.compile(r'^description:\s*"?([^"\n]+?)"?\s*$', re.MULTILINE),
1893
+ "version": re.compile(r'^version:\s*"?([\d.]+)"?\s*$', re.MULTILINE),
1894
+ "source": re.compile(r'^source:\s*"?(package|project)"?\s*$', re.MULTILINE),
1895
+ }
1896
+ parsed: dict = {}
1897
+ for field, pattern in required.items():
1898
+ value = extract_frontmatter_field(frontmatter, pattern)
1899
+ if not value:
1900
+ issues.append(Issue("error", f"missing_{field}", f"User-type frontmatter must declare `{field}`"))
1901
+ else:
1902
+ parsed[field] = value
1903
+
1904
+ if "id" in parsed and parsed["id"] != path.stem:
1905
+ issues.append(Issue(
1906
+ "error",
1907
+ "id_filename_mismatch",
1908
+ f"User-type id `{parsed['id']}` must match filename stem `{path.stem}`",
1909
+ ))
1910
+
1911
+ if "kind" in parsed and parsed["kind"] != "user-type":
1912
+ issues.append(Issue(
1913
+ "error",
1914
+ "invalid_kind",
1915
+ f"User-type kind must be `user-type` (got `{parsed['kind']}`)",
1916
+ ))
1917
+
1918
+ if "description" in parsed and len(parsed["description"]) > 160:
1919
+ issues.append(Issue(
1920
+ "warning",
1921
+ "long_description",
1922
+ f"User-type description is {len(parsed['description'])} chars (target ≤ 160)",
1923
+ ))
1924
+
1925
+ sections = extract_sections(text)
1926
+ for required_section in REQUIRED_USERTYPE_SECTIONS:
1927
+ if required_section not in sections:
1928
+ issues.append(Issue(
1929
+ "error",
1930
+ "missing_section",
1931
+ f"User-type is missing required section `## {required_section}`",
1932
+ ))
1933
+
1934
+ # Anti-Generic Quality Bar: ≥ 3 Unique Questions
1935
+ uq_block = extract_section_block(text, "Unique Questions")
1936
+ if uq_block:
1937
+ bullet_count = len(re.findall(r"^\s*[-*]\s+", uq_block, re.MULTILINE))
1938
+ if bullet_count < 3:
1939
+ issues.append(Issue(
1940
+ "warning",
1941
+ "too_few_unique_questions",
1942
+ f"User-type has {bullet_count} unique questions (target ≥ 3)",
1943
+ ))
1944
+
1945
+ line_count = len(text.splitlines())
1946
+ if line_count > USERTYPE_LINE_BUDGET:
1947
+ issues.append(Issue(
1948
+ "warning",
1949
+ "size_budget",
1950
+ f"User-type has {line_count} lines (budget ≤ {USERTYPE_LINE_BUDGET})",
1951
+ ))
1952
+
1953
+ if not H1_PATTERN.search(text):
1954
+ issues.append(Issue("warning", "missing_h1", "User-type is missing an H1 heading"))
1955
+
1956
+ if not text.endswith("\n"):
1957
+ issues.append(Issue("warning", "no_trailing_newline", "File must end with exactly one newline"))
1958
+
1959
+ return LintResult(
1960
+ file=str(path),
1961
+ artifact_type="user-type",
1962
+ status=classify_status(issues),
1963
+ issues=issues,
1964
+ suggestions=[],
1965
+ )
1966
+
1967
+
1852
1968
  def gather_all_candidate_files(root: Path) -> list[Path]:
1853
1969
  """Gather all lintable files. Prefers .agent-src.uncompressed/ (source of truth).
1854
1970
  Falls back to .agent-src/ only if uncompressed doesn't exist.
@@ -1906,6 +2022,18 @@ def gather_all_candidate_files(root: Path) -> list[Path]:
1906
2022
  if not f.is_symlink():
1907
2023
  candidates.append(f)
1908
2024
 
2025
+ # User-types (sister axis to personas — methodology vs end-user).
2026
+ # Top-level .md only; README and _template/ subtree excluded.
2027
+ uncompressed_usertypes = root / ".agent-src.uncompressed" / "user-types"
2028
+ augment_usertypes = root / ".agent-src" / "user-types"
2029
+ usertypes_base = uncompressed_usertypes if uncompressed_usertypes.exists() else augment_usertypes
2030
+ if usertypes_base.exists():
2031
+ for f in usertypes_base.glob("*.md"):
2032
+ if f.name.lower() == "readme.md":
2033
+ continue
2034
+ if not f.is_symlink():
2035
+ candidates.append(f)
2036
+
1909
2037
  # Frugality charter (Phase 0.4 Layer 2). Lives in contexts/, not
1910
2038
  # walked by the artifact-type loops above, but still needs the
1911
2039
  # index-integrity check.
@@ -2093,8 +2221,13 @@ def _is_execution_artifact(path: Path, text: str) -> bool:
2093
2221
  path_lower = str(path).lower()
2094
2222
  text_lower = text.lower()
2095
2223
 
2096
- # Exclude commands, guidelines, and personas — they are not execution-oriented
2097
- if "/commands/" in path_lower or "/guidelines/" in path_lower or "/personas/" in path_lower:
2224
+ # Exclude commands, guidelines, personas, user-types — not execution-oriented
2225
+ if (
2226
+ "/commands/" in path_lower
2227
+ or "/guidelines/" in path_lower
2228
+ or "/personas/" in path_lower
2229
+ or "/user-types/" in path_lower
2230
+ ):
2098
2231
  return False
2099
2232
 
2100
2233
  # File name match — strong signal
@@ -2779,7 +2912,7 @@ def lint_output_schema(path: Path, text: str) -> List[Issue]:
2779
2912
 
2780
2913
 
2781
2914
  # Artefact types that carry a JSON-Schema contract for their frontmatter.
2782
- _SCHEMA_ARTEFACT_TYPES = {"skill", "rule", "command", "persona"}
2915
+ _SCHEMA_ARTEFACT_TYPES = {"skill", "rule", "command", "persona", "user-type"}
2783
2916
 
2784
2917
 
2785
2918
  def lint_frontmatter_schema(path: Path, text: str, artifact_type: str) -> List[Issue]:
@@ -2840,6 +2973,8 @@ def lint_file(path: Path, repo_root: Path | None = None) -> LintResult:
2840
2973
  result = lint_guideline(display_path, text)
2841
2974
  elif artifact_type == "persona":
2842
2975
  result = lint_persona(display_path, text)
2976
+ elif artifact_type == "user-type":
2977
+ result = lint_usertype(display_path, text)
2843
2978
  else:
2844
2979
  # Frugality charter lives in contexts/ (artifact_type == unknown)
2845
2980
  # but still needs Layer 2 index-integrity validation.
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python3
2
+ """Block D · D3 — audit_user_type_coverage.
3
+
4
+ Coverage audit for the user-type axis. User-types are **CLI-only** in v1
5
+ (see `docs/contracts/adr-user-types-axis.md` and Phase 4 step 3 of
6
+ `agents/roadmaps/step-6-user-types-axis.md`) — skills do NOT declare a
7
+ `user-types:` frontmatter key, so persona-style citation counting does
8
+ not apply. Instead this script:
9
+
10
+ - Inventories every user-type file in the source directory.
11
+ - Scans skills, commands, and `docs/` for `--user-type=<id>` mentions.
12
+ - Flags **orphan references** (CLI mention to a non-existent id) and
13
+ **never-referenced** user-types (file exists but nobody cites it).
14
+
15
+ Inputs:
16
+ --user-types-dir DIR — directory holding user-type Markdown files
17
+ --search-root DIR — root to recurse for `--user-type=<id>` mentions
18
+ --json — machine-readable output
19
+
20
+ Output: per-user-type reference count + status (ok / never-referenced /
21
+ orphan). Exit code: 0 always (advisory, not a CI gate).
22
+
23
+ Stdlib-only. ≤ 130 LOC. Sibling of `audit_persona_coverage.py`.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import re
30
+ import sys
31
+ from pathlib import Path
32
+ from typing import Dict, List, Set
33
+
34
+ ROOT = Path(__file__).resolve().parents[2]
35
+ DEFAULT_USER_TYPES = ROOT / ".agent-src.uncompressed" / "user-types"
36
+ DEFAULT_SEARCH_ROOT = ROOT / ".agent-src.uncompressed"
37
+ REFERENCE_THRESHOLD = 1 # user-type with 0 references → flagged.
38
+
39
+ # Matches `--user-type=<id>` in command markdown, skill prose, docs.
40
+ _REFERENCE_RE = re.compile(r"--user-type=([\w-]+)")
41
+
42
+
43
+ def _read_block(path: Path) -> str:
44
+ text = path.read_text(encoding="utf-8", errors="replace")
45
+ if not text.startswith("---"):
46
+ return ""
47
+ end = text.find("\n---", 3)
48
+ return text[3:end] if end != -1 else ""
49
+
50
+
51
+ def _frontmatter_value(block: str, key: str) -> str | None:
52
+ m = re.search(rf"^{re.escape(key)}\s*:\s*(.+)$", block, re.MULTILINE)
53
+ if not m:
54
+ return None
55
+ val = m.group(1).strip()
56
+ if val.startswith('"') and val.endswith('"'):
57
+ val = val[1:-1]
58
+ return val
59
+
60
+
61
+ def _load_user_types(user_types_dir: Path) -> Set[str]:
62
+ ids: Set[str] = set()
63
+ if not user_types_dir.is_dir():
64
+ return ids
65
+ for md in sorted(user_types_dir.glob("*.md")):
66
+ if md.name.lower() == "readme.md":
67
+ continue
68
+ block = _read_block(md)
69
+ slug = _frontmatter_value(block, "id") or md.stem
70
+ ids.add(slug)
71
+ # Walk one level deeper to skip `_template/` etc.
72
+ for md in sorted(user_types_dir.glob("*/*.md")):
73
+ if "_template" in md.parts:
74
+ continue
75
+ block = _read_block(md)
76
+ slug = _frontmatter_value(block, "id") or md.parent.name
77
+ ids.add(slug)
78
+ return ids
79
+
80
+
81
+ def _count_references(search_root: Path, skip_dir: Path) -> Dict[str, int]:
82
+ counts: Dict[str, int] = {}
83
+ if not search_root.is_dir():
84
+ return counts
85
+ skip_resolved = skip_dir.resolve() if skip_dir.is_dir() else None
86
+ for md in search_root.rglob("*.md"):
87
+ # Don't count references inside the user-types dir itself
88
+ # (the README documents the flag in example form).
89
+ if skip_resolved and skip_resolved in md.resolve().parents:
90
+ continue
91
+ text = md.read_text(encoding="utf-8", errors="replace")
92
+ for slug in _REFERENCE_RE.findall(text):
93
+ counts[slug] = counts.get(slug, 0) + 1
94
+ return counts
95
+
96
+
97
+ def audit(user_types_dir: Path, search_root: Path) -> List[Dict[str, object]]:
98
+ ids = _load_user_types(user_types_dir)
99
+ references = _count_references(search_root, user_types_dir)
100
+ rows: List[Dict[str, object]] = []
101
+ for slug in sorted(ids):
102
+ count = references.get(slug, 0)
103
+ status = "ok" if count >= REFERENCE_THRESHOLD else "never-referenced"
104
+ rows.append({"user_type": slug, "references": count,
105
+ "threshold": REFERENCE_THRESHOLD, "status": status})
106
+ for slug in sorted(references.keys()):
107
+ if slug not in ids:
108
+ rows.append({"user_type": slug, "references": references[slug],
109
+ "threshold": REFERENCE_THRESHOLD, "status": "orphan"})
110
+ return rows
111
+
112
+
113
+ def _print_human(rows: List[Dict[str, object]]) -> None:
114
+ if not rows:
115
+ print("(no user-types found)")
116
+ return
117
+ width = max(len(str(r["user_type"])) for r in rows)
118
+ print(f" {'user-type':<{width}} refs status")
119
+ print(f" {'-' * width} ----- ----------------")
120
+ for r in rows:
121
+ print(f" {str(r['user_type']):<{width}} "
122
+ f"{int(r['references']):>5} {r['status']}")
123
+ flagged = [r for r in rows if r["status"] != "ok"]
124
+ if flagged:
125
+ print(f"\n {len(flagged)} user-type(s) flagged "
126
+ f"(never-referenced or orphan).")
127
+
128
+
129
+ def main(argv: List[str] | None = None) -> int:
130
+ parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
131
+ parser.add_argument("--user-types-dir", default=str(DEFAULT_USER_TYPES))
132
+ parser.add_argument("--search-root", default=str(DEFAULT_SEARCH_ROOT))
133
+ parser.add_argument("--json", action="store_true",
134
+ help="emit JSON instead of text")
135
+ args = parser.parse_args(argv)
136
+ rows = audit(Path(args.user_types_dir), Path(args.search_root))
137
+ if args.json:
138
+ json.dump({"rows": rows}, sys.stdout, indent=2)
139
+ sys.stdout.write("\n")
140
+ else:
141
+ _print_human(rows)
142
+ return 0
143
+
144
+
145
+ _SAMPLE = {"threshold": REFERENCE_THRESHOLD}
146
+
147
+ if __name__ == "__main__":
148
+ raise SystemExit(main())
@@ -113,6 +113,12 @@ def main(argv: list[str] | None = None) -> int:
113
113
  print(f"error: unsupported profile {profile!r}", file=sys.stderr)
114
114
  return 2
115
115
  profile_values = load_profile(profile_dir, profile)
116
+ # Preserve existing user_type (step-9 axis) so the template's
117
+ # __USER_TYPE__ placeholder renders without forcing the user to
118
+ # re-pass --user-type on every sync. Empty string = no filter.
119
+ personal = user_data.get("personal") if isinstance(user_data.get("personal"), dict) else {}
120
+ existing_user_type = str(personal.get("user_type") or "") if personal else ""
121
+ profile_values["user_type"] = existing_user_type
116
122
  template_body = load_template(template_path, profile_values)
117
123
  except FileNotFoundError as exc:
118
124
  print(f"error: {exc}", file=sys.stderr)