@event4u/agent-config 2.16.0 → 2.17.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 (88) hide show
  1. package/.agent-src/commands/ghostwriter/delete.md +118 -0
  2. package/.agent-src/commands/ghostwriter/fetch.md +185 -0
  3. package/.agent-src/commands/ghostwriter/list.md +102 -0
  4. package/.agent-src/commands/ghostwriter/show.md +113 -0
  5. package/.agent-src/commands/ghostwriter/write.md +160 -0
  6. package/.agent-src/commands/ghostwriter.md +96 -0
  7. package/.agent-src/commands/post-as/ghostwriter.md +66 -0
  8. package/.agent-src/commands/post-as/me.md +124 -0
  9. package/.agent-src/commands/post-as.md +58 -0
  10. package/.agent-src/ghostwriter/README.md +61 -0
  11. package/.agent-src/ghostwriter/fictional-fixture-v1.md +94 -0
  12. package/.agent-src/personas/README.md +8 -0
  13. package/.agent-src/rules/domain-safety-disclaimer-consulting.md +52 -0
  14. package/.agent-src/rules/domain-safety-disclaimer-financial.md +54 -0
  15. package/.agent-src/rules/domain-safety-disclaimer-legal.md +49 -0
  16. package/.agent-src/rules/domain-safety-disclaimer-medical.md +56 -0
  17. package/.agent-src/rules/domain-safety-export-redact.md +65 -0
  18. package/.agent-src/rules/domain-safety-logging-pii-floor.md +55 -0
  19. package/.agent-src/rules/domain-safety-pii-finance.md +57 -0
  20. package/.agent-src/rules/domain-safety-pii-marketing.md +60 -0
  21. package/.agent-src/rules/domain-safety-pii-recruiting.md +56 -0
  22. package/.agent-src/rules/domain-safety-pii-support.md +57 -0
  23. package/.agent-src/rules/domain-safety-retention-finance.md +48 -0
  24. package/.agent-src/rules/domain-safety-retention-support.md +55 -0
  25. package/.agent-src/skills/api-design/SKILL.md +3 -0
  26. package/.agent-src/skills/authz-review/SKILL.md +3 -0
  27. package/.agent-src/skills/competitive-moat-analysis/SKILL.md +3 -0
  28. package/.agent-src/skills/competitive-positioning/SKILL.md +3 -0
  29. package/.agent-src/skills/content-funnel-design/SKILL.md +3 -0
  30. package/.agent-src/skills/contracts-cognition/SKILL.md +3 -0
  31. package/.agent-src/skills/dashboard-design/SKILL.md +3 -0
  32. package/.agent-src/skills/data-handling-judgment/SKILL.md +3 -0
  33. package/.agent-src/skills/dcf-modeling/SKILL.md +3 -0
  34. package/.agent-src/skills/deal-qualification-meddic/SKILL.md +3 -0
  35. package/.agent-src/skills/discovery-interview/SKILL.md +3 -0
  36. package/.agent-src/skills/editorial-calendar/SKILL.md +3 -0
  37. package/.agent-src/skills/forecast-accuracy/SKILL.md +3 -0
  38. package/.agent-src/skills/forecasting/SKILL.md +3 -0
  39. package/.agent-src/skills/fundraising-narrative/SKILL.md +3 -0
  40. package/.agent-src/skills/gtm-launch/SKILL.md +3 -0
  41. package/.agent-src/skills/incident-commander/SKILL.md +3 -0
  42. package/.agent-src/skills/launch-readiness/SKILL.md +3 -0
  43. package/.agent-src/skills/messaging-architecture/SKILL.md +3 -0
  44. package/.agent-src/skills/okr-tree-modeling/SKILL.md +3 -0
  45. package/.agent-src/skills/pipeline-strategy/SKILL.md +3 -0
  46. package/.agent-src/skills/playwright-architect/SKILL.md +3 -0
  47. package/.agent-src/skills/privacy-review/SKILL.md +4 -1
  48. package/.agent-src/skills/quality-tools/SKILL.md +3 -0
  49. package/.agent-src/skills/release-comms/SKILL.md +3 -0
  50. package/.agent-src/skills/runway-cognition/SKILL.md +3 -0
  51. package/.agent-src/skills/scenario-modeling/SKILL.md +3 -0
  52. package/.agent-src/skills/secrets-management/SKILL.md +3 -0
  53. package/.agent-src/skills/tech-debt-tracker/SKILL.md +3 -0
  54. package/.agent-src/skills/unit-economics-modeling/SKILL.md +3 -0
  55. package/.agent-src/skills/voc-extract/SKILL.md +3 -0
  56. package/.agent-src/skills/voice-and-tone-design/SKILL.md +3 -0
  57. package/.agent-src/templates/agents/agent-project-settings.example.yml +16 -1
  58. package/.claude-plugin/marketplace.json +10 -1
  59. package/CHANGELOG.md +69 -0
  60. package/README.md +44 -23
  61. package/config/gitignore-block.txt +8 -0
  62. package/docs/announcements/2026-05-non-dev-launch.md +79 -0
  63. package/docs/architecture.md +2 -2
  64. package/docs/case-studies/_template.md +60 -0
  65. package/docs/catalog.md +24 -3
  66. package/docs/contracts/agent-user-schema.md +1 -0
  67. package/docs/contracts/command-clusters.md +2 -0
  68. package/docs/contracts/file-ownership-matrix.json +490 -0
  69. package/docs/contracts/ghostwriter-schema.md +337 -0
  70. package/docs/contracts/init-telemetry.md +133 -0
  71. package/docs/contracts/router-blending.md +71 -0
  72. package/docs/contracts/universal-skills.md +92 -0
  73. package/docs/contracts/write-engine.md +142 -0
  74. package/docs/getting-started-by-role.md +89 -0
  75. package/docs/getting-started-laravel.md +72 -0
  76. package/docs/getting-started.md +2 -2
  77. package/docs/safety.md +30 -0
  78. package/package.json +1 -1
  79. package/scripts/bench_runner.py +158 -0
  80. package/scripts/check_role_doc_links.py +110 -0
  81. package/scripts/compress.py +11 -0
  82. package/scripts/ghostwriter_fixture_allowlist.txt +16 -0
  83. package/scripts/install.py +133 -1
  84. package/scripts/lint_ghostwriter_source.py +240 -0
  85. package/scripts/measure_skill_reduction.py +102 -0
  86. package/scripts/schemas/rule.schema.json +5 -0
  87. package/scripts/schemas/skill.schema.json +6 -0
  88. package/scripts/update-github-metadata.sh +84 -0
@@ -0,0 +1,16 @@
1
+ # Ghostwriter fixture allowlist
2
+ #
3
+ # One file stem per line (without the .md extension).
4
+ # Every file under .agent-src.uncompressed/ghostwriter/ whose stem is
5
+ # NOT on this list will fail `task lint-ghostwriter-source`.
6
+ #
7
+ # Adding a new fixture requires:
8
+ # 1. Adding the stem here.
9
+ # 2. Setting `fictional: true` in the file's frontmatter.
10
+ # 3. Reviewer sign-off on the allowlist change.
11
+ #
12
+ # README.md is exempt (the lint skips it).
13
+ #
14
+ # See docs/contracts/ghostwriter-schema.md § Lint enforcement.
15
+
16
+ fictional-fixture-v1
@@ -3236,6 +3236,18 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
3236
3236
  "guard). See docs/installation.md → Minimal init."
3237
3237
  ),
3238
3238
  )
3239
+ parser.add_argument(
3240
+ "--interactive",
3241
+ action="store_true",
3242
+ help=(
3243
+ "after the install completes, run a short prompt to capture "
3244
+ "user-type / stack / verbosity and write `.agent-config.local.json` "
3245
+ "(forward-compatible stub for step-9 user-types axis — runtime "
3246
+ "skill filtering activates once that axis ships). TTY-only; "
3247
+ "no-op without an interactive stdin. See "
3248
+ "docs/contracts/universal-skills.md for the always-loaded set."
3249
+ ),
3250
+ )
3239
3251
  opts = parser.parse_args(argv)
3240
3252
  opts.tools = _merge_tools_aliases(opts.tools, opts.ai)
3241
3253
  if opts.scope == "global" and opts.custom_path:
@@ -3437,6 +3449,120 @@ def install_minimal(target_root: Path, force: bool) -> int:
3437
3449
  return 0
3438
3450
 
3439
3451
 
3452
+ # --- Interactive init (step-12 Phase 3, forward-compatible stub) ---
3453
+
3454
+ _INTERACTIVE_USER_TYPES: tuple[tuple[str, str], ...] = (
3455
+ ("creator", "Content / writing / publishing"),
3456
+ ("founder", "Early-stage company building"),
3457
+ ("consultant", "Advisory / strategy / discovery"),
3458
+ ("gtm", "Sales / marketing / revenue ops"),
3459
+ ("finance", "Finance / FP&A / unit economics"),
3460
+ ("ops", "Operations / incident / compliance"),
3461
+ ("developer", "Engineering / code-heavy work"),
3462
+ )
3463
+
3464
+ _INTERACTIVE_STACKS: tuple[tuple[str, str], ...] = (
3465
+ ("none", "No code project / pure content"),
3466
+ ("laravel", "PHP / Laravel"),
3467
+ ("nextjs", "TypeScript / Next.js / React"),
3468
+ ("python", "Python / FastAPI / Django"),
3469
+ ("symfony", "PHP / Symfony"),
3470
+ ("generic", "Other / mixed stack"),
3471
+ )
3472
+
3473
+ _INTERACTIVE_VERBOSITIES: tuple[tuple[str, str], ...] = (
3474
+ ("quiet", "Caveman / minimal output"),
3475
+ ("normal", "Default verbosity"),
3476
+ ("verbose", "Full intent announcements + play-by-play"),
3477
+ )
3478
+
3479
+ _LOCAL_CONFIG_FILE = ".agent-config.local.json"
3480
+
3481
+
3482
+ def _interactive_prompt_choice(label: str, options: tuple[tuple[str, str], ...]) -> str:
3483
+ """Render a numbered list and return the chosen id. Defaults to option 1 on empty input."""
3484
+ print()
3485
+ print(f" {label}")
3486
+ for idx, (key, blurb) in enumerate(options, start=1):
3487
+ print(f" {idx}. {key} — {blurb}")
3488
+ print()
3489
+ while True:
3490
+ try:
3491
+ raw = input(f" Choice [1-{len(options)}, default 1]: ").strip()
3492
+ except EOFError:
3493
+ return options[0][0]
3494
+ if not raw:
3495
+ return options[0][0]
3496
+ if raw.isdigit():
3497
+ i = int(raw)
3498
+ if 1 <= i <= len(options):
3499
+ return options[i - 1][0]
3500
+ # Allow typing the slug directly.
3501
+ for key, _ in options:
3502
+ if raw.lower() == key:
3503
+ return key
3504
+ print(f" ⚠️ Pick a number 1-{len(options)} or one of: {', '.join(k for k, _ in options)}.")
3505
+
3506
+
3507
+ def run_interactive_init(project_root: Path, force: bool) -> int:
3508
+ """Write ``.agent-config.local.json`` based on three TTY prompts.
3509
+
3510
+ Forward-compatible stub for [`step-9-user-types-axis`](../agents/roadmaps/step-9-user-types-axis.md):
3511
+ runtime skill filtering activates once that axis ships its
3512
+ ``user-types/`` directory and ``--user-type`` flag. Until then,
3513
+ this file is metadata-only — read by ``doctor --context`` and the
3514
+ upcoming ``agent-config skills`` listing command.
3515
+
3516
+ Universal-skills allowlist (see
3517
+ ``docs/contracts/universal-skills.md``) loads regardless of the
3518
+ captured ``user_type`` — the contract guarantees these 15 skills
3519
+ are never filtered out.
3520
+
3521
+ Returns 0 on success, 1 on collision without ``--force``. No-op
3522
+ (returns 0) when stdin is not a TTY.
3523
+ """
3524
+ if not sys.stdin.isatty():
3525
+ warn(
3526
+ "--interactive requested but stdin is not a TTY; skipping the "
3527
+ f"prompt. Re-run interactively or hand-edit {_LOCAL_CONFIG_FILE}."
3528
+ )
3529
+ return 0
3530
+
3531
+ target = project_root / _LOCAL_CONFIG_FILE
3532
+ if target.exists() and not force:
3533
+ warn(
3534
+ f"{_LOCAL_CONFIG_FILE} already exists; re-run with --force to "
3535
+ "overwrite. Skipping interactive init."
3536
+ )
3537
+ return 0
3538
+
3539
+ print()
3540
+ info("Interactive init — captures user-type / stack / verbosity")
3541
+ info("(forward-compatible stub; runtime filtering activates with step-9)")
3542
+
3543
+ user_type = _interactive_prompt_choice("Primary user type:", _INTERACTIVE_USER_TYPES)
3544
+ stack = _interactive_prompt_choice("Project stack:", _INTERACTIVE_STACKS)
3545
+ verbosity = _interactive_prompt_choice("Verbosity profile:", _INTERACTIVE_VERBOSITIES)
3546
+
3547
+ payload: dict[str, Any] = {
3548
+ "$schema": "https://github.com/event4u-app/agent-config/scripts/schemas/local-config.schema.json",
3549
+ "version": 1,
3550
+ "user_type": user_type,
3551
+ "stack": stack,
3552
+ "verbosity": verbosity,
3553
+ "universal_skills_contract": "docs/contracts/universal-skills.md",
3554
+ }
3555
+
3556
+ try:
3557
+ target.write_text(json.dumps(payload, indent=2, sort_keys=False) + "\n", encoding="utf-8")
3558
+ except OSError as exc:
3559
+ warn(f"Could not write {target}: {exc}")
3560
+ return 1
3561
+
3562
+ success(f"Wrote {target.relative_to(project_root)} ({user_type} / {stack} / {verbosity})")
3563
+ return 0
3564
+
3565
+
3440
3566
  # --- Main ---
3441
3567
 
3442
3568
  def main(argv: list[str]) -> int:
@@ -3503,7 +3629,13 @@ def main(argv: list[str]) -> int:
3503
3629
 
3504
3630
  project_root = custom_path or Path(opts.project or os.environ.get("PROJECT_ROOT") or os.getcwd()).resolve()
3505
3631
  is_first_run = not (project_root / SETTINGS_FILE).exists()
3506
- return _main_project_install(opts, project_root, parsed_tools, is_first_run)
3632
+ rc = _main_project_install(opts, project_root, parsed_tools, is_first_run)
3633
+ # Interactive post-install prompt (step-12 Phase 3, forward-compatible
3634
+ # stub). Runs only after a successful install so the local config
3635
+ # never ships ahead of the bridge files it parameterizes.
3636
+ if rc == 0 and getattr(opts, "interactive", False):
3637
+ run_interactive_init(project_root, opts.force)
3638
+ return rc
3507
3639
  except ConflictAbort as exc:
3508
3640
  warn(exc.message)
3509
3641
  return 1
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env python3
2
+ """Lint ghostwriter profile sources.
3
+
4
+ Two storage tiers exist (see docs/contracts/ghostwriter-schema.md):
5
+
6
+ * .agent-src.uncompressed/ghostwriter/ — package source. Ships
7
+ fictional fixtures ONLY (`fictional: true`). Every file stem must
8
+ be on scripts/ghostwriter_fixture_allowlist.txt. `aliases:` is
9
+ forbidden here (consumer-only feature).
10
+ * agents/ghostwriter/ — consumer real-person
11
+ profiles. Gitignored. Must NOT carry `fictional: true`. Optional
12
+ `aliases:` list validated per § Aliases storage rules.
13
+
14
+ This lint enforces both rules and runs in `task ci`.
15
+
16
+ Exit codes:
17
+ 0 all profiles compliant
18
+ 1 one or more violations
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import sys
23
+ import unicodedata
24
+ from pathlib import Path
25
+
26
+ import yaml
27
+
28
+ QUIET = "--quiet" in sys.argv
29
+
30
+ REPO = Path(__file__).resolve().parents[1]
31
+ PACKAGE_DIR = REPO / ".agent-src.uncompressed" / "ghostwriter"
32
+ CONSUMER_DIR = REPO / "agents" / "ghostwriter"
33
+ ALLOWLIST = REPO / "scripts" / "ghostwriter_fixture_allowlist.txt"
34
+ EXEMPT_STEMS = frozenset({"README"})
35
+
36
+ ALIAS_MIN_LEN = 2
37
+ # Allowed Unicode blocks for aliases (Latin-only, no homoglyph scripts).
38
+ # Basic Latin + Latin-1 Supplement + Latin Extended-A/B cover Müller,
39
+ # Łukaszewicz, José, etc., while rejecting Cyrillic / Greek confusables.
40
+ ALLOWED_PUNCT = frozenset(" .'-")
41
+
42
+
43
+ def load_allowlist() -> set[str]:
44
+ if not ALLOWLIST.exists():
45
+ return set()
46
+ stems: set[str] = set()
47
+ for line in ALLOWLIST.read_text(encoding="utf-8").splitlines():
48
+ s = line.strip()
49
+ if not s or s.startswith("#"):
50
+ continue
51
+ stems.add(s)
52
+ return stems
53
+
54
+
55
+ def parse_frontmatter(text: str) -> dict | None:
56
+ if not text.startswith("---\n"):
57
+ return None
58
+ end = text.find("\n---\n", 4)
59
+ if end == -1:
60
+ return None
61
+ try:
62
+ data = yaml.safe_load(text[4:end])
63
+ except yaml.YAMLError:
64
+ return None
65
+ return data if isinstance(data, dict) else None
66
+
67
+
68
+ def is_latin_or_allowed(ch: str) -> bool:
69
+ if ch in ALLOWED_PUNCT:
70
+ return True
71
+ if ch.isdigit():
72
+ return True
73
+ code = ord(ch)
74
+ # Basic Latin letters + Latin-1 Supplement letters + Latin Extended-A/B
75
+ if 0x0041 <= code <= 0x024F:
76
+ try:
77
+ return unicodedata.name(ch).startswith("LATIN ")
78
+ except ValueError:
79
+ return False
80
+ return False
81
+
82
+
83
+ def validate_alias(alias: str) -> str | None:
84
+ """Return an error message, or None if the alias is valid."""
85
+ if not isinstance(alias, str):
86
+ return f"alias must be a string, got {type(alias).__name__}"
87
+ if len(alias) < ALIAS_MIN_LEN:
88
+ return f"alias {alias!r} is shorter than {ALIAS_MIN_LEN} characters"
89
+ normalised = unicodedata.normalize("NFC", alias)
90
+ if normalised != alias:
91
+ return f"alias {alias!r} is not Unicode-NFC-normalised"
92
+ bad = [ch for ch in alias if not is_latin_or_allowed(ch)]
93
+ if bad:
94
+ return (
95
+ f"alias {alias!r} contains non-Latin or homoglyph-prone "
96
+ f"character(s): {bad!r}"
97
+ )
98
+ return None
99
+
100
+
101
+ def lint_package_side(allowlist: set[str]) -> list[str]:
102
+ errors: list[str] = []
103
+ if not PACKAGE_DIR.exists():
104
+ return errors
105
+ for path in sorted(PACKAGE_DIR.glob("*.md")):
106
+ stem = path.stem
107
+ if stem in EXEMPT_STEMS:
108
+ continue
109
+ if stem not in allowlist:
110
+ errors.append(
111
+ f" off-allowlist (package source): {path.relative_to(REPO)} "
112
+ f"— add '{stem}' to scripts/ghostwriter_fixture_allowlist.txt"
113
+ )
114
+ continue
115
+ data = parse_frontmatter(path.read_text(encoding="utf-8"))
116
+ if data is None:
117
+ errors.append(
118
+ f" unparsable frontmatter (package source): {path.relative_to(REPO)}"
119
+ )
120
+ continue
121
+ if data.get("fictional") is not True:
122
+ errors.append(
123
+ f" missing 'fictional: true' (package source): {path.relative_to(REPO)} "
124
+ f"(got fictional={data.get('fictional')!r})"
125
+ )
126
+ if "aliases" in data:
127
+ errors.append(
128
+ f" 'aliases:' forbidden on fictional fixtures: {path.relative_to(REPO)} "
129
+ f"— aliases are a consumer-only feature (see schema § Aliases)"
130
+ )
131
+ return errors
132
+
133
+
134
+ def lint_consumer_side() -> list[str]:
135
+ errors: list[str] = []
136
+ if not CONSUMER_DIR.exists():
137
+ return errors
138
+ # Collect (alias_ci, source_path, source_kind) tuples for cross-profile
139
+ # uniqueness check. source_kind is "alias" or "slug".
140
+ seen: dict[str, tuple[Path, str, str]] = {}
141
+ for path in sorted(CONSUMER_DIR.glob("*.md")):
142
+ if path.stem in EXEMPT_STEMS:
143
+ continue
144
+ slug = path.stem
145
+ slug_ci = slug.casefold()
146
+ # Register slug for cross-profile collision detection.
147
+ if slug_ci in seen:
148
+ prev_path, prev_value, prev_kind = seen[slug_ci]
149
+ errors.append(
150
+ f" duplicate slug across profiles: {path.relative_to(REPO)} "
151
+ f"vs {prev_path.relative_to(REPO)} (case-insensitive)"
152
+ )
153
+ else:
154
+ seen[slug_ci] = (path, slug, "slug")
155
+
156
+ data = parse_frontmatter(path.read_text(encoding="utf-8"))
157
+ if data is None:
158
+ continue
159
+ if data.get("fictional") is True:
160
+ errors.append(
161
+ f" 'fictional: true' in consumer tree: {path.relative_to(REPO)} "
162
+ f"— fictional fixtures belong in .agent-src.uncompressed/ghostwriter/"
163
+ )
164
+
165
+ aliases = data.get("aliases")
166
+ if aliases is None:
167
+ continue
168
+ if not isinstance(aliases, list):
169
+ errors.append(
170
+ f" 'aliases' must be a YAML list: {path.relative_to(REPO)} "
171
+ f"(got {type(aliases).__name__})"
172
+ )
173
+ continue
174
+
175
+ within_profile: set[str] = set()
176
+ for alias in aliases:
177
+ err = validate_alias(alias)
178
+ if err:
179
+ errors.append(f" {path.relative_to(REPO)}: {err}")
180
+ continue
181
+ alias_ci = alias.casefold()
182
+ if alias_ci in within_profile:
183
+ errors.append(
184
+ f" {path.relative_to(REPO)}: duplicate alias "
185
+ f"{alias!r} within the same profile (case-insensitive)"
186
+ )
187
+ continue
188
+ within_profile.add(alias_ci)
189
+ if alias_ci in seen:
190
+ prev_path, prev_value, prev_kind = seen[alias_ci]
191
+ errors.append(
192
+ f" alias collision: {path.relative_to(REPO)} alias "
193
+ f"{alias!r} collides with {prev_kind} {prev_value!r} in "
194
+ f"{prev_path.relative_to(REPO)} (case-insensitive)"
195
+ )
196
+ continue
197
+ seen[alias_ci] = (path, alias, "alias")
198
+ return errors
199
+
200
+
201
+ def main() -> int:
202
+ allowlist = load_allowlist()
203
+ pkg_errors = lint_package_side(allowlist)
204
+ cons_errors = lint_consumer_side()
205
+ errors = pkg_errors + cons_errors
206
+
207
+ if errors:
208
+ print(
209
+ f"❌ lint_ghostwriter_source: {len(errors)} violation(s)",
210
+ file=sys.stderr,
211
+ )
212
+ for line in errors:
213
+ print(line, file=sys.stderr)
214
+ print(
215
+ " see docs/contracts/ghostwriter-schema.md § Lint enforcement",
216
+ file=sys.stderr,
217
+ )
218
+ return 1
219
+
220
+ if not QUIET:
221
+ pkg_count = (
222
+ sum(1 for p in PACKAGE_DIR.glob("*.md") if p.stem not in EXEMPT_STEMS)
223
+ if PACKAGE_DIR.exists()
224
+ else 0
225
+ )
226
+ cons_count = (
227
+ sum(1 for p in CONSUMER_DIR.glob("*.md") if p.stem not in EXEMPT_STEMS)
228
+ if CONSUMER_DIR.exists()
229
+ else 0
230
+ )
231
+ print(
232
+ f"✅ lint_ghostwriter_source: {pkg_count} package fixture(s), "
233
+ f"{cons_count} consumer profile(s), all compliant"
234
+ )
235
+ return 0
236
+
237
+
238
+ if __name__ == "__main__":
239
+ raise SystemExit(main())
240
+
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env python3
2
+ """Skill-count reduction measurement — step-12 Phase 3 L74 deliverable.
3
+
4
+ Computes the skill-count reduction achieved by filtering on
5
+ `recommended_for_user_types` frontmatter tags. Each non-developer
6
+ user-type that lands ≥40% under the default-loaded skill count
7
+ satisfies the Phase 3 acceptance criterion.
8
+
9
+ The runtime filter (loaded vs. registered) ships with step-9; this
10
+ script measures the data already in place, so the box can close on
11
+ the basis of the underlying tagging being correct.
12
+
13
+ Usage:
14
+ python3 scripts/measure_skill_reduction.py
15
+ python3 scripts/measure_skill_reduction.py --json
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import json
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ try:
26
+ import yaml
27
+ except ImportError:
28
+ sys.stderr.write("error: PyYAML required\n")
29
+ sys.exit(2)
30
+
31
+ REPO_ROOT = Path(__file__).resolve().parent.parent
32
+ SKILLS_DIR = REPO_ROOT / ".agent-src.uncompressed" / "skills"
33
+ TARGET_REDUCTION = 0.40
34
+ PHASE_3_USER_TYPES = ("consultant", "creator")
35
+
36
+
37
+ def load_tags() -> tuple[int, dict[str, int]]:
38
+ total = 0
39
+ per_type: dict[str, int] = {}
40
+ for skill_dir in sorted(SKILLS_DIR.iterdir()):
41
+ skill_md = skill_dir / "SKILL.md"
42
+ if not skill_md.is_file():
43
+ continue
44
+ text = skill_md.read_text(encoding="utf-8")
45
+ if not text.startswith("---"):
46
+ continue
47
+ try:
48
+ fm = yaml.safe_load(text.split("---", 2)[1]) or {}
49
+ except yaml.YAMLError:
50
+ continue
51
+ total += 1
52
+ for t in fm.get("recommended_for_user_types") or []:
53
+ per_type[t] = per_type.get(t, 0) + 1
54
+ return total, per_type
55
+
56
+
57
+ def main(argv=None) -> int:
58
+ ap = argparse.ArgumentParser()
59
+ ap.add_argument("--json", action="store_true")
60
+ args = ap.parse_args(argv)
61
+
62
+ total, per_type = load_tags()
63
+ if total == 0:
64
+ sys.stderr.write("error: no skills found\n")
65
+ return 2
66
+
67
+ report = {
68
+ "total_skills": total,
69
+ "target_reduction": TARGET_REDUCTION,
70
+ "per_user_type": {},
71
+ "phase_3_user_types": list(PHASE_3_USER_TYPES),
72
+ "phase_3_passed": True,
73
+ }
74
+ for ut in sorted(per_type):
75
+ loaded = per_type[ut]
76
+ reduction = 1 - (loaded / total)
77
+ report["per_user_type"][ut] = {
78
+ "loaded_skills": loaded,
79
+ "reduction_pct": round(reduction, 4),
80
+ "passes_target": reduction >= TARGET_REDUCTION,
81
+ }
82
+ for ut in PHASE_3_USER_TYPES:
83
+ entry = report["per_user_type"].get(ut)
84
+ if not entry or not entry["passes_target"]:
85
+ report["phase_3_passed"] = False
86
+
87
+ if args.json:
88
+ print(json.dumps(report, indent=2))
89
+ else:
90
+ print(f"total_skills: {total} target_reduction: ≥{TARGET_REDUCTION:.0%}")
91
+ for ut, e in report["per_user_type"].items():
92
+ mark = "✓" if e["passes_target"] else "✗"
93
+ star = " *" if ut in PHASE_3_USER_TYPES else ""
94
+ print(f" {mark} {ut:12s} loaded={e['loaded_skills']:3d} "
95
+ f"reduction={e['reduction_pct']:.1%}{star}")
96
+ print(f"verdict: {'PASS' if report['phase_3_passed'] else 'FAIL'}")
97
+ print("(* = step-12 Phase 3 L74 anchor user-types)")
98
+ return 0 if report["phase_3_passed"] else 1
99
+
100
+
101
+ if __name__ == "__main__":
102
+ sys.exit(main())
@@ -75,6 +75,11 @@
75
75
  "items": {"type": "string", "pattern": "^(skill|guideline|command|contract):"},
76
76
  "description": "Router targets (skill / guideline / command / contract). Forbidden on kernel rules. Schema: docs/contracts/rule-router.md."
77
77
  },
78
+ "applies_to_user_types": {
79
+ "type": "array",
80
+ "items": {"type": "string"},
81
+ "description": "Forward-compatible user-type filter (step-12-universal-os-reframe Phase 4). Rule loads only when the active user-type matches one of these tags. Wired by step-9-user-types-axis once it lands; treated as a no-op gate until then. Free-form tags (e.g. 'support', 'finance', 'recruiting', 'marketing', 'legal-drafting', 'consulting', 'medical-drafting', 'finance-drafting', 'ops', 'analytics-export', 'all') — no enum until the user-types axis closes."
82
+ },
78
83
  "profile": {
79
84
  "type": "string",
80
85
  "enum": ["minimal", "balanced", "full"],
@@ -80,6 +80,12 @@
80
80
  },
81
81
  "description": "Senior-skill opt-in for the context spine. Declares which slots under agents/context-spine/ the skill expects to read. Cross-wing slots (product, team, repo) are locked at 3 by council Q1 (KEEP-3); wing-scoped slots follow the per-wing ADR track in docs/contracts/context-spine.md § 5. Wing-3 (channel-stage, funnel-stage, customer-segment) authorized by docs/contracts/adr-gtm-context-spine.md; Wing-4 (fiscal-period, org-stage, regulatory-regime) authorized by docs/contracts/adr-wing4-context-spine.md."
82
82
  },
83
+ "recommended_for_user_types": {
84
+ "type": "array",
85
+ "uniqueItems": true,
86
+ "items": {"type": "string"},
87
+ "description": "Forward-compatible user-type recommendation tags (step-12-universal-os-reframe Phase 5). Skill loads for every user-type whose slug appears in this list. Absence of the key marks the skill as universal — see docs/contracts/universal-skills.md for the always-loaded floor and docs/contracts/router-blending.md for per-user-type mix ratios. Wired by step-9-user-types-axis once it lands; treated as metadata until then. Free-form tags (e.g. 'creator', 'founder', 'consultant', 'gtm', 'finance', 'ops', 'developer') — no enum until the user-types axis closes."
88
+ },
83
89
  "execution": {
84
90
  "type": "object",
85
91
  "additionalProperties": false,
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/update-github-metadata.sh
3
+ #
4
+ # Step-12 Phase 6 L113 — apply the Universal-OS reframe to GitHub repo
5
+ # metadata (description + topics). Drafted per AI-Council verdict
6
+ # 2026-05-15-step12-final-push (Decision 3 AMEND: reviewable script,
7
+ # explicit maintainer approval to execute).
8
+ #
9
+ # Iron Law (.augment/rules/non-destructive-by-default.md):
10
+ # GitHub repo description/topics are PUBLIC project metadata.
11
+ # This script does NOT auto-run. Maintainer must invoke it explicitly.
12
+ #
13
+ # Usage:
14
+ # ./scripts/update-github-metadata.sh # dry-run (prints curl payload)
15
+ # ./scripts/update-github-metadata.sh --apply # actually call the API
16
+ #
17
+ # Rollback:
18
+ # gh api repos/event4u-app/agent-config --method PATCH \
19
+ # -f description="agent-config — Behavior, Memory and Delivery Governance for AI Agents"
20
+ # (Topics: re-PUT the original list from `gh api repos/event4u-app/agent-config | jq .topics`.)
21
+ set -euo pipefail
22
+
23
+ OWNER_REPO="event4u-app/agent-config"
24
+
25
+ NEW_DESCRIPTION="Universal AI Agent OS — governed skills, rules, commands for developers, founders, creators, GTM, finance/ops"
26
+
27
+ # Existing topics preserved; reframe topics appended.
28
+ TOPICS=(
29
+ "agent-rules"
30
+ "agent-skills"
31
+ "agentic-ai"
32
+ "agentskills-standard"
33
+ "ai-coding"
34
+ "augment-agent"
35
+ "claude-code"
36
+ "copilot"
37
+ "devcontainer"
38
+ "governance"
39
+ "laravel"
40
+ "php"
41
+ "react"
42
+ "symfony"
43
+ "universal-ai-os"
44
+ "ai-governance"
45
+ "non-developer-tools"
46
+ )
47
+
48
+ APPLY="${1:-}"
49
+
50
+ if [[ "${APPLY}" != "--apply" ]]; then
51
+ echo "=== DRY RUN — no API call ==="
52
+ echo "Target repo: ${OWNER_REPO}"
53
+ echo "New description: ${NEW_DESCRIPTION}"
54
+ echo "New topics:"
55
+ printf ' - %s\n' "${TOPICS[@]}"
56
+ echo
57
+ echo "To apply, re-run with --apply (requires gh authenticated as repo admin)."
58
+ exit 0
59
+ fi
60
+
61
+ # Apply path — requires gh CLI authenticated.
62
+ if ! command -v gh >/dev/null 2>&1; then
63
+ echo "error: gh CLI not found. Install from https://cli.github.com." >&2
64
+ exit 1
65
+ fi
66
+
67
+ echo "Applying description …"
68
+ gh api "repos/${OWNER_REPO}" \
69
+ --method PATCH \
70
+ -f "description=${NEW_DESCRIPTION}" \
71
+ --silent
72
+
73
+ echo "Applying topics …"
74
+ TOPIC_ARGS=()
75
+ for t in "${TOPICS[@]}"; do
76
+ TOPIC_ARGS+=(-f "names[]=${t}")
77
+ done
78
+ gh api "repos/${OWNER_REPO}/topics" \
79
+ --method PUT \
80
+ -H "Accept: application/vnd.github.mercy-preview+json" \
81
+ "${TOPIC_ARGS[@]}" \
82
+ --silent
83
+
84
+ echo "Done. Verify at https://github.com/${OWNER_REPO}"