@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.
Files changed (106) hide show
  1. package/.agent-src/commands/ghostwriter/delete.md +118 -0
  2. package/.agent-src/commands/ghostwriter/fetch.md +185 -0
  3. package/.agent-src/commands/ghostwriter/list.md +102 -0
  4. package/.agent-src/commands/ghostwriter/show.md +113 -0
  5. package/.agent-src/commands/ghostwriter/write.md +160 -0
  6. package/.agent-src/commands/ghostwriter.md +96 -0
  7. package/.agent-src/commands/post-as/ghostwriter.md +66 -0
  8. package/.agent-src/commands/post-as/me.md +124 -0
  9. package/.agent-src/commands/post-as.md +58 -0
  10. package/.agent-src/ghostwriter/README.md +61 -0
  11. package/.agent-src/ghostwriter/fictional-fixture-v1.md +94 -0
  12. package/.agent-src/personas/README.md +8 -0
  13. package/.agent-src/rules/domain-safety-disclaimer-consulting.md +52 -0
  14. package/.agent-src/rules/domain-safety-disclaimer-financial.md +54 -0
  15. package/.agent-src/rules/domain-safety-disclaimer-legal.md +49 -0
  16. package/.agent-src/rules/domain-safety-disclaimer-medical.md +56 -0
  17. package/.agent-src/rules/domain-safety-export-redact.md +65 -0
  18. package/.agent-src/rules/domain-safety-logging-pii-floor.md +55 -0
  19. package/.agent-src/rules/domain-safety-pii-finance.md +57 -0
  20. package/.agent-src/rules/domain-safety-pii-marketing.md +60 -0
  21. package/.agent-src/rules/domain-safety-pii-recruiting.md +56 -0
  22. package/.agent-src/rules/domain-safety-pii-support.md +57 -0
  23. package/.agent-src/rules/domain-safety-retention-finance.md +48 -0
  24. package/.agent-src/rules/domain-safety-retention-support.md +55 -0
  25. package/.agent-src/skills/api-design/SKILL.md +3 -0
  26. package/.agent-src/skills/authz-review/SKILL.md +3 -0
  27. package/.agent-src/skills/competitive-moat-analysis/SKILL.md +3 -0
  28. package/.agent-src/skills/competitive-positioning/SKILL.md +3 -0
  29. package/.agent-src/skills/content-funnel-design/SKILL.md +3 -0
  30. package/.agent-src/skills/contracts-cognition/SKILL.md +3 -0
  31. package/.agent-src/skills/dashboard-design/SKILL.md +3 -0
  32. package/.agent-src/skills/data-handling-judgment/SKILL.md +3 -0
  33. package/.agent-src/skills/dcf-modeling/SKILL.md +3 -0
  34. package/.agent-src/skills/deal-qualification-meddic/SKILL.md +3 -0
  35. package/.agent-src/skills/discovery-interview/SKILL.md +3 -0
  36. package/.agent-src/skills/editorial-calendar/SKILL.md +3 -0
  37. package/.agent-src/skills/forecast-accuracy/SKILL.md +3 -0
  38. package/.agent-src/skills/forecasting/SKILL.md +3 -0
  39. package/.agent-src/skills/fundraising-narrative/SKILL.md +3 -0
  40. package/.agent-src/skills/gtm-launch/SKILL.md +3 -0
  41. package/.agent-src/skills/incident-commander/SKILL.md +3 -0
  42. package/.agent-src/skills/launch-readiness/SKILL.md +3 -0
  43. package/.agent-src/skills/messaging-architecture/SKILL.md +3 -0
  44. package/.agent-src/skills/okr-tree-modeling/SKILL.md +3 -0
  45. package/.agent-src/skills/pipeline-strategy/SKILL.md +3 -0
  46. package/.agent-src/skills/playwright-architect/SKILL.md +3 -0
  47. package/.agent-src/skills/privacy-review/SKILL.md +4 -1
  48. package/.agent-src/skills/quality-tools/SKILL.md +3 -0
  49. package/.agent-src/skills/release-comms/SKILL.md +3 -0
  50. package/.agent-src/skills/runway-cognition/SKILL.md +3 -0
  51. package/.agent-src/skills/scenario-modeling/SKILL.md +3 -0
  52. package/.agent-src/skills/secrets-management/SKILL.md +3 -0
  53. package/.agent-src/skills/tech-debt-tracker/SKILL.md +3 -0
  54. package/.agent-src/skills/unit-economics-modeling/SKILL.md +3 -0
  55. package/.agent-src/skills/voc-extract/SKILL.md +3 -0
  56. package/.agent-src/skills/voice-and-tone-design/SKILL.md +3 -0
  57. package/.agent-src/templates/agents/agent-project-settings.example.yml +16 -1
  58. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +299 -20
  59. package/.claude-plugin/marketplace.json +10 -1
  60. package/CHANGELOG.md +200 -211
  61. package/README.md +55 -23
  62. package/config/gitignore-block.txt +8 -0
  63. package/docs/announcements/2026-05-non-dev-launch.md +79 -0
  64. package/docs/architecture.md +2 -2
  65. package/docs/archive/CHANGELOG-pre-2.15.0.md +244 -0
  66. package/docs/case-studies/_template.md +60 -0
  67. package/docs/catalog.md +24 -3
  68. package/docs/contracts/agent-user-schema.md +1 -0
  69. package/docs/contracts/command-clusters.md +2 -0
  70. package/docs/contracts/file-ownership-matrix.json +490 -0
  71. package/docs/contracts/ghostwriter-schema.md +337 -0
  72. package/docs/contracts/init-telemetry.md +133 -0
  73. package/docs/contracts/router-blending.md +71 -0
  74. package/docs/contracts/universal-skills.md +92 -0
  75. package/docs/contracts/write-engine.md +142 -0
  76. package/docs/getting-started-by-role.md +89 -0
  77. package/docs/getting-started-laravel.md +72 -0
  78. package/docs/getting-started.md +2 -2
  79. package/docs/installation.md +221 -2
  80. package/docs/safety.md +30 -0
  81. package/package.json +1 -1
  82. package/scripts/_cli/cmd_doctor.py +238 -8
  83. package/scripts/_cli/cmd_migrate.py +6 -1
  84. package/scripts/_cli/cmd_prune.py +8 -3
  85. package/scripts/_cli/cmd_sync.py +7 -3
  86. package/scripts/_cli/cmd_uninstall.py +4 -3
  87. package/scripts/_cli/cmd_update.py +5 -1
  88. package/scripts/_cli/cmd_validate.py +6 -3
  89. package/scripts/_cli/cmd_versions.py +15 -2
  90. package/scripts/_lib/agent_settings.py +299 -20
  91. package/scripts/agent-config +64 -0
  92. package/scripts/bench_runner.py +158 -0
  93. package/scripts/check_role_doc_links.py +110 -0
  94. package/scripts/compress.py +11 -0
  95. package/scripts/ghostwriter_fixture_allowlist.txt +16 -0
  96. package/scripts/install +39 -2
  97. package/scripts/install.py +304 -1
  98. package/scripts/install.sh +20 -0
  99. package/scripts/lint_ghostwriter_source.py +240 -0
  100. package/scripts/measure_skill_reduction.py +102 -0
  101. package/scripts/schemas/rule.schema.json +5 -0
  102. package/scripts/schemas/skill.schema.json +6 -0
  103. package/scripts/update-github-metadata.sh +84 -0
  104. package/templates/agent-config-wrapper.sh +7 -0
  105. package/templates/minimal/.agent-settings.yml +23 -0
  106. 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())
@@ -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,48p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
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"
@@ -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
- return _main_project_install(opts, project_root, parsed_tools, is_first_run)
3632
+ rc = _main_project_install(opts, project_root, parsed_tools, is_first_run)
3633
+ # Interactive post-install prompt (step-12 Phase 3, forward-compatible
3634
+ # stub). Runs only after a successful install so the local config
3635
+ # never ships ahead of the bridge files it parameterizes.
3636
+ if rc == 0 and getattr(opts, "interactive", False):
3637
+ run_interactive_init(project_root, opts.force)
3638
+ return rc
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).
@@ -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