@event4u/agent-config 5.7.0 → 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.
- package/.agent-src/commands/agent-handoff.md +1 -1
- package/.agent-src/commands/agent-status.md +1 -1
- package/.agent-src/commands/agents/audit.md +1 -1
- package/.agent-src/commands/agents/init.md +1 -1
- package/.agent-src/commands/agents/user/accept.md +3 -3
- package/.agent-src/commands/agents/user/init.md +4 -4
- package/.agent-src/commands/agents/user/show.md +3 -3
- package/.agent-src/commands/agents/user/update.md +3 -3
- package/.agent-src/commands/agents/user.md +1 -1
- package/.agent-src/commands/agents.md +1 -1
- package/.agent-src/commands/analytics/prune.md +1 -1
- package/.agent-src/commands/analytics/show.md +1 -1
- package/.agent-src/commands/analytics.md +1 -1
- package/.agent-src/commands/bug-fix.md +1 -1
- package/.agent-src/commands/challenge-me.md +1 -1
- package/.agent-src/commands/chat-history/import.md +1 -1
- package/.agent-src/commands/chat-history/learn.md +1 -1
- package/.agent-src/commands/chat-history/show.md +1 -1
- package/.agent-src/commands/chat-history.md +1 -1
- package/.agent-src/commands/check-current-md.md +1 -1
- package/.agent-src/commands/condense.md +1 -1
- package/.agent-src/commands/context.md +1 -1
- package/.agent-src/commands/cost-report.md +1 -1
- package/.agent-src/commands/council.md +3 -3
- package/.agent-src/commands/create-pr/description-only.md +1 -1
- package/.agent-src/commands/create-pr.md +1 -1
- package/.agent-src/commands/e2e-heal.md +1 -1
- package/.agent-src/commands/e2e-plan.md +1 -1
- package/.agent-src/commands/feature.md +1 -1
- package/.agent-src/commands/fix/ci.md +1 -1
- package/.agent-src/commands/fix/portability.md +1 -1
- package/.agent-src/commands/fix/pr-bot-comments.md +1 -1
- package/.agent-src/commands/fix/pr-comments.md +1 -1
- package/.agent-src/commands/fix/pr-developer-comments.md +1 -1
- package/.agent-src/commands/fix/refs.md +1 -1
- package/.agent-src/commands/fix/seeder.md +1 -1
- package/.agent-src/commands/fix.md +1 -1
- package/.agent-src/commands/judge.md +1 -1
- package/.agent-src/commands/knowledge/cross-repo.md +1 -1
- package/.agent-src/commands/knowledge/forget.md +1 -1
- package/.agent-src/commands/knowledge/ingest.md +1 -1
- package/.agent-src/commands/knowledge/list.md +1 -1
- package/.agent-src/commands/knowledge.md +1 -1
- package/.agent-src/commands/memory/add.md +1 -1
- package/.agent-src/commands/memory/learn-low-impact.md +1 -1
- package/.agent-src/commands/memory/load.md +1 -1
- package/.agent-src/commands/memory/mine-session.md +1 -1
- package/.agent-src/commands/memory/promote.md +1 -1
- package/.agent-src/commands/memory/propose.md +1 -1
- package/.agent-src/commands/memory.md +1 -1
- package/.agent-src/commands/mode.md +1 -1
- package/.agent-src/commands/optimize/agents-dir.md +1 -1
- package/.agent-src/commands/optimize/augmentignore.md +1 -1
- package/.agent-src/commands/optimize/rtk.md +1 -1
- package/.agent-src/commands/optimize/skills.md +1 -1
- package/.agent-src/commands/optimize.md +1 -1
- package/.agent-src/commands/orchestrate.md +1 -1
- package/.agent-src/commands/override/create.md +1 -1
- package/.agent-src/commands/override/manage.md +1 -1
- package/.agent-src/commands/override.md +1 -1
- package/.agent-src/commands/package-reset.md +1 -1
- package/.agent-src/commands/prediction-pool.md +31 -12
- package/.agent-src/commands/profile/activate.md +81 -0
- package/.agent-src/commands/profile/deactivate.md +68 -0
- package/.agent-src/commands/profile/show.md +70 -0
- package/.agent-src/commands/profile.md +68 -0
- package/.agent-src/commands/project-health.md +1 -1
- package/.agent-src/commands/quality-fix.md +1 -1
- package/.agent-src/commands/roadmap/process-full.md +1 -1
- package/.agent-src/commands/roadmap/process-phase.md +1 -1
- package/.agent-src/commands/roadmap/process-step.md +1 -1
- package/.agent-src/commands/roadmap.md +1 -1
- package/.agent-src/commands/set-cost-profile.md +1 -1
- package/.agent-src/commands/skill/preview.md +3 -3
- package/.agent-src/commands/skill.md +1 -1
- package/.agent-src/commands/skills/discover.md +1 -1
- package/.agent-src/commands/skills.md +1 -1
- package/.agent-src/commands/sync-agent-settings.md +1 -1
- package/.agent-src/commands/sync-gitignore/fix.md +1 -1
- package/.agent-src/commands/sync-gitignore.md +1 -1
- package/.agent-src/commands/update-form-request-messages.md +1 -1
- package/.agent-src/skills/check-refs/SKILL.md +1 -1
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +1 -1
- package/.agent-src/skills/git-workflow/SKILL.md +1 -1
- package/.agent-src/skills/jira-integration/SKILL.md +1 -1
- package/.agent-src/skills/markitdown/SKILL.md +1 -1
- package/.agent-src/skills/prediction-pool-optimizer/SKILL.md +195 -77
- package/.agent-src/skills/prediction-pool-optimizer/evals/triggers.json +3 -1
- package/.agent-src/skills/prediction-pool-optimizer/reference/ev-fixtures.md +111 -16
- package/.agent-src/skills/prediction-pool-optimizer/reference/odds-and-bonus.md +109 -0
- package/.agent-src/skills/rtk-output-filtering/SKILL.md +1 -1
- package/.agent-src/skills/script-writing/SKILL.md +1 -1
- package/.agent-src/skills/token-optimizer/SKILL.md +1 -1
- package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -1
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +52 -5
- package/.claude-plugin/marketplace.json +370 -366
- package/CHANGELOG.md +60 -0
- package/README.md +2 -2
- package/config/discovery/session-profiles.yml +37 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +183 -95
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +9 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +8 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +7 -3
- package/docs/contracts/command-clusters.md +2 -0
- package/docs/contracts/session-profile-overlay.md +120 -0
- package/docs/customization.md +26 -0
- package/docs/decisions/ADR-010-profile-pack-preset-boundary.md +36 -0
- package/docs/decisions/ADR-038-canonical-settings-path.md +66 -0
- package/docs/decisions/ADR-039-claude-skills-untracked.md +139 -0
- package/docs/decisions/INDEX.md +2 -0
- package/docs/development.md +12 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +8 -2
- package/docs/skills-catalog.md +5 -1
- package/llms.txt +4 -0
- package/package.json +1 -1
- package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/scripts/_cli/cmd_doctor.py +3 -2
- package/scripts/_cli/cmd_versions.py +2 -2
- package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
- package/scripts/_lib/agent_settings.py +52 -5
- package/scripts/_lib/agent_src.py +30 -0
- package/scripts/ai_council/session.py +5 -1
- package/scripts/audit_command_surface.py +7 -1
- package/scripts/audit_initial_context.py +10 -2
- package/scripts/check_gate_paths.py +117 -0
- package/scripts/check_references.py +51 -2
- package/scripts/check_test_coverage_diff.py +180 -0
- package/scripts/compile_router.py +5 -1
- package/scripts/condense.py +79 -2
- package/scripts/config/session_profiles.py +492 -0
- package/scripts/council_cli.py +5 -1
- package/scripts/hook_manifest.yaml +15 -7
- package/scripts/hooks/dispatch_hook.py +8 -0
- package/scripts/install-hooks.sh +2 -1
- package/scripts/install.py +76 -5
- package/scripts/inventory_abstraction_budget.py +6 -1
- package/scripts/lint_agents_md.py +11 -4
- package/scripts/lint_hook_concern_budget.py +5 -1
- package/scripts/lint_marketplace.py +18 -7
- package/scripts/lint_roadmap_ci_steps.py +5 -1
- package/scripts/lint_roadmap_complexity.py +5 -1
- package/scripts/mcp_server/prompts.py +5 -1
- package/scripts/prediction-pool/pool_winsim.py +236 -0
- package/scripts/prediction-pool/score_ev.py +188 -0
- package/scripts/profile_staleness_hook.py +69 -0
- package/scripts/roadmap_progress_hook.py +56 -6
- package/scripts/smoke_quickstart.py +3 -2
- package/scripts/sync_agent_settings.py +8 -3
- package/scripts/validate_agent_settings.py +5 -1
- package/scripts/validate_decision_engine.py +5 -1
- package/scripts/measure_roadmap_trajectory.py +0 -112
- 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())
|
package/scripts/council_cli.py
CHANGED
|
@@ -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
|
|
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".
|
|
@@ -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-hooks.sh
CHANGED
|
@@ -56,7 +56,8 @@ echo "✅ Pre-push hook installed."
|
|
|
56
56
|
cat > "$HOOKS_DIR/pre-commit" << 'EOF'
|
|
57
57
|
#!/usr/bin/env bash
|
|
58
58
|
# Pre-commit hook: verify .claude-plugin/marketplace.json lists every skill
|
|
59
|
-
# that exists on disk under .
|
|
59
|
+
# that exists on disk under the committed skill sources (.agent-src/skills/
|
|
60
|
+
# + .claude-plugin/skills/), AND verify
|
|
60
61
|
# agents/roadmaps-progress.md is in sync with the current state of
|
|
61
62
|
# agents/roadmaps/ (roadmap-progress-sync Iron Law).
|
|
62
63
|
|