@event4u/agent-config 2.15.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/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +299 -20
- package/.claude-plugin/marketplace.json +10 -1
- package/CHANGELOG.md +200 -211
- package/README.md +55 -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/archive/CHANGELOG-pre-2.15.0.md +244 -0
- 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/installation.md +221 -2
- package/docs/safety.md +30 -0
- package/package.json +1 -1
- package/scripts/_cli/cmd_doctor.py +238 -8
- package/scripts/_cli/cmd_migrate.py +6 -1
- package/scripts/_cli/cmd_prune.py +8 -3
- package/scripts/_cli/cmd_sync.py +7 -3
- package/scripts/_cli/cmd_uninstall.py +4 -3
- package/scripts/_cli/cmd_update.py +5 -1
- package/scripts/_cli/cmd_validate.py +6 -3
- package/scripts/_cli/cmd_versions.py +15 -2
- package/scripts/_lib/agent_settings.py +299 -20
- package/scripts/agent-config +64 -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 +39 -2
- package/scripts/install.py +304 -1
- package/scripts/install.sh +20 -0
- 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
- package/templates/agent-config-wrapper.sh +7 -0
- package/templates/minimal/.agent-settings.yml +23 -0
- package/templates/minimal/agents-gitkeep +2 -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
|
@@ -44,6 +44,12 @@
|
|
|
44
44
|
# for child subprocesses. All bridge content is bundled
|
|
45
45
|
# in the package, so install itself is already offline-safe;
|
|
46
46
|
# this flag is the explicit air-gap / CI guarantee.
|
|
47
|
+
# --minimal Bootstrap only `.agent-settings.yml`, `agents/.gitkeep`,
|
|
48
|
+
# and the `./agent-config` wrapper. No tool payload, no
|
|
49
|
+
# AGENTS.md, no symlinks. Refuses to install inside an
|
|
50
|
+
# existing agent-config project (nested-install guard).
|
|
51
|
+
# See docs/installation.md → "Minimal init".
|
|
52
|
+
# --settings-only Alias for --minimal.
|
|
47
53
|
# --help, -h Show this help
|
|
48
54
|
#
|
|
49
55
|
# Examples:
|
|
@@ -75,12 +81,13 @@ GLOBAL=false
|
|
|
75
81
|
SCOPE=""
|
|
76
82
|
CUSTOM_PATH=""
|
|
77
83
|
OFFLINE=false
|
|
84
|
+
MINIMAL=false
|
|
78
85
|
|
|
79
86
|
# Single source of truth for valid tool IDs (also referenced by install.sh / install.py).
|
|
80
87
|
VALID_TOOLS="claude-code claude-desktop cursor windsurf cline gemini-cli copilot augment aider codex roocode continue kilocode zed jetbrains kiro all"
|
|
81
88
|
|
|
82
89
|
show_help() {
|
|
83
|
-
sed -n '3,
|
|
90
|
+
sed -n '3,54p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
list_tools() {
|
|
@@ -157,6 +164,8 @@ while [[ $# -gt 0 ]]; do
|
|
|
157
164
|
--custom-path) CUSTOM_PATH="$2"; shift 2 ;;
|
|
158
165
|
--custom-path=*) CUSTOM_PATH="${1#*=}"; shift ;;
|
|
159
166
|
--offline) OFFLINE=true; shift ;;
|
|
167
|
+
--minimal|--settings-only)
|
|
168
|
+
MINIMAL=true; shift ;;
|
|
160
169
|
--help|-h) show_help; exit 0 ;;
|
|
161
170
|
*) err "Unknown argument: $1"; show_help >&2; exit 1 ;;
|
|
162
171
|
esac
|
|
@@ -202,7 +211,7 @@ prompt_tools() {
|
|
|
202
211
|
echo " ✅ Selected: $TOOLS"
|
|
203
212
|
}
|
|
204
213
|
|
|
205
|
-
if ! $TOOLS_EXPLICIT && ! $YES && ! $QUIET && ! $LIST_TOOLS && [[ -t 0 && -t 1 ]]; then
|
|
214
|
+
if ! $MINIMAL && ! $TOOLS_EXPLICIT && ! $YES && ! $QUIET && ! $LIST_TOOLS && [[ -t 0 && -t 1 ]]; then
|
|
206
215
|
prompt_tools
|
|
207
216
|
TOOLS_EXPLICIT=true
|
|
208
217
|
fi
|
|
@@ -280,6 +289,7 @@ run_sync() {
|
|
|
280
289
|
$DRY_RUN && args+=(--dry-run)
|
|
281
290
|
$VERBOSE && args+=(--verbose)
|
|
282
291
|
$QUIET && args+=(--quiet)
|
|
292
|
+
$MINIMAL && args+=(--minimal)
|
|
283
293
|
args+=(--tools="$TOOLS")
|
|
284
294
|
bash "$INSTALL_SH" "${args[@]}"
|
|
285
295
|
}
|
|
@@ -305,12 +315,39 @@ run_bridges() {
|
|
|
305
315
|
[[ -n "$SCOPE" ]] && args+=(--scope="$SCOPE")
|
|
306
316
|
[[ -n "$CUSTOM_PATH" ]] && args+=(--custom-path="$CUSTOM_PATH")
|
|
307
317
|
$OFFLINE && args+=(--offline)
|
|
318
|
+
$MINIMAL && args+=(--minimal)
|
|
308
319
|
args+=(--tools="$TOOLS")
|
|
309
320
|
"$python_bin" "$INSTALL_PY" "${args[@]}"
|
|
310
321
|
}
|
|
311
322
|
|
|
312
323
|
RC=0
|
|
313
324
|
|
|
325
|
+
# Minimal init runs the bridge stage *first* so its nested-install guard
|
|
326
|
+
# (Step 7 Phase 2) fires before any wrapper / file is written. The
|
|
327
|
+
# payload sync stage is then a no-op in minimal mode (install.sh
|
|
328
|
+
# short-circuits) but is still invoked so it can install the
|
|
329
|
+
# `./agent-config` wrapper on a confirmed-clean target.
|
|
330
|
+
if $MINIMAL; then
|
|
331
|
+
if ! $SKIP_BRIDGES; then
|
|
332
|
+
if [[ ! -f "$INSTALL_PY" ]]; then
|
|
333
|
+
err "Missing $INSTALL_PY"
|
|
334
|
+
exit 1
|
|
335
|
+
fi
|
|
336
|
+
run_bridges || RC=$?
|
|
337
|
+
fi
|
|
338
|
+
if [[ $RC -ne 0 ]]; then
|
|
339
|
+
exit $RC
|
|
340
|
+
fi
|
|
341
|
+
if ! $SKIP_SYNC; then
|
|
342
|
+
if [[ ! -f "$INSTALL_SH" ]]; then
|
|
343
|
+
err "Missing $INSTALL_SH"
|
|
344
|
+
exit 1
|
|
345
|
+
fi
|
|
346
|
+
run_sync || RC=$?
|
|
347
|
+
fi
|
|
348
|
+
exit $RC
|
|
349
|
+
fi
|
|
350
|
+
|
|
314
351
|
if ! $SKIP_SYNC; then
|
|
315
352
|
if [[ ! -f "$INSTALL_SH" ]]; then
|
|
316
353
|
err "Missing $INSTALL_SH"
|
package/scripts/install.py
CHANGED
|
@@ -3223,6 +3223,31 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
|
3223
3223
|
"explicit guarantee for air-gapped / CI runs."
|
|
3224
3224
|
),
|
|
3225
3225
|
)
|
|
3226
|
+
parser.add_argument(
|
|
3227
|
+
"--minimal",
|
|
3228
|
+
"--settings-only",
|
|
3229
|
+
dest="minimal",
|
|
3230
|
+
action="store_true",
|
|
3231
|
+
help=(
|
|
3232
|
+
"bootstrap only the project-local override layer: writes "
|
|
3233
|
+
"agents/.gitkeep and a .agent-settings.yml stub. No tool "
|
|
3234
|
+
"payload, no AGENTS.md, no symlinks. Refuses to install "
|
|
3235
|
+
"inside an existing agent-config project (nested-install "
|
|
3236
|
+
"guard). See docs/installation.md → Minimal init."
|
|
3237
|
+
),
|
|
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
|
+
)
|
|
3226
3251
|
opts = parser.parse_args(argv)
|
|
3227
3252
|
opts.tools = _merge_tools_aliases(opts.tools, opts.ai)
|
|
3228
3253
|
if opts.scope == "global" and opts.custom_path:
|
|
@@ -3282,6 +3307,262 @@ def _is_tool_enabled(tools: set[str], tool_id: str) -> bool:
|
|
|
3282
3307
|
return tool_id in tools
|
|
3283
3308
|
|
|
3284
3309
|
|
|
3310
|
+
# --- Minimal init (Step 7 Phase 2) ---
|
|
3311
|
+
|
|
3312
|
+
|
|
3313
|
+
def _minimal_templates_root() -> Path:
|
|
3314
|
+
"""Resolve the bundled ``templates/minimal/`` directory.
|
|
3315
|
+
|
|
3316
|
+
Walks up from this file looking for ``templates/minimal/``; this
|
|
3317
|
+
works both in development mode (running the source tree) and from
|
|
3318
|
+
an ``npm install -g`` install (the script lives under the package
|
|
3319
|
+
root regardless).
|
|
3320
|
+
"""
|
|
3321
|
+
for ancestor in (Path(__file__).resolve(), *Path(__file__).resolve().parents):
|
|
3322
|
+
candidate = ancestor / "templates" / "minimal"
|
|
3323
|
+
if candidate.is_dir():
|
|
3324
|
+
return candidate
|
|
3325
|
+
fail("Could not locate templates/minimal/ — package install is corrupt.")
|
|
3326
|
+
return Path() # unreachable
|
|
3327
|
+
|
|
3328
|
+
|
|
3329
|
+
#: Relative path of the install-mode marker file written by both the
|
|
3330
|
+
#: minimal short-circuit and the full install path (Step 8 A5). Read by
|
|
3331
|
+
#: ``doctor --context`` (and any future tooling) instead of inferring
|
|
3332
|
+
#: install state from filesystem heuristics like ``AGENTS.md`` presence.
|
|
3333
|
+
INSTALL_MODE_MARKER_REL = "agents/.agent-state/install-mode.txt"
|
|
3334
|
+
|
|
3335
|
+
|
|
3336
|
+
def _write_install_mode_marker(project_root: Path, mode: str) -> None:
|
|
3337
|
+
"""Write ``agents/.agent-state/install-mode.txt`` = ``mode\\n``.
|
|
3338
|
+
|
|
3339
|
+
Idempotent: overwrites unconditionally so re-installs flip the
|
|
3340
|
+
state correctly (e.g. minimal → full upgrade). Failure to write
|
|
3341
|
+
is non-fatal — install proceeds and ``doctor --context`` falls
|
|
3342
|
+
back to the filesystem heuristic. ``mode`` must be ``minimal``
|
|
3343
|
+
or ``full``.
|
|
3344
|
+
"""
|
|
3345
|
+
if mode not in ("minimal", "full"):
|
|
3346
|
+
return
|
|
3347
|
+
marker = project_root / INSTALL_MODE_MARKER_REL
|
|
3348
|
+
try:
|
|
3349
|
+
marker.parent.mkdir(parents=True, exist_ok=True)
|
|
3350
|
+
marker.write_text(f"{mode}\n", encoding="utf-8")
|
|
3351
|
+
except OSError:
|
|
3352
|
+
# Marker is advisory; install must not abort because the
|
|
3353
|
+
# state dir is unwritable (e.g. read-only mount in CI).
|
|
3354
|
+
pass
|
|
3355
|
+
|
|
3356
|
+
|
|
3357
|
+
def install_minimal(target_root: Path, force: bool) -> int:
|
|
3358
|
+
"""Bootstrap the project-local override layer only (D2-compliant).
|
|
3359
|
+
|
|
3360
|
+
Writes:
|
|
3361
|
+
|
|
3362
|
+
* ``agents/.gitkeep`` so the folder is committable.
|
|
3363
|
+
* ``.agent-settings.yml`` stub (cost_profile=balanced, version pin
|
|
3364
|
+
commented out per D4).
|
|
3365
|
+
|
|
3366
|
+
Refuses (exit 1) when ``target_root`` is **inside** an existing
|
|
3367
|
+
agent-config project (Phase-1 anchor walk above the target). The
|
|
3368
|
+
in-target case is allowed and treated as idempotent — re-running
|
|
3369
|
+
``--minimal`` in a folder that already has ``.agent-settings.yml``
|
|
3370
|
+
does nothing unless ``--force`` is passed.
|
|
3371
|
+
|
|
3372
|
+
Does **not** touch ``.gitignore`` (D2 — user owns the ignore file).
|
|
3373
|
+
The ``./agent-config`` wrapper is installed by ``scripts/install.sh``
|
|
3374
|
+
in its own minimal short-circuit.
|
|
3375
|
+
"""
|
|
3376
|
+
try:
|
|
3377
|
+
from scripts._lib.agent_settings import find_project_root_with_anchor # noqa: PLC0415
|
|
3378
|
+
except ImportError: # pragma: no cover — alt sys.path layout
|
|
3379
|
+
from _lib.agent_settings import find_project_root_with_anchor # type: ignore[no-redef] # noqa: PLC0415
|
|
3380
|
+
|
|
3381
|
+
target_root = target_root.resolve()
|
|
3382
|
+
target_root.mkdir(parents=True, exist_ok=True)
|
|
3383
|
+
|
|
3384
|
+
# Nested-install guard: walk up from the *parent* of target_root.
|
|
3385
|
+
# An anchor at target_root itself is allowed (re-running --minimal
|
|
3386
|
+
# in the same project is idempotent); only a root *above* target
|
|
3387
|
+
# blocks the install.
|
|
3388
|
+
parent = target_root.parent
|
|
3389
|
+
if parent != target_root: # not filesystem root
|
|
3390
|
+
existing = find_project_root_with_anchor(parent)
|
|
3391
|
+
if existing is not None and existing[0] != target_root:
|
|
3392
|
+
root, anchor = existing
|
|
3393
|
+
fail(
|
|
3394
|
+
"Refusing to nest an agent-config layer inside an existing "
|
|
3395
|
+
f"project (anchor: {anchor}). Existing root: {root}. "
|
|
3396
|
+
"Remove the parent layer first or run `--minimal` outside it."
|
|
3397
|
+
)
|
|
3398
|
+
return 1 # unreachable; fail() exits
|
|
3399
|
+
|
|
3400
|
+
templates = _minimal_templates_root()
|
|
3401
|
+
settings_src = templates / SETTINGS_FILE
|
|
3402
|
+
gitkeep_src = templates / "agents-gitkeep"
|
|
3403
|
+
|
|
3404
|
+
if not settings_src.is_file() or not gitkeep_src.is_file():
|
|
3405
|
+
fail(f"Bundled minimal templates missing under {templates}")
|
|
3406
|
+
|
|
3407
|
+
info(f"Minimal init → {target_root}")
|
|
3408
|
+
|
|
3409
|
+
# 1. agents/.gitkeep
|
|
3410
|
+
agents_dir = target_root / "agents"
|
|
3411
|
+
agents_dir.mkdir(exist_ok=True)
|
|
3412
|
+
gitkeep_dst = agents_dir / ".gitkeep"
|
|
3413
|
+
if gitkeep_dst.exists() and not force:
|
|
3414
|
+
skip(f"agents/.gitkeep already exists (use --force to overwrite)")
|
|
3415
|
+
else:
|
|
3416
|
+
gitkeep_dst.write_text(gitkeep_src.read_text(encoding="utf-8"), encoding="utf-8")
|
|
3417
|
+
success("Wrote agents/.gitkeep")
|
|
3418
|
+
|
|
3419
|
+
# 2. .agent-settings.yml stub
|
|
3420
|
+
settings_dst = target_root / SETTINGS_FILE
|
|
3421
|
+
if settings_dst.exists() and not force:
|
|
3422
|
+
skip(f"{SETTINGS_FILE} already exists (use --force to overwrite)")
|
|
3423
|
+
else:
|
|
3424
|
+
settings_dst.write_text(settings_src.read_text(encoding="utf-8"), encoding="utf-8")
|
|
3425
|
+
success(f"Wrote {SETTINGS_FILE}")
|
|
3426
|
+
|
|
3427
|
+
# 3. install-mode marker (Step 8 A5) — authoritative state for
|
|
3428
|
+
# doctor --context and future install-aware tooling. Written even
|
|
3429
|
+
# on idempotent re-runs so the marker is repaired if removed.
|
|
3430
|
+
_write_install_mode_marker(target_root, "minimal")
|
|
3431
|
+
|
|
3432
|
+
# Stderr upgrade hint (Step 8 A5) — minimal installs are intentionally
|
|
3433
|
+
# stripped; surface the upgrade path on stderr so it appears in
|
|
3434
|
+
# human terminals without polluting stdout-parsed output. Suppressed
|
|
3435
|
+
# under --quiet to honor scripted-install contracts.
|
|
3436
|
+
if not QUIET:
|
|
3437
|
+
print(
|
|
3438
|
+
"ℹ️ Minimal install — run `agent-config install --force` "
|
|
3439
|
+
"to add AGENTS.md, bridges, and tool integrations.",
|
|
3440
|
+
file=sys.stderr,
|
|
3441
|
+
)
|
|
3442
|
+
|
|
3443
|
+
if not QUIET:
|
|
3444
|
+
print()
|
|
3445
|
+
info("Next steps:")
|
|
3446
|
+
info(" • Ensure `agent-config` is on $PATH: npm install -g @event4u/agent-config")
|
|
3447
|
+
info(" • Add `.agent-settings.yml` and `agents/` to git (or to .gitignore — your call).")
|
|
3448
|
+
info(" • Run `agent-config doctor` to verify the layer is picked up.")
|
|
3449
|
+
return 0
|
|
3450
|
+
|
|
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
|
+
|
|
3285
3566
|
# --- Main ---
|
|
3286
3567
|
|
|
3287
3568
|
def main(argv: list[str]) -> int:
|
|
@@ -3302,6 +3583,17 @@ def main(argv: list[str]) -> int:
|
|
|
3302
3583
|
if opts.profile not in SUPPORTED_PROFILES:
|
|
3303
3584
|
fail(f"Unsupported profile: {opts.profile}. Supported: {', '.join(SUPPORTED_PROFILES)}")
|
|
3304
3585
|
|
|
3586
|
+
# Minimal-init short-circuit (Step 7 Phase 2): bypass scope
|
|
3587
|
+
# detection, conflict policy, and the full bridge install. Writes
|
|
3588
|
+
# only the project-local override layer (agents/.gitkeep +
|
|
3589
|
+
# .agent-settings.yml stub). The bash wrapper handles the
|
|
3590
|
+
# `./agent-config` script; everything else is intentionally absent.
|
|
3591
|
+
if opts.minimal:
|
|
3592
|
+
target_root = Path(
|
|
3593
|
+
opts.custom_path or opts.project or os.environ.get("PROJECT_ROOT") or os.getcwd()
|
|
3594
|
+
).resolve()
|
|
3595
|
+
return install_minimal(target_root, opts.force)
|
|
3596
|
+
|
|
3305
3597
|
# Multi-signal scope detection (Phase 1.3) + scope resolution
|
|
3306
3598
|
# (Phase 1.4). Order of precedence (highest first):
|
|
3307
3599
|
# 1. --scope=<x> — explicit user override (CI-friendly; auto = honor detection)
|
|
@@ -3337,7 +3629,13 @@ def main(argv: list[str]) -> int:
|
|
|
3337
3629
|
|
|
3338
3630
|
project_root = custom_path or Path(opts.project or os.environ.get("PROJECT_ROOT") or os.getcwd()).resolve()
|
|
3339
3631
|
is_first_run = not (project_root / SETTINGS_FILE).exists()
|
|
3340
|
-
|
|
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
|
|
3341
3639
|
except ConflictAbort as exc:
|
|
3342
3640
|
warn(exc.message)
|
|
3343
3641
|
return 1
|
|
@@ -3376,6 +3674,11 @@ def _main_project_install(
|
|
|
3376
3674
|
|
|
3377
3675
|
ensure_agent_settings(project_root, package_root, opts.profile, opts.force)
|
|
3378
3676
|
|
|
3677
|
+
# Install-mode marker (Step 8 A5) — full path flips any prior
|
|
3678
|
+
# minimal marker to "full" so doctor --context reflects the
|
|
3679
|
+
# upgraded state. Idempotent on re-runs of the same scope.
|
|
3680
|
+
_write_install_mode_marker(project_root, "full")
|
|
3681
|
+
|
|
3379
3682
|
tools = parsed_tools
|
|
3380
3683
|
|
|
3381
3684
|
# Per-tool merged_keys collected from JSON bridge merges (P1.5).
|
package/scripts/install.sh
CHANGED
|
@@ -37,6 +37,10 @@ DRY_RUN=false
|
|
|
37
37
|
VERBOSE=false
|
|
38
38
|
QUIET=false
|
|
39
39
|
SKIP_GITIGNORE=false
|
|
40
|
+
# When true, skip payload sync entirely and only install the project-local
|
|
41
|
+
# `./agent-config` wrapper (Step 7 Phase 2). The bridge stage (install.py)
|
|
42
|
+
# handles the .agent-settings.yml stub + nested-install guard.
|
|
43
|
+
MINIMAL=false
|
|
40
44
|
# Comma-separated tool IDs (default: all). Set by --tools or the
|
|
41
45
|
# orchestrator (scripts/install). The .augment/ substrate is always
|
|
42
46
|
# synced because every other tool symlinks back into it.
|
|
@@ -75,6 +79,7 @@ parse_args() {
|
|
|
75
79
|
--skip-gitignore) SKIP_GITIGNORE=true; shift ;;
|
|
76
80
|
--tools) TOOLS="$2"; shift 2 ;;
|
|
77
81
|
--tools=*) TOOLS="${1#*=}"; shift ;;
|
|
82
|
+
--minimal|--settings-only) MINIMAL=true; shift ;;
|
|
78
83
|
--help|-h) show_help; exit 0 ;;
|
|
79
84
|
*) log_error "Unknown argument: $1"; show_help; exit 1 ;;
|
|
80
85
|
esac
|
|
@@ -726,6 +731,21 @@ install_cli_wrapper() {
|
|
|
726
731
|
main() {
|
|
727
732
|
parse_args "$@"
|
|
728
733
|
|
|
734
|
+
# Minimal-init short-circuit (Step 7 Phase 2): skip every payload-sync
|
|
735
|
+
# stage and only install the project-local `./agent-config` wrapper.
|
|
736
|
+
# The bridge stage (install.py) handles the .agent-settings.yml stub
|
|
737
|
+
# + nested-install guard. No .augment/, no AGENTS.md, no symlinks.
|
|
738
|
+
if $MINIMAL; then
|
|
739
|
+
if ! $QUIET; then
|
|
740
|
+
echo "🔧 Minimal init — installing ./agent-config wrapper only"
|
|
741
|
+
echo " Target: $TARGET_DIR"
|
|
742
|
+
$DRY_RUN && echo " Mode: DRY RUN"
|
|
743
|
+
fi
|
|
744
|
+
install_cli_wrapper "$TARGET_DIR"
|
|
745
|
+
$QUIET || echo "✅ Wrapper installed (payload sync skipped)."
|
|
746
|
+
return 0
|
|
747
|
+
fi
|
|
748
|
+
|
|
729
749
|
# First-run detection: gate the verbose source/target banner behind the
|
|
730
750
|
# absence of .agent-settings.yml. Re-runs print a single status line.
|
|
731
751
|
local is_first_run=false
|