@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.
- package/.agent-src/commands/ghostwriter/delete.md +118 -0
- package/.agent-src/commands/ghostwriter/fetch.md +185 -0
- package/.agent-src/commands/ghostwriter/list.md +102 -0
- package/.agent-src/commands/ghostwriter/show.md +113 -0
- package/.agent-src/commands/ghostwriter/write.md +160 -0
- package/.agent-src/commands/ghostwriter.md +96 -0
- package/.agent-src/commands/post-as/ghostwriter.md +66 -0
- package/.agent-src/commands/post-as/me.md +124 -0
- package/.agent-src/commands/post-as.md +58 -0
- package/.agent-src/ghostwriter/README.md +61 -0
- package/.agent-src/ghostwriter/fictional-fixture-v1.md +94 -0
- package/.agent-src/personas/README.md +8 -0
- package/.agent-src/rules/domain-safety-disclaimer-consulting.md +52 -0
- package/.agent-src/rules/domain-safety-disclaimer-financial.md +54 -0
- package/.agent-src/rules/domain-safety-disclaimer-legal.md +49 -0
- package/.agent-src/rules/domain-safety-disclaimer-medical.md +56 -0
- package/.agent-src/rules/domain-safety-export-redact.md +65 -0
- package/.agent-src/rules/domain-safety-logging-pii-floor.md +55 -0
- package/.agent-src/rules/domain-safety-pii-finance.md +57 -0
- package/.agent-src/rules/domain-safety-pii-marketing.md +60 -0
- package/.agent-src/rules/domain-safety-pii-recruiting.md +56 -0
- package/.agent-src/rules/domain-safety-pii-support.md +57 -0
- package/.agent-src/rules/domain-safety-retention-finance.md +48 -0
- package/.agent-src/rules/domain-safety-retention-support.md +55 -0
- package/.agent-src/skills/api-design/SKILL.md +3 -0
- package/.agent-src/skills/authz-review/SKILL.md +3 -0
- package/.agent-src/skills/competitive-moat-analysis/SKILL.md +3 -0
- package/.agent-src/skills/competitive-positioning/SKILL.md +3 -0
- package/.agent-src/skills/content-funnel-design/SKILL.md +3 -0
- package/.agent-src/skills/contracts-cognition/SKILL.md +3 -0
- package/.agent-src/skills/dashboard-design/SKILL.md +3 -0
- package/.agent-src/skills/data-handling-judgment/SKILL.md +3 -0
- package/.agent-src/skills/dcf-modeling/SKILL.md +3 -0
- package/.agent-src/skills/deal-qualification-meddic/SKILL.md +3 -0
- package/.agent-src/skills/discovery-interview/SKILL.md +3 -0
- package/.agent-src/skills/editorial-calendar/SKILL.md +3 -0
- package/.agent-src/skills/forecast-accuracy/SKILL.md +3 -0
- package/.agent-src/skills/forecasting/SKILL.md +3 -0
- package/.agent-src/skills/fundraising-narrative/SKILL.md +3 -0
- package/.agent-src/skills/gtm-launch/SKILL.md +3 -0
- package/.agent-src/skills/incident-commander/SKILL.md +3 -0
- package/.agent-src/skills/launch-readiness/SKILL.md +3 -0
- package/.agent-src/skills/messaging-architecture/SKILL.md +3 -0
- package/.agent-src/skills/okr-tree-modeling/SKILL.md +3 -0
- package/.agent-src/skills/pipeline-strategy/SKILL.md +3 -0
- package/.agent-src/skills/playwright-architect/SKILL.md +3 -0
- package/.agent-src/skills/privacy-review/SKILL.md +4 -1
- package/.agent-src/skills/quality-tools/SKILL.md +3 -0
- package/.agent-src/skills/release-comms/SKILL.md +3 -0
- package/.agent-src/skills/runway-cognition/SKILL.md +3 -0
- package/.agent-src/skills/scenario-modeling/SKILL.md +3 -0
- package/.agent-src/skills/secrets-management/SKILL.md +3 -0
- package/.agent-src/skills/tech-debt-tracker/SKILL.md +3 -0
- package/.agent-src/skills/unit-economics-modeling/SKILL.md +3 -0
- package/.agent-src/skills/voc-extract/SKILL.md +3 -0
- package/.agent-src/skills/voice-and-tone-design/SKILL.md +3 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +16 -1
- package/.claude-plugin/marketplace.json +10 -1
- package/CHANGELOG.md +69 -0
- package/README.md +44 -23
- package/config/gitignore-block.txt +8 -0
- package/docs/announcements/2026-05-non-dev-launch.md +79 -0
- package/docs/architecture.md +2 -2
- package/docs/case-studies/_template.md +60 -0
- package/docs/catalog.md +24 -3
- package/docs/contracts/agent-user-schema.md +1 -0
- package/docs/contracts/command-clusters.md +2 -0
- package/docs/contracts/file-ownership-matrix.json +490 -0
- package/docs/contracts/ghostwriter-schema.md +337 -0
- package/docs/contracts/init-telemetry.md +133 -0
- package/docs/contracts/router-blending.md +71 -0
- package/docs/contracts/universal-skills.md +92 -0
- package/docs/contracts/write-engine.md +142 -0
- package/docs/getting-started-by-role.md +89 -0
- package/docs/getting-started-laravel.md +72 -0
- package/docs/getting-started.md +2 -2
- package/docs/safety.md +30 -0
- package/package.json +1 -1
- package/scripts/bench_runner.py +158 -0
- package/scripts/check_role_doc_links.py +110 -0
- package/scripts/compress.py +11 -0
- package/scripts/ghostwriter_fixture_allowlist.txt +16 -0
- package/scripts/install.py +133 -1
- package/scripts/lint_ghostwriter_source.py +240 -0
- package/scripts/measure_skill_reduction.py +102 -0
- package/scripts/schemas/rule.schema.json +5 -0
- package/scripts/schemas/skill.schema.json +6 -0
- 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
|
package/scripts/install.py
CHANGED
|
@@ -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
|
-
|
|
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}"
|