@event4u/agent-config 5.6.1 → 5.8.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 (225) hide show
  1. package/.agent-src/commands/agent-handoff.md +1 -1
  2. package/.agent-src/commands/agent-status.md +1 -1
  3. package/.agent-src/commands/agents/audit.md +1 -1
  4. package/.agent-src/commands/agents/init.md +1 -1
  5. package/.agent-src/commands/agents/user/accept.md +3 -3
  6. package/.agent-src/commands/agents/user/init.md +4 -4
  7. package/.agent-src/commands/agents/user/show.md +3 -3
  8. package/.agent-src/commands/agents/user/update.md +3 -3
  9. package/.agent-src/commands/agents/user.md +1 -1
  10. package/.agent-src/commands/agents.md +1 -1
  11. package/.agent-src/commands/analytics/prune.md +1 -1
  12. package/.agent-src/commands/analytics/show.md +1 -1
  13. package/.agent-src/commands/analytics.md +1 -1
  14. package/.agent-src/commands/bug-fix.md +1 -1
  15. package/.agent-src/commands/challenge-me.md +1 -1
  16. package/.agent-src/commands/chat-history/import.md +1 -1
  17. package/.agent-src/commands/chat-history/learn.md +1 -1
  18. package/.agent-src/commands/chat-history/show.md +1 -1
  19. package/.agent-src/commands/chat-history.md +1 -1
  20. package/.agent-src/commands/check-current-md.md +1 -1
  21. package/.agent-src/commands/condense.md +1 -1
  22. package/.agent-src/commands/context.md +1 -1
  23. package/.agent-src/commands/cost-report.md +13 -8
  24. package/.agent-src/commands/council.md +3 -3
  25. package/.agent-src/commands/create-pr/description-only.md +1 -1
  26. package/.agent-src/commands/create-pr.md +1 -1
  27. package/.agent-src/commands/e2e-heal.md +1 -1
  28. package/.agent-src/commands/e2e-plan.md +1 -1
  29. package/.agent-src/commands/feature.md +1 -1
  30. package/.agent-src/commands/fix/ci.md +1 -1
  31. package/.agent-src/commands/fix/portability.md +1 -1
  32. package/.agent-src/commands/fix/pr-bot-comments.md +1 -1
  33. package/.agent-src/commands/fix/pr-comments.md +1 -1
  34. package/.agent-src/commands/fix/pr-developer-comments.md +1 -1
  35. package/.agent-src/commands/fix/refs.md +1 -1
  36. package/.agent-src/commands/fix/seeder.md +1 -1
  37. package/.agent-src/commands/fix.md +1 -1
  38. package/.agent-src/commands/judge.md +1 -1
  39. package/.agent-src/commands/knowledge/cross-repo.md +1 -1
  40. package/.agent-src/commands/knowledge/forget.md +1 -1
  41. package/.agent-src/commands/knowledge/ingest.md +1 -1
  42. package/.agent-src/commands/knowledge/list.md +1 -1
  43. package/.agent-src/commands/knowledge.md +1 -1
  44. package/.agent-src/commands/memory/add.md +1 -1
  45. package/.agent-src/commands/memory/learn-low-impact.md +1 -1
  46. package/.agent-src/commands/memory/load.md +1 -1
  47. package/.agent-src/commands/memory/mine-session.md +1 -1
  48. package/.agent-src/commands/memory/promote.md +1 -1
  49. package/.agent-src/commands/memory/propose.md +1 -1
  50. package/.agent-src/commands/memory.md +1 -1
  51. package/.agent-src/commands/mode.md +1 -1
  52. package/.agent-src/commands/optimize/agents-dir.md +1 -1
  53. package/.agent-src/commands/optimize/augmentignore.md +1 -1
  54. package/.agent-src/commands/optimize/rtk.md +1 -1
  55. package/.agent-src/commands/optimize/skills.md +1 -1
  56. package/.agent-src/commands/optimize.md +1 -1
  57. package/.agent-src/commands/orchestrate.md +1 -1
  58. package/.agent-src/commands/override/create.md +1 -1
  59. package/.agent-src/commands/override/manage.md +1 -1
  60. package/.agent-src/commands/override.md +1 -1
  61. package/.agent-src/commands/package-reset.md +1 -1
  62. package/.agent-src/commands/prediction-pool.md +234 -0
  63. package/.agent-src/commands/profile/activate.md +81 -0
  64. package/.agent-src/commands/profile/deactivate.md +68 -0
  65. package/.agent-src/commands/profile/show.md +70 -0
  66. package/.agent-src/commands/profile.md +68 -0
  67. package/.agent-src/commands/project-health.md +1 -1
  68. package/.agent-src/commands/quality-fix.md +1 -1
  69. package/.agent-src/commands/roadmap/process-full.md +1 -1
  70. package/.agent-src/commands/roadmap/process-phase.md +1 -1
  71. package/.agent-src/commands/roadmap/process-step.md +1 -1
  72. package/.agent-src/commands/roadmap.md +1 -1
  73. package/.agent-src/commands/set-cost-profile.md +9 -9
  74. package/.agent-src/commands/skill/preview.md +3 -3
  75. package/.agent-src/commands/skill.md +1 -1
  76. package/.agent-src/commands/skills/discover.md +1 -1
  77. package/.agent-src/commands/skills.md +1 -1
  78. package/.agent-src/commands/sync-agent-settings.md +3 -3
  79. package/.agent-src/commands/sync-gitignore/fix.md +1 -1
  80. package/.agent-src/commands/sync-gitignore.md +1 -1
  81. package/.agent-src/commands/update-form-request-messages.md +1 -1
  82. package/.agent-src/presets/README.md +1 -1
  83. package/.agent-src/profiles/README.md +1 -1
  84. package/.agent-src/rules/non-destructive-by-default.md +2 -1
  85. package/.agent-src/skills/check-refs/SKILL.md +1 -1
  86. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +1 -1
  87. package/.agent-src/skills/git-workflow/SKILL.md +1 -1
  88. package/.agent-src/skills/jira-integration/SKILL.md +1 -1
  89. package/.agent-src/skills/markitdown/SKILL.md +1 -1
  90. package/.agent-src/skills/prediction-pool-optimizer/SKILL.md +314 -0
  91. package/.agent-src/skills/prediction-pool-optimizer/evals/triggers.json +20 -0
  92. package/.agent-src/skills/prediction-pool-optimizer/reference/ev-fixtures.md +175 -0
  93. package/.agent-src/skills/prediction-pool-optimizer/reference/odds-and-bonus.md +109 -0
  94. package/.agent-src/skills/rtk-output-filtering/SKILL.md +1 -1
  95. package/.agent-src/skills/script-writing/SKILL.md +1 -1
  96. package/.agent-src/skills/token-optimizer/SKILL.md +1 -1
  97. package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -1
  98. package/.agent-src/templates/agent-settings.md +7 -7
  99. package/.agent-src/templates/agents/agent-project-settings.example.yml +2 -2
  100. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +54 -6
  101. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +1 -1
  102. package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +9 -7
  103. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +9 -10
  104. package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +17 -4
  105. package/.claude-plugin/marketplace.json +370 -364
  106. package/CHANGELOG.md +108 -0
  107. package/README.md +2 -2
  108. package/config/agent-settings.template.yml +11 -2
  109. package/config/discovery/packs.yml +11 -0
  110. package/config/discovery/session-profiles.yml +37 -0
  111. package/config/discovery/workspaces.yml +1 -1
  112. package/config/profiles/balanced.ini +1 -1
  113. package/config/profiles/full.ini +1 -1
  114. package/config/profiles/minimal.ini +1 -1
  115. package/dist/discovery/deprecation-report.md +1 -1
  116. package/dist/discovery/discovery-manifest.json +254 -100
  117. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  118. package/dist/discovery/discovery-manifest.summary.md +4 -3
  119. package/dist/discovery/orphan-report.md +1 -1
  120. package/dist/discovery/packs.json +41 -6
  121. package/dist/discovery/trust-report.md +3 -3
  122. package/dist/discovery/workspaces.json +19 -6
  123. package/dist/mcp/registry-manifest.json +3 -3
  124. package/dist/server/io/substituteTemplate.js +3 -3
  125. package/dist/server/io/substituteTemplate.js.map +1 -1
  126. package/dist/server/routes/settings.js +2 -2
  127. package/dist/server/routes/settings.js.map +1 -1
  128. package/dist/server/schemas/settings.js +4 -2
  129. package/dist/server/schemas/settings.js.map +1 -1
  130. package/dist/ui/assets/{index-DVsyUMZe.js → index-5lFqAKL0.js} +2 -2
  131. package/dist/ui/assets/index-5lFqAKL0.js.map +1 -0
  132. package/dist/ui/index.html +1 -1
  133. package/docs/architecture/current-onboard-baseline.md +3 -3
  134. package/docs/architecture.md +2 -2
  135. package/docs/catalog.md +11 -5
  136. package/docs/contracts/adr-level-6-productization.md +1 -1
  137. package/docs/contracts/command-clusters.md +2 -0
  138. package/docs/contracts/config-presets.md +2 -2
  139. package/docs/contracts/cost-profile-defaults.md +5 -5
  140. package/docs/contracts/discovery-manifest.schema.json +1 -1
  141. package/docs/contracts/explain-trace.schema.json +3 -3
  142. package/docs/contracts/memory-visibility-v1.md +15 -7
  143. package/docs/contracts/profile-system.md +2 -2
  144. package/docs/contracts/session-profile-overlay.md +120 -0
  145. package/docs/contracts/settings-api.md +3 -3
  146. package/docs/contracts/value-report-schema.md +14 -1
  147. package/docs/customization.md +47 -5
  148. package/docs/decisions/ADR-010-profile-pack-preset-boundary.md +47 -11
  149. package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +16 -2
  150. package/docs/decisions/ADR-034-per-skill-model-recommendation-transport.md +1 -1
  151. package/docs/decisions/ADR-036-global-install-browser-wizard-handoff.md +106 -0
  152. package/docs/decisions/ADR-037-cost-profile-untangle.md +117 -0
  153. package/docs/decisions/ADR-038-canonical-settings-path.md +66 -0
  154. package/docs/decisions/ADR-039-claude-skills-untracked.md +139 -0
  155. package/docs/decisions/ADR-rule-kernel-and-router.md +1 -1
  156. package/docs/decisions/INDEX.md +4 -0
  157. package/docs/development.md +12 -0
  158. package/docs/getting-started.md +2 -2
  159. package/docs/guidelines/agent-infra/layered-settings.md +10 -4
  160. package/docs/installation.md +3 -3
  161. package/docs/setup/mcp-client-config.md +1 -1
  162. package/docs/skills-catalog.md +5 -1
  163. package/docs/value.md +9 -7
  164. package/docs/wizard.md +1 -1
  165. package/llms.txt +4 -0
  166. package/package.json +1 -1
  167. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  168. package/scripts/_cli/cmd_doctor.py +3 -2
  169. package/scripts/_cli/cmd_explain.py +1 -1
  170. package/scripts/_cli/cmd_versions.py +2 -2
  171. package/scripts/_cli/explain_last/inputs.py +11 -8
  172. package/scripts/_cli/explain_last/sections/inputs.py +1 -1
  173. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  174. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  175. package/scripts/_lib/agent_settings.py +54 -6
  176. package/scripts/_lib/agent_src.py +30 -0
  177. package/scripts/_lib/value_ladder.py +99 -2
  178. package/scripts/_lib/value_report.py +30 -16
  179. package/scripts/ai_council/modes.py +1 -1
  180. package/scripts/ai_council/session.py +5 -1
  181. package/scripts/audit_command_surface.py +7 -1
  182. package/scripts/audit_initial_context.py +26 -2
  183. package/scripts/check_gate_paths.py +117 -0
  184. package/scripts/check_references.py +51 -2
  185. package/scripts/check_skill_requires.py +143 -0
  186. package/scripts/check_test_coverage_diff.py +180 -0
  187. package/scripts/compile_router.py +5 -1
  188. package/scripts/condense.py +92 -4
  189. package/scripts/config/session_profiles.py +492 -0
  190. package/scripts/council_cli.py +5 -1
  191. package/scripts/first-run.sh +11 -11
  192. package/scripts/hook_manifest.yaml +15 -7
  193. package/scripts/hooks/dispatch_hook.py +8 -0
  194. package/scripts/install +14 -1
  195. package/scripts/install-hooks.sh +2 -1
  196. package/scripts/install.py +203 -433
  197. package/scripts/install_anthropic_key.sh +1 -1
  198. package/scripts/install_openai_key.sh +1 -1
  199. package/scripts/inventory_abstraction_budget.py +6 -1
  200. package/scripts/lint_agents_md.py +11 -4
  201. package/scripts/lint_discovery_vocabulary.py +5 -5
  202. package/scripts/lint_hook_concern_budget.py +5 -1
  203. package/scripts/lint_marketplace.py +18 -7
  204. package/scripts/lint_roadmap_ci_steps.py +5 -1
  205. package/scripts/lint_roadmap_complexity.py +5 -1
  206. package/scripts/lint_value_dashboard.py +1 -1
  207. package/scripts/mcp_server/prompts.py +5 -1
  208. package/scripts/prediction-pool/adapters/_schema.md +42 -0
  209. package/scripts/prediction-pool/adapters/kicktipp.yml +23 -0
  210. package/scripts/prediction-pool/poisson_sim.py +167 -0
  211. package/scripts/prediction-pool/pool_winsim.py +236 -0
  212. package/scripts/prediction-pool/score_ev.py +188 -0
  213. package/scripts/profile_staleness_hook.py +69 -0
  214. package/scripts/render_value_md.py +1 -0
  215. package/scripts/roadmap_progress_hook.py +56 -6
  216. package/scripts/schemas/agent-settings.schema.json +77 -0
  217. package/scripts/schemas/skill.schema.json +7 -0
  218. package/scripts/smoke_quickstart.py +7 -6
  219. package/scripts/sync_agent_settings.py +12 -5
  220. package/scripts/validate_agent_settings.py +124 -0
  221. package/scripts/validate_decision_engine.py +5 -1
  222. package/templates/minimal/.agent-settings.yml +1 -1
  223. package/dist/ui/assets/index-DVsyUMZe.js.map +0 -1
  224. package/scripts/measure_roadmap_trajectory.py +0 -112
  225. package/scripts/verify_roadmap_closure.py +0 -327
@@ -0,0 +1,492 @@
1
+ """Session-profile overlay — recommendation-bias MVP.
2
+
3
+ Implements the `runtime.active_packs` overlay locked in the
4
+ session-profile-activation roadmap (Phase 0 decisions, 2026-06-02):
5
+
6
+ * The overlay is an **ephemeral** list of pack ids written to
7
+ ``agents/settings/.agent-settings.local.yml`` (gitignored, deepest layer),
8
+ never the committed settings file. It is a runtime modulation of the
9
+ existing ``pack`` axis, not a fifth axis (ADR-010 addendum).
10
+ * Activation resolves a token (a ``session-profiles.yml`` alias OR a raw
11
+ pack id) to a seed set, **fails fast** if a seed pack is not installed,
12
+ then expands the transitive ``requires_hint`` closure from ``packs.yml``.
13
+ * Reads are **fail-open**: a corrupt / unparseable / schema-invalid overlay
14
+ is ignored and the full surface returns (the council's trust-boundary
15
+ requirement). Writes are **atomic** (tmp + ``os.replace``).
16
+ * Deactivation is **explicit** (``/profile deactivate``) — option (a). There
17
+ is no silent ``session_start`` reset (the registry-refresh Catch-22); the
18
+ hook only emits a staleness *notice*.
19
+
20
+ Surfacing rule (recommendation-bias): an artefact from the discovery
21
+ manifest is surfaced when it is **core-trust** (or unscoped) — always shown
22
+ — OR its ``packs`` intersect the active overlay. Execution is NOT gated.
23
+
24
+ Pure functions are unit-testable; the ``__main__`` CLI is what the
25
+ ``/profile`` command shells out to.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import json
31
+ import os
32
+ import sys
33
+ import tempfile
34
+ from dataclasses import dataclass, field
35
+ from pathlib import Path
36
+ from typing import Any
37
+
38
+ try: # lazy PyYAML, mirrors scripts/config/profiles.py
39
+ import yaml
40
+ except Exception: # pragma: no cover - yaml is a hard dep in practice
41
+ yaml = None # type: ignore
42
+
43
+ from scripts._lib import agent_settings
44
+
45
+ # --- Paths -----------------------------------------------------------------
46
+
47
+ PACKS_VOCAB_REL = "config/discovery/packs.yml"
48
+ ALIASES_REL = "config/discovery/session-profiles.yml"
49
+ DISCOVERY_MANIFEST_REL = "dist/discovery/discovery-manifest.json"
50
+
51
+ #: Dotted key the overlay lives under in the local settings file.
52
+ OVERLAY_SECTION = "runtime"
53
+ OVERLAY_KEY = "active_packs"
54
+
55
+ #: Trust levels that are ALWAYS surfaced regardless of the active overlay.
56
+ ALWAYS_TRUST_LEVELS = frozenset({"core"})
57
+
58
+
59
+ class SessionProfileError(ValueError):
60
+ """Raised for an unknown token or a not-installed pack (fail-fast)."""
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class ActivationResult:
65
+ active_packs: tuple[str, ...]
66
+ requested: tuple[str, ...]
67
+ closure_added: tuple[str, ...] = ()
68
+ notes: tuple[str, ...] = ()
69
+
70
+
71
+ @dataclass
72
+ class SurfaceResult:
73
+ active_packs: list[str]
74
+ shown: list[dict[str, Any]] = field(default_factory=list)
75
+ hidden: list[dict[str, Any]] = field(default_factory=list)
76
+
77
+
78
+ # --- Loaders ---------------------------------------------------------------
79
+
80
+ def _read_yaml(path: Path) -> Any:
81
+ if yaml is None or not path.exists():
82
+ return None
83
+ try:
84
+ with path.open(encoding="utf-8") as fh:
85
+ return yaml.safe_load(fh)
86
+ except Exception:
87
+ return None
88
+
89
+
90
+ def load_packs_vocab(repo_root: Path) -> dict[str, dict[str, Any]]:
91
+ """Return ``{pack_id: pack_dict}`` from ``packs.yml`` (empty on failure)."""
92
+ data = _read_yaml(repo_root / PACKS_VOCAB_REL)
93
+ if not isinstance(data, list):
94
+ return {}
95
+ out: dict[str, dict[str, Any]] = {}
96
+ for entry in data:
97
+ if isinstance(entry, dict) and entry.get("id"):
98
+ out[str(entry["id"])] = entry
99
+ return out
100
+
101
+
102
+ def load_aliases(repo_root: Path) -> dict[str, list[str]]:
103
+ """Return ``{alias: [pack_id, ...]}`` from ``session-profiles.yml``."""
104
+ data = _read_yaml(repo_root / ALIASES_REL)
105
+ if not isinstance(data, dict):
106
+ return {}
107
+ aliases = data.get("aliases")
108
+ if not isinstance(aliases, dict):
109
+ return {}
110
+ out: dict[str, list[str]] = {}
111
+ for name, packs in aliases.items():
112
+ if isinstance(packs, list):
113
+ out[str(name)] = [str(p) for p in packs]
114
+ return out
115
+
116
+
117
+ def installed_packs(repo_root: Path, settings: dict[str, Any] | None = None) -> set[str]:
118
+ """The set of pack ids treated as installed.
119
+
120
+ Source of truth: the top-level ``packs:`` block injected into the
121
+ settings file at install time. When absent (e.g. the maintainer repo,
122
+ or a base-only install) the **full vocabulary** is treated as available
123
+ — every pack's artefacts are present on disk there.
124
+ """
125
+ if settings is None:
126
+ settings = agent_settings.load_agent_settings(cwd=repo_root)
127
+ declared = settings.get("packs")
128
+ if isinstance(declared, list) and declared:
129
+ return {str(p) for p in declared}
130
+ return set(load_packs_vocab(repo_root).keys())
131
+
132
+
133
+ # --- Closure + token resolution -------------------------------------------
134
+
135
+ def expand_closure(seeds: list[str] | set[str], vocab: dict[str, dict[str, Any]]) -> list[str]:
136
+ """Transitive ``requires_hint`` closure of ``seeds``, sorted, deduped."""
137
+ seen: set[str] = set()
138
+ stack = list(seeds)
139
+ while stack:
140
+ pid = stack.pop()
141
+ if pid in seen:
142
+ continue
143
+ seen.add(pid)
144
+ entry = vocab.get(pid) or {}
145
+ for dep in entry.get("requires_hint") or []:
146
+ if dep not in seen:
147
+ stack.append(str(dep))
148
+ return sorted(seen)
149
+
150
+
151
+ def resolve_tokens(
152
+ tokens: list[str],
153
+ vocab: dict[str, dict[str, Any]],
154
+ aliases: dict[str, list[str]],
155
+ ) -> list[str]:
156
+ """Resolve activation tokens (alias names or pack ids) to a seed pack set.
157
+
158
+ Raises :class:`SessionProfileError` for a token that is neither a known
159
+ alias nor a known pack id.
160
+ """
161
+ seeds: set[str] = set()
162
+ for token in tokens:
163
+ if token in aliases:
164
+ seeds.update(aliases[token])
165
+ elif token in vocab:
166
+ seeds.add(token)
167
+ else:
168
+ known = sorted(set(aliases) | set(vocab))
169
+ raise SessionProfileError(
170
+ f"unknown profile/pack '{token}'. Known: {', '.join(known)}"
171
+ )
172
+ return sorted(seeds)
173
+
174
+
175
+ # --- Overlay read / write (fail-open read, atomic write) -------------------
176
+
177
+ def _overlay_path(repo_root: Path) -> Path:
178
+ return repo_root.joinpath(*agent_settings.LOCAL_PROJECT_SUBDIR, agent_settings.LOCAL_PROJECT_FILE)
179
+
180
+
181
+ def read_overlay(repo_root: Path) -> list[str]:
182
+ """Return the active pack list. **Fail-open**: any problem → ``[]``.
183
+
184
+ Schema: ``runtime.active_packs`` must be a list of strings. Anything
185
+ else (missing, wrong type, unparseable file) yields an empty list so a
186
+ corrupt overlay never hides the full surface.
187
+ """
188
+ data = _read_yaml(_overlay_path(repo_root))
189
+ if not isinstance(data, dict):
190
+ return []
191
+ runtime = data.get(OVERLAY_SECTION)
192
+ if not isinstance(runtime, dict):
193
+ return []
194
+ packs = runtime.get(OVERLAY_KEY)
195
+ if not isinstance(packs, list):
196
+ return []
197
+ return [str(p) for p in packs if isinstance(p, (str, int))]
198
+
199
+
200
+ def _write_local(repo_root: Path, data: dict[str, Any]) -> None:
201
+ """Atomic write of the whole local settings dict (tmp + os.replace)."""
202
+ path = _overlay_path(repo_root)
203
+ path.parent.mkdir(parents=True, exist_ok=True)
204
+ header = (
205
+ "# Per-machine local overrides (gitignored, deepest-winning layer).\n"
206
+ "# `runtime.active_packs` is the EPHEMERAL session-profile overlay —\n"
207
+ "# managed by `/profile`. Delete the key (or this file) to reset.\n"
208
+ )
209
+ body = yaml.safe_dump(data, sort_keys=False, default_flow_style=False) if yaml else ""
210
+ fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=".agent-settings.local.", suffix=".tmp")
211
+ try:
212
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
213
+ fh.write(header)
214
+ fh.write(body)
215
+ os.replace(tmp, path)
216
+ finally:
217
+ if os.path.exists(tmp):
218
+ os.unlink(tmp)
219
+
220
+
221
+ def set_overlay(repo_root: Path, packs: list[str]) -> None:
222
+ """Set ``runtime.active_packs`` to ``packs`` (atomic), preserving other keys."""
223
+ data = _read_yaml(_overlay_path(repo_root))
224
+ if not isinstance(data, dict):
225
+ data = {}
226
+ runtime = data.get(OVERLAY_SECTION)
227
+ if not isinstance(runtime, dict):
228
+ runtime = {}
229
+ if packs:
230
+ runtime[OVERLAY_KEY] = sorted(set(packs))
231
+ data[OVERLAY_SECTION] = runtime
232
+ else:
233
+ runtime.pop(OVERLAY_KEY, None)
234
+ if runtime:
235
+ data[OVERLAY_SECTION] = runtime
236
+ else:
237
+ data.pop(OVERLAY_SECTION, None)
238
+ _write_local(repo_root, data)
239
+
240
+
241
+ def clear_overlay(repo_root: Path) -> None:
242
+ set_overlay(repo_root, [])
243
+
244
+
245
+ # --- High-level operations -------------------------------------------------
246
+
247
+ def activate(repo_root: Path, tokens: list[str], settings: dict[str, Any] | None = None) -> ActivationResult:
248
+ """Resolve + validate + expand + write the overlay for ``tokens``.
249
+
250
+ Fail-fast (raises :class:`SessionProfileError`) when a resolved seed
251
+ pack is not installed.
252
+ """
253
+ vocab = load_packs_vocab(repo_root)
254
+ aliases = load_aliases(repo_root)
255
+ seeds = resolve_tokens(tokens, vocab, aliases)
256
+ inst = installed_packs(repo_root, settings)
257
+ missing = [p for p in seeds if p not in inst]
258
+ if missing:
259
+ raise SessionProfileError(
260
+ f"not installed: {', '.join(sorted(missing))}. "
261
+ f"Install the pack first (it is not in your settings `packs:` list)."
262
+ )
263
+ closure = expand_closure(seeds, vocab)
264
+ # Closure members must also be installed; drop + note any that are not
265
+ # (defensive — a misconfigured requires_hint should not block activation).
266
+ usable = [p for p in closure if p in inst]
267
+ dropped = [p for p in closure if p not in inst]
268
+ set_overlay(repo_root, usable)
269
+ notes = []
270
+ if dropped:
271
+ notes.append(f"closure deps not installed, skipped: {', '.join(sorted(dropped))}")
272
+ added = sorted(set(usable) - set(seeds))
273
+ return ActivationResult(
274
+ active_packs=tuple(sorted(usable)),
275
+ requested=tuple(tokens),
276
+ closure_added=tuple(added),
277
+ notes=tuple(notes),
278
+ )
279
+
280
+
281
+ def deactivate(repo_root: Path, tokens: list[str] | None = None) -> list[str]:
282
+ """Clear the overlay (no tokens) or remove the named packs from it.
283
+
284
+ Returns the resulting active pack list. With ``tokens``, only the named
285
+ packs *themselves* are removed from the flat active set — never their
286
+ transitive closure. A shared dependency therefore survives as long as it
287
+ is its own entry in the overlay (e.g. deactivating ``laravel`` while
288
+ ``php`` is active leaves both ``php`` and ``engineering-base`` in place).
289
+ This is the safe, predictable behaviour for a flat pack overlay: removing
290
+ a pack only ever *widens* the surface, never hides something a remaining
291
+ pack needs.
292
+ """
293
+ if not tokens:
294
+ clear_overlay(repo_root)
295
+ return []
296
+ vocab = load_packs_vocab(repo_root)
297
+ aliases = load_aliases(repo_root)
298
+ to_remove = set(resolve_tokens(tokens, vocab, aliases))
299
+ current = set(read_overlay(repo_root))
300
+ new_active = sorted(current - to_remove)
301
+ set_overlay(repo_root, new_active)
302
+ return new_active
303
+
304
+
305
+ # --- Surface filter (recommendation-bias) ----------------------------------
306
+
307
+ def load_manifest(repo_root: Path) -> list[dict[str, Any]]:
308
+ path = repo_root / DISCOVERY_MANIFEST_REL
309
+ if not path.exists():
310
+ return []
311
+ try:
312
+ data = json.loads(path.read_text(encoding="utf-8"))
313
+ except Exception:
314
+ return []
315
+ arts = data.get("artefacts")
316
+ return arts if isinstance(arts, list) else []
317
+
318
+
319
+ def is_always_shown(artefact: dict[str, Any]) -> bool:
320
+ """Core-trust or unscoped artefacts are always surfaced."""
321
+ packs = artefact.get("packs") or []
322
+ if not packs:
323
+ return True
324
+ level = (artefact.get("trust") or {}).get("level")
325
+ return level in ALWAYS_TRUST_LEVELS
326
+
327
+
328
+ def is_surfaced(artefact: dict[str, Any], active: set[str]) -> bool:
329
+ if not active:
330
+ return True # no overlay → everything surfaces
331
+ if is_always_shown(artefact):
332
+ return True
333
+ return bool(set(artefact.get("packs") or []) & active)
334
+
335
+
336
+ def compute_surface(
337
+ repo_root: Path,
338
+ category: str | None = None,
339
+ active: list[str] | None = None,
340
+ ) -> SurfaceResult:
341
+ """Split manifest artefacts into shown / hidden for the active overlay."""
342
+ if active is None:
343
+ active = read_overlay(repo_root)
344
+ active_set = set(active)
345
+ result = SurfaceResult(active_packs=sorted(active_set))
346
+ for art in load_manifest(repo_root):
347
+ if category and art.get("category") != category:
348
+ continue
349
+ if art.get("category") not in {"command", "skill"}:
350
+ continue
351
+ slim = {
352
+ "name": art.get("name"),
353
+ "category": art.get("category"),
354
+ "packs": art.get("packs") or [],
355
+ }
356
+ if is_surfaced(art, active_set):
357
+ result.shown.append(slim)
358
+ else:
359
+ result.hidden.append(slim)
360
+ return result
361
+
362
+
363
+ def stale_notice(repo_root: Path) -> str | None:
364
+ """Return the `session_start` staleness notice, or ``None`` if no overlay.
365
+
366
+ Implements option (a)'s companion: the overlay survives a restart, so on
367
+ a new session we *remind* (never silently reset).
368
+ """
369
+ active = read_overlay(repo_root)
370
+ if not active:
371
+ return None
372
+ return (
373
+ f"profile still active from a previous session: {', '.join(active)} "
374
+ f"— `/profile deactivate` to clear, `/profile show` for details."
375
+ )
376
+
377
+
378
+ # --- CLI -------------------------------------------------------------------
379
+
380
+ def _repo_root(arg: str | None) -> Path:
381
+ if arg:
382
+ return Path(arg).resolve()
383
+ found = agent_settings.find_project_root(Path.cwd())
384
+ return found or Path.cwd()
385
+
386
+
387
+ def main(argv: list[str] | None = None) -> int:
388
+ # Shared flags available both before AND after the subcommand.
389
+ common = argparse.ArgumentParser(add_help=False)
390
+ common.add_argument("--root", default=None, help="repo root (default: auto-detect)")
391
+ common.add_argument("--json", action="store_true", help="machine-readable output")
392
+
393
+ ap = argparse.ArgumentParser(
394
+ prog="session_profiles", description="Session-profile overlay manager.", parents=[common]
395
+ )
396
+ sub = ap.add_subparsers(dest="cmd", required=True)
397
+
398
+ p_act = sub.add_parser("activate", parents=[common], help="activate one or more profiles/packs")
399
+ p_act.add_argument("tokens", nargs="+")
400
+
401
+ p_de = sub.add_parser("deactivate", parents=[common], help="deactivate (clear, or named tokens)")
402
+ p_de.add_argument("tokens", nargs="*")
403
+
404
+ sub.add_parser("show", parents=[common], help="show active overlay + surface counts")
405
+ p_surf = sub.add_parser("surface", parents=[common], help="list shown/hidden artefacts")
406
+ p_surf.add_argument("--category", choices=["command", "skill"], default=None)
407
+ sub.add_parser("stale-notice", parents=[common], help="emit session_start staleness notice if any")
408
+
409
+ args = ap.parse_args(argv)
410
+ root = _repo_root(args.root)
411
+
412
+ try:
413
+ if args.cmd == "activate":
414
+ res = activate(root, args.tokens)
415
+ payload = {
416
+ "active_packs": list(res.active_packs),
417
+ "requested": list(res.requested),
418
+ "closure_added": list(res.closure_added),
419
+ "notes": list(res.notes),
420
+ }
421
+ if args.json:
422
+ print(json.dumps(payload))
423
+ else:
424
+ print(f"activated: {', '.join(res.active_packs) or '(none)'}")
425
+ if res.closure_added:
426
+ print(f" + closure: {', '.join(res.closure_added)}")
427
+ for n in res.notes:
428
+ print(f" note: {n}")
429
+ return 0
430
+
431
+ if args.cmd == "deactivate":
432
+ active = deactivate(root, args.tokens or None)
433
+ if args.json:
434
+ print(json.dumps({"active_packs": active}))
435
+ else:
436
+ print(f"active now: {', '.join(active) or '(none — full surface)'}")
437
+ return 0
438
+
439
+ if args.cmd == "show":
440
+ active = read_overlay(root)
441
+ surf = compute_surface(root, active=active)
442
+ cmds_shown = sum(1 for a in surf.shown if a["category"] == "command")
443
+ skills_shown = sum(1 for a in surf.shown if a["category"] == "skill")
444
+ if args.json:
445
+ print(json.dumps({
446
+ "active_packs": active,
447
+ "shown_total": len(surf.shown),
448
+ "hidden_total": len(surf.hidden),
449
+ "commands_shown": cmds_shown,
450
+ "skills_shown": skills_shown,
451
+ }))
452
+ else:
453
+ if not active:
454
+ print("no profile active — full surface (everything shown).")
455
+ else:
456
+ print(f"active packs: {', '.join(active)}")
457
+ print(f"surfaced: {cmds_shown} commands, {skills_shown} skills "
458
+ f"({len(surf.hidden)} hidden behind inactive packs)")
459
+ return 0
460
+
461
+ if args.cmd == "surface":
462
+ surf = compute_surface(root, category=args.category)
463
+ if args.json:
464
+ print(json.dumps({
465
+ "active_packs": surf.active_packs,
466
+ "shown": surf.shown,
467
+ "hidden": surf.hidden,
468
+ }))
469
+ else:
470
+ print(f"active: {', '.join(surf.active_packs) or '(none)'}")
471
+ print(f"shown ({len(surf.shown)}):")
472
+ for a in surf.shown:
473
+ print(f" + {a['category']}/{a['name']}")
474
+ print(f"hidden ({len(surf.hidden)}):")
475
+ for a in surf.hidden:
476
+ print(f" - {a['category']}/{a['name']} [{','.join(a['packs'])}]")
477
+ return 0
478
+
479
+ if args.cmd == "stale-notice":
480
+ notice = stale_notice(root)
481
+ if notice:
482
+ print(notice)
483
+ return 0
484
+ except SessionProfileError as exc:
485
+ print(f"error: {exc}", file=sys.stderr)
486
+ return 2
487
+
488
+ return 0
489
+
490
+
491
+ if __name__ == "__main__": # pragma: no cover
492
+ raise SystemExit(main())
@@ -18,12 +18,16 @@ import json
18
18
  import sys
19
19
  from dataclasses import asdict
20
20
  from pathlib import Path
21
+ try: # invocation-agnostic import (repo-root-on-path vs scripts-on-path)
22
+ from scripts._lib.agent_settings import project_settings_path
23
+ except ModuleNotFoundError: # pragma: no cover
24
+ from _lib.agent_settings import project_settings_path
21
25
  from typing import Any
22
26
 
23
27
  import yaml
24
28
 
25
29
  REPO_ROOT = Path(__file__).resolve().parents[1]
26
- SETTINGS_FILE = REPO_ROOT / ".agent-settings.yml"
30
+ SETTINGS_FILE = project_settings_path(REPO_ROOT)
27
31
  AI_COUNCIL_FILE = REPO_ROOT / "agents" / "settings" / ".ai-council.yml"
28
32
 
29
33
  # Canonical output dirs per ai-council § "Output path convention".
@@ -9,14 +9,14 @@ echo "Agent Config — First Run"
9
9
  echo ""
10
10
 
11
11
  # --- Profile detection ---
12
- # The YAML format stores `cost_profile` as a top-level scalar:
13
- # cost_profile: minimal
12
+ # The YAML format stores `rule_loading_tier` as a top-level scalar:
13
+ # rule_loading_tier: minimal
14
14
  # It may be unquoted or double-quoted after migration. We strip both.
15
- read_cost_profile() {
15
+ read_rule_loading_tier() {
16
16
  local file="$1"
17
- grep -E '^cost_profile:' "$file" 2>/dev/null \
17
+ grep -E '^rule_loading_tier:' "$file" 2>/dev/null \
18
18
  | head -n1 \
19
- | sed -E 's/^cost_profile:[[:space:]]*//' \
19
+ | sed -E 's/^rule_loading_tier:[[:space:]]*//' \
20
20
  | sed -E 's/^"(.*)"$/\1/' \
21
21
  | sed -E "s/^'(.*)'\$/\\1/" \
22
22
  | tr -d '[:space:]'
@@ -25,16 +25,16 @@ read_cost_profile() {
25
25
  if [ -f "$SETTINGS_FILE" ]; then
26
26
  echo "✅ Found $SETTINGS_FILE"
27
27
  echo ""
28
- CURRENT_PROFILE=$(read_cost_profile "$SETTINGS_FILE" || true)
28
+ CURRENT_PROFILE=$(read_rule_loading_tier "$SETTINGS_FILE" || true)
29
29
 
30
30
  if [ -n "${CURRENT_PROFILE:-}" ]; then
31
- echo "Active cost_profile: $CURRENT_PROFILE"
31
+ echo "Active rule_loading_tier: $CURRENT_PROFILE"
32
32
  else
33
- echo "No cost_profile configured yet."
33
+ echo "No rule_loading_tier configured yet."
34
34
  echo ""
35
35
  echo "Recommended: add this to $SETTINGS_FILE:"
36
36
  echo ""
37
- echo " cost_profile: minimal"
37
+ echo " rule_loading_tier: minimal"
38
38
  fi
39
39
  elif [ -f "$LEGACY_SETTINGS_FILE" ]; then
40
40
  echo "⚠️ Found legacy $LEGACY_SETTINGS_FILE (key=value format)."
@@ -47,7 +47,7 @@ else
47
47
  echo ""
48
48
  echo "Create one with:"
49
49
  echo ""
50
- echo " cost_profile: minimal"
50
+ echo " rule_loading_tier: minimal"
51
51
  echo ""
52
52
  fi
53
53
 
@@ -97,7 +97,7 @@ echo " minimal rules, skills, commands only"
97
97
  echo " balanced + runtime dispatcher"
98
98
  echo " full + experimental read-only tool adapters"
99
99
  echo ""
100
- echo "Change profile: edit cost_profile: <name> in $SETTINGS_FILE"
100
+ echo "Change profile: edit rule_loading_tier: <name> in $SETTINGS_FILE"
101
101
  echo "Profile details: docs/customization.md"
102
102
  echo "Getting started: docs/getting-started.md"
103
103
  echo ""
@@ -54,16 +54,24 @@ concerns:
54
54
  script: scripts/first_run_gate_hook.py
55
55
  args: []
56
56
  fail_closed: false
57
+ # session-profile-activation Phase 1 — emits a one-line staleness notice
58
+ # when a new session starts with a `runtime.active_packs` overlay carried
59
+ # over from a previous session. Never resets (option a, explicit
60
+ # /profile deactivate); never blocks.
61
+ profile-staleness:
62
+ script: scripts/profile_staleness_hook.py
63
+ args: []
64
+ fail_closed: false
57
65
 
58
66
  platforms:
59
67
  augment:
60
- session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff]
68
+ session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness]
61
69
  session_end: [chat-history]
62
70
  stop: [chat-history, verify-before-complete]
63
71
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff]
64
72
 
65
73
  claude:
66
- session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff]
74
+ session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness]
67
75
  session_end: [chat-history]
68
76
  stop: [chat-history, verify-before-complete]
69
77
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
@@ -84,7 +92,7 @@ platforms:
84
92
  # Decision matrix + upstream blockers tracked in
85
93
  # agents/settings/contexts/chat-history-platform-hooks.md § Cowork.
86
94
  cowork:
87
- session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff]
95
+ session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness]
88
96
  session_end: [chat-history]
89
97
  stop: [chat-history, verify-before-complete]
90
98
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
@@ -98,7 +106,7 @@ platforms:
98
106
  # IDE-only — CLI-only users fall back to /checkpoint per
99
107
  # agents/settings/contexts/chat-history-platform-hooks.md.
100
108
  cursor:
101
- session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff]
109
+ session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness]
102
110
  session_end: [chat-history]
103
111
  stop: [chat-history, verify-before-complete]
104
112
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
@@ -113,7 +121,7 @@ platforms:
113
121
  # both map to session_start. TaskCancel maps to stop because the
114
122
  # session is interrupted with partial state (mirrors Augment Stop).
115
123
  cline:
116
- session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff]
124
+ session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness]
117
125
  session_end: [chat-history]
118
126
  stop: [chat-history, verify-before-complete]
119
127
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
@@ -132,7 +140,7 @@ platforms:
132
140
  # surface to record verification commands; documented limitation).
133
141
  # minimal-safe-diff is omitted entirely on Windsurf for the same reason.
134
142
  windsurf:
135
- session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete]
143
+ session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness]
136
144
  stop: [chat-history, verify-before-complete]
137
145
  user_prompt_submit: [chat-history, verify-before-complete]
138
146
 
@@ -147,7 +155,7 @@ platforms:
147
155
  # turn-check semantics. AfterAgent fires when the agent loop ends
148
156
  # — this is our `stop` slot.
149
157
  gemini:
150
- session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff]
158
+ session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness]
151
159
  session_end: [chat-history]
152
160
  stop: [chat-history, verify-before-complete]
153
161
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
@@ -252,6 +252,13 @@ def _run_concern(concern: dict, envelope: dict) -> tuple[int, str, str, int]:
252
252
  # dispatcher handles downstream.
253
253
  return (3, f"{concern.get('name')}: script missing: {script}", "", 0)
254
254
 
255
+ # Pass the package root so concerns can locate package-shipped
256
+ # distributed content (e.g. the roadmap-progress regenerator) when a
257
+ # global-only consumer repo (ADR-020) carries no project-local copy.
258
+ # REPO_ROOT is the dispatcher's own resolved package root — the same
259
+ # anchor it used to find this concern script above.
260
+ concern_env = {**os.environ, "AGENT_CONFIG_PACKAGE_ROOT": str(REPO_ROOT)}
261
+
255
262
  started = time.monotonic()
256
263
  try:
257
264
  proc = subprocess.run(
@@ -260,6 +267,7 @@ def _run_concern(concern: dict, envelope: dict) -> tuple[int, str, str, int]:
260
267
  capture_output=True,
261
268
  text=True,
262
269
  cwd=workspace,
270
+ env=concern_env,
263
271
  timeout=30,
264
272
  check=False,
265
273
  )
package/scripts/install CHANGED
@@ -232,7 +232,20 @@ prompt_tools() {
232
232
  echo " ✅ Selected: $TOOLS"
233
233
  }
234
234
 
235
- if ! $MINIMAL && ! $TOOLS_EXPLICIT && ! $YES && ! $QUIET && ! $LIST_TOOLS && [[ -t 0 && -t 1 ]]; then
235
+ # When an interactive global install will hand off to the browser wizard,
236
+ # the wizard is the single tool-selection surface — skip the terminal
237
+ # picker so it does not pre-empt (and, via TOOLS_EXPLICIT, suppress) the
238
+ # GUI. Mirrors install.py::_wizard_should_launch minus the --no-ui flag,
239
+ # which the bash orchestrator never receives (it errors on unknown args).
240
+ # Headless paths (no TTY / CI / AGENT_CONFIG_NO_UI) still get the picker.
241
+ wizard_will_handle_tools=false
242
+ if $GLOBAL && [[ -t 0 && -t 1 && -z "${CI:-}" ]] \
243
+ && { [[ -z "${AGENT_CONFIG_NO_UI:-}" ]] || [[ "${AGENT_CONFIG_NO_UI:-}" == "0" ]]; }; then
244
+ wizard_will_handle_tools=true
245
+ fi
246
+
247
+ if ! $MINIMAL && ! $TOOLS_EXPLICIT && ! $YES && ! $QUIET && ! $LIST_TOOLS \
248
+ && [[ -t 0 && -t 1 ]] && ! $wizard_will_handle_tools; then
236
249
  prompt_tools
237
250
  TOOLS_EXPLICIT=true
238
251
  fi