@event4u/agent-config 2.16.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +98 -0
- package/README.md +44 -23
- package/config/agent-settings.template.yml +7 -0
- 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 +25 -4
- package/docs/contracts/adr-install-user-type-axis.md +107 -0
- 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 +132 -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/audit_user_type_axis.py +140 -0
- 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 +9 -1
- package/scripts/install.py +214 -8
- package/scripts/install.sh +7 -0
- package/scripts/lint_ghostwriter_source.py +240 -0
- package/scripts/mcp_server/prompts.py +134 -2
- 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/schemas/user-type-axis.schema.json +56 -0
- package/scripts/sync_agent_settings.py +6 -0
- package/scripts/update-github-metadata.sh +84 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Verify every skill link in role-based docs resolves to a real file.
|
|
3
|
+
|
|
4
|
+
Part of step-12 Phase 2. Runs in `task ci` to catch link rot when a
|
|
5
|
+
skill is renamed or removed but the role docs still reference it.
|
|
6
|
+
|
|
7
|
+
Scans `docs/getting-started-by-role.md` and `docs/getting-started-laravel.md`
|
|
8
|
+
for markdown links of the form `../.agent-src/skills/<name>/SKILL.md`
|
|
9
|
+
(relative to docs/) and checks that the target file exists on disk.
|
|
10
|
+
|
|
11
|
+
Exit codes:
|
|
12
|
+
0 — every link resolves
|
|
13
|
+
1 — at least one broken link; prints the offending file:line:url tuples
|
|
14
|
+
2 — usage error (one of the role doc files missing)
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python3 scripts/check_role_doc_links.py
|
|
18
|
+
python3 scripts/check_role_doc_links.py --quiet
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
28
|
+
DOCS_DIR = ROOT / "docs"
|
|
29
|
+
|
|
30
|
+
# (display-path, on-disk path, link-anchor) — anchor is the relative
|
|
31
|
+
# prefix that identifies a skill link from inside docs/.
|
|
32
|
+
ROLE_DOCS = [
|
|
33
|
+
DOCS_DIR / "getting-started-by-role.md",
|
|
34
|
+
DOCS_DIR / "getting-started-laravel.md",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# Markdown link: [label](path). We only check the (path) part. The
|
|
38
|
+
# regex tolerates trailing #anchor fragments and ignores absolute URLs.
|
|
39
|
+
LINK_RE = re.compile(r"\]\(([^)\s]+)\)")
|
|
40
|
+
|
|
41
|
+
# Anchors we know how to resolve. Each tuple is (prefix, base_dir).
|
|
42
|
+
ANCHORS: list[tuple[str, Path]] = [
|
|
43
|
+
("../.agent-src/skills/", ROOT / ".agent-src" / "skills"),
|
|
44
|
+
("../.agent-src/commands/", ROOT / ".agent-src" / "commands"),
|
|
45
|
+
("../.agent-src/rules/", ROOT / ".agent-src" / "rules"),
|
|
46
|
+
("../agents/", ROOT / "agents"),
|
|
47
|
+
("contracts/", DOCS_DIR / "contracts"),
|
|
48
|
+
("guidelines/", DOCS_DIR / "guidelines"),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve(url: str, doc_path: Path) -> Path | None:
|
|
53
|
+
"""Return the on-disk target path for a relative link, or None if external."""
|
|
54
|
+
if url.startswith(("http://", "https://", "mailto:")):
|
|
55
|
+
return None
|
|
56
|
+
bare = url.split("#", 1)[0]
|
|
57
|
+
if not bare:
|
|
58
|
+
return None
|
|
59
|
+
# Relative to the doc's own directory.
|
|
60
|
+
target = (doc_path.parent / bare).resolve()
|
|
61
|
+
return target
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def scan(doc_path: Path) -> list[tuple[int, str]]:
|
|
65
|
+
"""Return list of (line_no, url) tuples for every non-external link."""
|
|
66
|
+
if not doc_path.is_file():
|
|
67
|
+
print(f"error: missing role doc: {doc_path}", file=sys.stderr)
|
|
68
|
+
sys.exit(2)
|
|
69
|
+
links: list[tuple[int, str]] = []
|
|
70
|
+
for i, line in enumerate(doc_path.read_text(encoding="utf-8").splitlines(), 1):
|
|
71
|
+
for m in LINK_RE.finditer(line):
|
|
72
|
+
url = m.group(1)
|
|
73
|
+
if url.startswith(("http://", "https://", "mailto:")):
|
|
74
|
+
continue
|
|
75
|
+
links.append((i, url))
|
|
76
|
+
return links
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main() -> int:
|
|
80
|
+
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
81
|
+
p.add_argument("--quiet", action="store_true", help="Suppress success summary.")
|
|
82
|
+
args = p.parse_args()
|
|
83
|
+
|
|
84
|
+
failures: list[tuple[Path, int, str]] = []
|
|
85
|
+
checked = 0
|
|
86
|
+
|
|
87
|
+
for doc in ROLE_DOCS:
|
|
88
|
+
for line_no, url in scan(doc):
|
|
89
|
+
target = resolve(url, doc)
|
|
90
|
+
if target is None:
|
|
91
|
+
continue
|
|
92
|
+
checked += 1
|
|
93
|
+
if not target.exists():
|
|
94
|
+
failures.append((doc, line_no, url))
|
|
95
|
+
|
|
96
|
+
if failures:
|
|
97
|
+
print("Broken links in role docs:", file=sys.stderr)
|
|
98
|
+
for doc, line_no, url in failures:
|
|
99
|
+
rel = doc.relative_to(ROOT)
|
|
100
|
+
print(f" {rel}:{line_no} -> {url}", file=sys.stderr)
|
|
101
|
+
print(f"\n{len(failures)} broken / {checked} checked", file=sys.stderr)
|
|
102
|
+
return 1
|
|
103
|
+
|
|
104
|
+
if not args.quiet:
|
|
105
|
+
print(f"check_role_doc_links: {checked} links OK across {len(ROLE_DOCS)} files")
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
sys.exit(main())
|
package/scripts/compress.py
CHANGED
|
@@ -77,6 +77,11 @@ def _tool_active(tool_id: str) -> bool:
|
|
|
77
77
|
# Files to copy as-is even if .md (not compressed by agent)
|
|
78
78
|
COPY_AS_IS = {"README.md"}
|
|
79
79
|
|
|
80
|
+
# Directories (relative to SOURCE_DIR) whose .md content is data, not prose,
|
|
81
|
+
# and must be copied verbatim. Ghostwriter fixtures carry voice_samples that
|
|
82
|
+
# would be destroyed by caveman compression.
|
|
83
|
+
COPY_AS_IS_DIRS = frozenset({"ghostwriter"})
|
|
84
|
+
|
|
80
85
|
|
|
81
86
|
def _read_augment_rules_use_symlinks() -> bool:
|
|
82
87
|
"""Read augment.rules_use_symlinks from .agent-settings.yml.
|
|
@@ -235,6 +240,12 @@ def should_compress(filepath: Path) -> bool:
|
|
|
235
240
|
return False
|
|
236
241
|
if filepath.name in COPY_AS_IS:
|
|
237
242
|
return False
|
|
243
|
+
try:
|
|
244
|
+
rel_parts = filepath.relative_to(SOURCE_DIR).parts
|
|
245
|
+
except ValueError:
|
|
246
|
+
rel_parts = filepath.parts
|
|
247
|
+
if rel_parts and rel_parts[0] in COPY_AS_IS_DIRS:
|
|
248
|
+
return False
|
|
238
249
|
return True
|
|
239
250
|
|
|
240
251
|
|
|
@@ -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
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
# --source <dir> Package source directory (default: auto-detect)
|
|
16
16
|
# --target <dir> Target project root (default: cwd)
|
|
17
17
|
# --profile <name> Cost profile for bridges (minimal|balanced|full)
|
|
18
|
+
# --user-type <id> Primary user-type for skill filtering (step-9 axis).
|
|
19
|
+
# Valid ids: consultant | creator | developer | finance
|
|
20
|
+
# | founder | gtm | ops. Default: empty (no filter).
|
|
21
|
+
# Written to personal.user_type in .agent-settings.yml.
|
|
18
22
|
# --tools <list> Comma-separated tool IDs to install (default: all).
|
|
19
23
|
# Valid: claude-code,claude-desktop,cursor,windsurf,
|
|
20
24
|
# cline,gemini-cli,copilot,augment,aider,codex,
|
|
@@ -82,12 +86,13 @@ SCOPE=""
|
|
|
82
86
|
CUSTOM_PATH=""
|
|
83
87
|
OFFLINE=false
|
|
84
88
|
MINIMAL=false
|
|
89
|
+
USER_TYPE=""
|
|
85
90
|
|
|
86
91
|
# Single source of truth for valid tool IDs (also referenced by install.sh / install.py).
|
|
87
92
|
VALID_TOOLS="claude-code claude-desktop cursor windsurf cline gemini-cli copilot augment aider codex roocode continue kilocode zed jetbrains kiro all"
|
|
88
93
|
|
|
89
94
|
show_help() {
|
|
90
|
-
sed -n '3,
|
|
95
|
+
sed -n '3,58p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
list_tools() {
|
|
@@ -146,6 +151,8 @@ while [[ $# -gt 0 ]]; do
|
|
|
146
151
|
--target=*) TARGET_DIR="${1#*=}"; shift ;;
|
|
147
152
|
--profile) PROFILE="$2"; shift 2 ;;
|
|
148
153
|
--profile=*) PROFILE="${1#*=}"; shift ;;
|
|
154
|
+
--user-type) USER_TYPE="$2"; shift 2 ;;
|
|
155
|
+
--user-type=*) USER_TYPE="${1#*=}"; shift ;;
|
|
149
156
|
--tools) TOOLS="${TOOLS:+$TOOLS,}$2"; TOOLS_EXPLICIT=true; shift 2 ;;
|
|
150
157
|
--tools=*) TOOLS="${TOOLS:+$TOOLS,}${1#*=}"; TOOLS_EXPLICIT=true; shift ;;
|
|
151
158
|
--ai) TOOLS="${TOOLS:+$TOOLS,}$2"; TOOLS_EXPLICIT=true; shift 2 ;;
|
|
@@ -309,6 +316,7 @@ run_bridges() {
|
|
|
309
316
|
|
|
310
317
|
local args=(--project "$TARGET_DIR" --package "$SOURCE_DIR")
|
|
311
318
|
[[ -n "$PROFILE" ]] && args+=(--profile="$PROFILE")
|
|
319
|
+
[[ -n "$USER_TYPE" ]] && args+=(--user-type="$USER_TYPE")
|
|
312
320
|
$FORCE && args+=(--force)
|
|
313
321
|
$QUIET && args+=(--quiet)
|
|
314
322
|
$GLOBAL && args+=(--global)
|
package/scripts/install.py
CHANGED
|
@@ -45,6 +45,8 @@ except ImportError: # pragma: no cover — alt sys.path layout
|
|
|
45
45
|
DEFAULT_PROFILE = "balanced"
|
|
46
46
|
SUPPORTED_PROFILES = ("minimal", "balanced", "full")
|
|
47
47
|
COST_PROFILE_PLACEHOLDER = "__COST_PROFILE__"
|
|
48
|
+
USER_TYPE_PLACEHOLDER = "__USER_TYPE__"
|
|
49
|
+
USER_TYPES_DIR = "user-types"
|
|
48
50
|
|
|
49
51
|
# Env-var equivalent of --force for CI / scripted installs (P3.4).
|
|
50
52
|
# When set to "1" the install run treats every conflict as
|
|
@@ -781,7 +783,44 @@ def _render_template(template: str, profile_values: "dict[str, str]") -> str:
|
|
|
781
783
|
return body
|
|
782
784
|
|
|
783
785
|
|
|
784
|
-
def
|
|
786
|
+
def _load_valid_user_types(package_root: Path) -> list[str]:
|
|
787
|
+
"""Return the sorted user-type slugs shipped under ``user-types/``.
|
|
788
|
+
|
|
789
|
+
Maps `user-types/<id>.yml` → `<id>`. The ``README.md`` is skipped.
|
|
790
|
+
Empty list when the directory is absent (older package payloads).
|
|
791
|
+
"""
|
|
792
|
+
directory = package_root / USER_TYPES_DIR
|
|
793
|
+
if not directory.is_dir():
|
|
794
|
+
return []
|
|
795
|
+
return sorted(p.stem for p in directory.glob("*.yml"))
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _validate_user_type(package_root: Path, value: str) -> str:
|
|
799
|
+
"""Return the validated user-type slug (empty string allowed → no filter)."""
|
|
800
|
+
cleaned = (value or "").strip()
|
|
801
|
+
if not cleaned:
|
|
802
|
+
return ""
|
|
803
|
+
valid = _load_valid_user_types(package_root)
|
|
804
|
+
if not valid:
|
|
805
|
+
fail(
|
|
806
|
+
f"--user-type={cleaned} requested but no user-types/*.yml present "
|
|
807
|
+
f"under {package_root}"
|
|
808
|
+
)
|
|
809
|
+
if cleaned not in valid:
|
|
810
|
+
fail(
|
|
811
|
+
f"Unknown --user-type={cleaned}. Valid: {', '.join(valid)} "
|
|
812
|
+
"(empty string disables the filter)."
|
|
813
|
+
)
|
|
814
|
+
return cleaned
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def ensure_agent_settings(
|
|
818
|
+
project_root: Path,
|
|
819
|
+
package_root: Path,
|
|
820
|
+
profile: str,
|
|
821
|
+
force: bool,
|
|
822
|
+
user_type: str = "",
|
|
823
|
+
) -> None:
|
|
785
824
|
target = project_root / SETTINGS_FILE
|
|
786
825
|
profile_source = package_root / "config" / "profiles" / f"{profile}.ini"
|
|
787
826
|
template_source = package_root / "config" / "agent-settings.template.yml"
|
|
@@ -794,12 +833,16 @@ def ensure_agent_settings(project_root: Path, package_root: Path, profile: str,
|
|
|
794
833
|
template = template_source.read_text(encoding="utf-8")
|
|
795
834
|
if COST_PROFILE_PLACEHOLDER not in template:
|
|
796
835
|
fail(f"Template is missing placeholder {COST_PROFILE_PLACEHOLDER}")
|
|
836
|
+
if USER_TYPE_PLACEHOLDER not in template:
|
|
837
|
+
fail(f"Template is missing placeholder {USER_TYPE_PLACEHOLDER}")
|
|
797
838
|
profile_values = _parse_profile_ini(profile_source)
|
|
798
839
|
if profile_values.get("cost_profile") != profile:
|
|
799
840
|
fail(
|
|
800
841
|
f"Profile preset {profile_source.name} has cost_profile="
|
|
801
842
|
f"{profile_values.get('cost_profile')!r} but --profile={profile}"
|
|
802
843
|
)
|
|
844
|
+
# Inject runtime-only values (not part of the .ini profile presets).
|
|
845
|
+
profile_values["user_type"] = _validate_user_type(package_root, user_type)
|
|
803
846
|
template_body = _render_template(template, profile_values)
|
|
804
847
|
|
|
805
848
|
legacy_target = project_root / LEGACY_SETTINGS_FILE
|
|
@@ -822,7 +865,9 @@ def ensure_agent_settings(project_root: Path, package_root: Path, profile: str,
|
|
|
822
865
|
return
|
|
823
866
|
|
|
824
867
|
write_file(target, template_body)
|
|
825
|
-
|
|
868
|
+
user_type_value = profile_values.get("user_type", "")
|
|
869
|
+
suffix = f", user_type={user_type_value}" if user_type_value else ""
|
|
870
|
+
success(f"{SETTINGS_FILE} created (cost_profile={profile}{suffix})")
|
|
826
871
|
|
|
827
872
|
|
|
828
873
|
def ensure_vscode_bridge(project_root: Path, package_type: str, force: bool) -> None:
|
|
@@ -3130,6 +3175,17 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
|
3130
3175
|
default=DEFAULT_PROFILE,
|
|
3131
3176
|
help=f"cost_profile value ({'|'.join(SUPPORTED_PROFILES)}, default: {DEFAULT_PROFILE})",
|
|
3132
3177
|
)
|
|
3178
|
+
parser.add_argument(
|
|
3179
|
+
"--user-type",
|
|
3180
|
+
dest="user_type",
|
|
3181
|
+
default="",
|
|
3182
|
+
help=(
|
|
3183
|
+
"primary user-type for skill filtering (step-9 axis). "
|
|
3184
|
+
"Valid ids: consultant | creator | developer | finance | "
|
|
3185
|
+
"founder | gtm | ops. Default: empty (no filter, every skill "
|
|
3186
|
+
"surfaces). Written to personal.user_type in .agent-settings.yml."
|
|
3187
|
+
),
|
|
3188
|
+
)
|
|
3133
3189
|
parser.add_argument("--force", action="store_true", help="overwrite existing files")
|
|
3134
3190
|
parser.add_argument("--skip-bridges", action="store_true", help="only create .agent-settings.yml")
|
|
3135
3191
|
parser.add_argument(
|
|
@@ -3236,6 +3292,18 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
|
3236
3292
|
"guard). See docs/installation.md → Minimal init."
|
|
3237
3293
|
),
|
|
3238
3294
|
)
|
|
3295
|
+
parser.add_argument(
|
|
3296
|
+
"--interactive",
|
|
3297
|
+
action="store_true",
|
|
3298
|
+
help=(
|
|
3299
|
+
"after the install completes, run a short prompt to capture "
|
|
3300
|
+
"user-type / stack / verbosity and write `.agent-config.local.json` "
|
|
3301
|
+
"(forward-compatible stub for step-9 user-types axis — runtime "
|
|
3302
|
+
"skill filtering activates once that axis ships). TTY-only; "
|
|
3303
|
+
"no-op without an interactive stdin. See "
|
|
3304
|
+
"docs/contracts/universal-skills.md for the always-loaded set."
|
|
3305
|
+
),
|
|
3306
|
+
)
|
|
3239
3307
|
opts = parser.parse_args(argv)
|
|
3240
3308
|
opts.tools = _merge_tools_aliases(opts.tools, opts.ai)
|
|
3241
3309
|
if opts.scope == "global" and opts.custom_path:
|
|
@@ -3342,7 +3410,7 @@ def _write_install_mode_marker(project_root: Path, mode: str) -> None:
|
|
|
3342
3410
|
pass
|
|
3343
3411
|
|
|
3344
3412
|
|
|
3345
|
-
def install_minimal(target_root: Path, force: bool) -> int:
|
|
3413
|
+
def install_minimal(target_root: Path, force: bool, user_type: str = "") -> int:
|
|
3346
3414
|
"""Bootstrap the project-local override layer only (D2-compliant).
|
|
3347
3415
|
|
|
3348
3416
|
Writes:
|
|
@@ -3409,8 +3477,16 @@ def install_minimal(target_root: Path, force: bool) -> int:
|
|
|
3409
3477
|
if settings_dst.exists() and not force:
|
|
3410
3478
|
skip(f"{SETTINGS_FILE} already exists (use --force to overwrite)")
|
|
3411
3479
|
else:
|
|
3412
|
-
|
|
3413
|
-
|
|
3480
|
+
body = settings_src.read_text(encoding="utf-8")
|
|
3481
|
+
if user_type:
|
|
3482
|
+
body = body.rstrip() + (
|
|
3483
|
+
"\n\n# --- Personal (step-9 user-type axis) ---\n"
|
|
3484
|
+
"personal:\n"
|
|
3485
|
+
f" user_type: {user_type}\n"
|
|
3486
|
+
)
|
|
3487
|
+
settings_dst.write_text(body, encoding="utf-8")
|
|
3488
|
+
suffix = f" (user_type={user_type})" if user_type else ""
|
|
3489
|
+
success(f"Wrote {SETTINGS_FILE}{suffix}")
|
|
3414
3490
|
|
|
3415
3491
|
# 3. install-mode marker (Step 8 A5) — authoritative state for
|
|
3416
3492
|
# doctor --context and future install-aware tooling. Written even
|
|
@@ -3437,6 +3513,120 @@ def install_minimal(target_root: Path, force: bool) -> int:
|
|
|
3437
3513
|
return 0
|
|
3438
3514
|
|
|
3439
3515
|
|
|
3516
|
+
# --- Interactive init (step-12 Phase 3, forward-compatible stub) ---
|
|
3517
|
+
|
|
3518
|
+
_INTERACTIVE_USER_TYPES: tuple[tuple[str, str], ...] = (
|
|
3519
|
+
("creator", "Content / writing / publishing"),
|
|
3520
|
+
("founder", "Early-stage company building"),
|
|
3521
|
+
("consultant", "Advisory / strategy / discovery"),
|
|
3522
|
+
("gtm", "Sales / marketing / revenue ops"),
|
|
3523
|
+
("finance", "Finance / FP&A / unit economics"),
|
|
3524
|
+
("ops", "Operations / incident / compliance"),
|
|
3525
|
+
("developer", "Engineering / code-heavy work"),
|
|
3526
|
+
)
|
|
3527
|
+
|
|
3528
|
+
_INTERACTIVE_STACKS: tuple[tuple[str, str], ...] = (
|
|
3529
|
+
("none", "No code project / pure content"),
|
|
3530
|
+
("laravel", "PHP / Laravel"),
|
|
3531
|
+
("nextjs", "TypeScript / Next.js / React"),
|
|
3532
|
+
("python", "Python / FastAPI / Django"),
|
|
3533
|
+
("symfony", "PHP / Symfony"),
|
|
3534
|
+
("generic", "Other / mixed stack"),
|
|
3535
|
+
)
|
|
3536
|
+
|
|
3537
|
+
_INTERACTIVE_VERBOSITIES: tuple[tuple[str, str], ...] = (
|
|
3538
|
+
("quiet", "Caveman / minimal output"),
|
|
3539
|
+
("normal", "Default verbosity"),
|
|
3540
|
+
("verbose", "Full intent announcements + play-by-play"),
|
|
3541
|
+
)
|
|
3542
|
+
|
|
3543
|
+
_LOCAL_CONFIG_FILE = ".agent-config.local.json"
|
|
3544
|
+
|
|
3545
|
+
|
|
3546
|
+
def _interactive_prompt_choice(label: str, options: tuple[tuple[str, str], ...]) -> str:
|
|
3547
|
+
"""Render a numbered list and return the chosen id. Defaults to option 1 on empty input."""
|
|
3548
|
+
print()
|
|
3549
|
+
print(f" {label}")
|
|
3550
|
+
for idx, (key, blurb) in enumerate(options, start=1):
|
|
3551
|
+
print(f" {idx}. {key} — {blurb}")
|
|
3552
|
+
print()
|
|
3553
|
+
while True:
|
|
3554
|
+
try:
|
|
3555
|
+
raw = input(f" Choice [1-{len(options)}, default 1]: ").strip()
|
|
3556
|
+
except EOFError:
|
|
3557
|
+
return options[0][0]
|
|
3558
|
+
if not raw:
|
|
3559
|
+
return options[0][0]
|
|
3560
|
+
if raw.isdigit():
|
|
3561
|
+
i = int(raw)
|
|
3562
|
+
if 1 <= i <= len(options):
|
|
3563
|
+
return options[i - 1][0]
|
|
3564
|
+
# Allow typing the slug directly.
|
|
3565
|
+
for key, _ in options:
|
|
3566
|
+
if raw.lower() == key:
|
|
3567
|
+
return key
|
|
3568
|
+
print(f" ⚠️ Pick a number 1-{len(options)} or one of: {', '.join(k for k, _ in options)}.")
|
|
3569
|
+
|
|
3570
|
+
|
|
3571
|
+
def run_interactive_init(project_root: Path, force: bool) -> int:
|
|
3572
|
+
"""Write ``.agent-config.local.json`` based on three TTY prompts.
|
|
3573
|
+
|
|
3574
|
+
Forward-compatible stub for [`step-9-user-types-axis`](../agents/roadmaps/step-9-user-types-axis.md):
|
|
3575
|
+
runtime skill filtering activates once that axis ships its
|
|
3576
|
+
``user-types/`` directory and ``--user-type`` flag. Until then,
|
|
3577
|
+
this file is metadata-only — read by ``doctor --context`` and the
|
|
3578
|
+
upcoming ``agent-config skills`` listing command.
|
|
3579
|
+
|
|
3580
|
+
Universal-skills allowlist (see
|
|
3581
|
+
``docs/contracts/universal-skills.md``) loads regardless of the
|
|
3582
|
+
captured ``user_type`` — the contract guarantees these 15 skills
|
|
3583
|
+
are never filtered out.
|
|
3584
|
+
|
|
3585
|
+
Returns 0 on success, 1 on collision without ``--force``. No-op
|
|
3586
|
+
(returns 0) when stdin is not a TTY.
|
|
3587
|
+
"""
|
|
3588
|
+
if not sys.stdin.isatty():
|
|
3589
|
+
warn(
|
|
3590
|
+
"--interactive requested but stdin is not a TTY; skipping the "
|
|
3591
|
+
f"prompt. Re-run interactively or hand-edit {_LOCAL_CONFIG_FILE}."
|
|
3592
|
+
)
|
|
3593
|
+
return 0
|
|
3594
|
+
|
|
3595
|
+
target = project_root / _LOCAL_CONFIG_FILE
|
|
3596
|
+
if target.exists() and not force:
|
|
3597
|
+
warn(
|
|
3598
|
+
f"{_LOCAL_CONFIG_FILE} already exists; re-run with --force to "
|
|
3599
|
+
"overwrite. Skipping interactive init."
|
|
3600
|
+
)
|
|
3601
|
+
return 0
|
|
3602
|
+
|
|
3603
|
+
print()
|
|
3604
|
+
info("Interactive init — captures user-type / stack / verbosity")
|
|
3605
|
+
info("(forward-compatible stub; runtime filtering activates with step-9)")
|
|
3606
|
+
|
|
3607
|
+
user_type = _interactive_prompt_choice("Primary user type:", _INTERACTIVE_USER_TYPES)
|
|
3608
|
+
stack = _interactive_prompt_choice("Project stack:", _INTERACTIVE_STACKS)
|
|
3609
|
+
verbosity = _interactive_prompt_choice("Verbosity profile:", _INTERACTIVE_VERBOSITIES)
|
|
3610
|
+
|
|
3611
|
+
payload: dict[str, Any] = {
|
|
3612
|
+
"$schema": "https://github.com/event4u-app/agent-config/scripts/schemas/local-config.schema.json",
|
|
3613
|
+
"version": 1,
|
|
3614
|
+
"user_type": user_type,
|
|
3615
|
+
"stack": stack,
|
|
3616
|
+
"verbosity": verbosity,
|
|
3617
|
+
"universal_skills_contract": "docs/contracts/universal-skills.md",
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
try:
|
|
3621
|
+
target.write_text(json.dumps(payload, indent=2, sort_keys=False) + "\n", encoding="utf-8")
|
|
3622
|
+
except OSError as exc:
|
|
3623
|
+
warn(f"Could not write {target}: {exc}")
|
|
3624
|
+
return 1
|
|
3625
|
+
|
|
3626
|
+
success(f"Wrote {target.relative_to(project_root)} ({user_type} / {stack} / {verbosity})")
|
|
3627
|
+
return 0
|
|
3628
|
+
|
|
3629
|
+
|
|
3440
3630
|
# --- Main ---
|
|
3441
3631
|
|
|
3442
3632
|
def main(argv: list[str]) -> int:
|
|
@@ -3466,7 +3656,13 @@ def main(argv: list[str]) -> int:
|
|
|
3466
3656
|
target_root = Path(
|
|
3467
3657
|
opts.custom_path or opts.project or os.environ.get("PROJECT_ROOT") or os.getcwd()
|
|
3468
3658
|
).resolve()
|
|
3469
|
-
|
|
3659
|
+
# Validate --user-type early so the minimal short-circuit fails
|
|
3660
|
+
# fast on a bogus slug instead of writing a half-formed stub.
|
|
3661
|
+
# _minimal_templates_root() returns <package_root>/templates/minimal;
|
|
3662
|
+
# walk two parents up to reach the package root where user-types/ lives.
|
|
3663
|
+
minimal_package_root = _minimal_templates_root().parent.parent
|
|
3664
|
+
validated_user_type = _validate_user_type(minimal_package_root, opts.user_type)
|
|
3665
|
+
return install_minimal(target_root, opts.force, validated_user_type)
|
|
3470
3666
|
|
|
3471
3667
|
# Multi-signal scope detection (Phase 1.3) + scope resolution
|
|
3472
3668
|
# (Phase 1.4). Order of precedence (highest first):
|
|
@@ -3503,7 +3699,13 @@ def main(argv: list[str]) -> int:
|
|
|
3503
3699
|
|
|
3504
3700
|
project_root = custom_path or Path(opts.project or os.environ.get("PROJECT_ROOT") or os.getcwd()).resolve()
|
|
3505
3701
|
is_first_run = not (project_root / SETTINGS_FILE).exists()
|
|
3506
|
-
|
|
3702
|
+
rc = _main_project_install(opts, project_root, parsed_tools, is_first_run)
|
|
3703
|
+
# Interactive post-install prompt (step-12 Phase 3, forward-compatible
|
|
3704
|
+
# stub). Runs only after a successful install so the local config
|
|
3705
|
+
# never ships ahead of the bridge files it parameterizes.
|
|
3706
|
+
if rc == 0 and getattr(opts, "interactive", False):
|
|
3707
|
+
run_interactive_init(project_root, opts.force)
|
|
3708
|
+
return rc
|
|
3507
3709
|
except ConflictAbort as exc:
|
|
3508
3710
|
warn(exc.message)
|
|
3509
3711
|
return 1
|
|
@@ -3538,9 +3740,13 @@ def _main_project_install(
|
|
|
3538
3740
|
info(f"Package: {package_root}")
|
|
3539
3741
|
info(f"Type: {package_type}")
|
|
3540
3742
|
info(f"Profile: {opts.profile}")
|
|
3743
|
+
if opts.user_type:
|
|
3744
|
+
info(f"UserType: {opts.user_type}")
|
|
3541
3745
|
print()
|
|
3542
3746
|
|
|
3543
|
-
ensure_agent_settings(
|
|
3747
|
+
ensure_agent_settings(
|
|
3748
|
+
project_root, package_root, opts.profile, opts.force, opts.user_type
|
|
3749
|
+
)
|
|
3544
3750
|
|
|
3545
3751
|
# Install-mode marker (Step 8 A5) — full path flips any prior
|
|
3546
3752
|
# minimal marker to "full" so doctor --context reflects the
|
package/scripts/install.sh
CHANGED
|
@@ -79,6 +79,13 @@ parse_args() {
|
|
|
79
79
|
--skip-gitignore) SKIP_GITIGNORE=true; shift ;;
|
|
80
80
|
--tools) TOOLS="$2"; shift 2 ;;
|
|
81
81
|
--tools=*) TOOLS="${1#*=}"; shift ;;
|
|
82
|
+
# --user-type is consumed by install.py (settings persistence).
|
|
83
|
+
# Accepted here so direct `bash scripts/install.sh --user-type=...`
|
|
84
|
+
# invocations from the `install` wrapper / standalone users do not
|
|
85
|
+
# trip the "Unknown argument" guard. Value is intentionally unused
|
|
86
|
+
# by the payload-sync stage.
|
|
87
|
+
--user-type) shift 2 ;;
|
|
88
|
+
--user-type=*) shift ;;
|
|
82
89
|
--minimal|--settings-only) MINIMAL=true; shift ;;
|
|
83
90
|
--help|-h) show_help; exit 0 ;;
|
|
84
91
|
*) log_error "Unknown argument: $1"; show_help; exit 1 ;;
|