@event4u/agent-config 5.9.0 → 5.10.1
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +38 -234
- package/README.md +13 -6
- package/config/agent-settings.template.yml +17 -0
- package/config/gitignore-block.txt +7 -0
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/server/schemas/settings.js +4 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/index-DcAWIwwY.js +40 -0
- package/dist/ui/assets/index-DcAWIwwY.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/archive/CHANGELOG-pre-5.9.0.md +270 -0
- package/docs/decisions/ADR-040-execution-model-projection-time-filtering.md +244 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/distribution/registries.md +1 -0
- package/docs/profiles.md +8 -0
- package/docs/wizard.md +25 -8
- package/package.json +1 -1
- package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/scripts/_cli/cmd_refresh.py +7 -1
- package/scripts/_cli/cmd_upgrade.py +31 -1
- package/scripts/_dispatch.bash +11 -0
- package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
- package/scripts/_lib/cli_wrapper.py +64 -0
- package/scripts/ai-video/lib/probe-audio.sh +20 -5
- package/scripts/hook_manifest.yaml +16 -7
- package/scripts/profile_use.py +125 -0
- package/scripts/wrapper_freshness_hook.py +86 -0
- package/dist/ui/assets/index-5lFqAKL0.js +0 -40
- package/dist/ui/assets/index-5lFqAKL0.js.map +0 -1
|
@@ -84,7 +84,7 @@ def _is_source_repo(project_root: Path) -> bool:
|
|
|
84
84
|
def _refresh_project(project_root: Path, out, err) -> int:
|
|
85
85
|
# Imported lazily: scripts.install is large and only needed for --project.
|
|
86
86
|
from scripts import install as installer
|
|
87
|
-
from scripts._lib import installed_lock
|
|
87
|
+
from scripts._lib import cli_wrapper, installed_lock
|
|
88
88
|
|
|
89
89
|
if _is_source_repo(project_root):
|
|
90
90
|
print("ℹ️ refresh --project skipped: this is the agent-config package "
|
|
@@ -114,6 +114,12 @@ def _refresh_project(project_root: Path, out, err) -> int:
|
|
|
114
114
|
encoding="utf-8")
|
|
115
115
|
print(f"✅ overrides scaffold: {overrides}", file=out)
|
|
116
116
|
|
|
117
|
+
# Re-stamp the ``./agent-config`` wrapper from the canonical template so
|
|
118
|
+
# an older, fallback-less wrapper cannot linger and break the hooks.
|
|
119
|
+
wrapper = cli_wrapper.install_cli_wrapper(project_root)
|
|
120
|
+
if wrapper is not None:
|
|
121
|
+
print(f"✅ ./agent-config wrapper refreshed: {wrapper}", file=out)
|
|
122
|
+
|
|
117
123
|
rc = _sync_gitignore(project_root, out, err)
|
|
118
124
|
if rc != 0:
|
|
119
125
|
return rc
|
|
@@ -8,12 +8,16 @@ goal: it installs the latest published package globally and re-runs the
|
|
|
8
8
|
global install so new skills / rules / hooks reach every consumer at
|
|
9
9
|
once.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Side effects, in order:
|
|
12
12
|
|
|
13
13
|
1. ``npm install -g @event4u/agent-config@latest`` — refresh the global
|
|
14
14
|
binary on PATH (the binary the Claude plugin hook resolves).
|
|
15
15
|
2. ``agent-config global`` (→ ``install.py --global``) — refresh the
|
|
16
16
|
global root (``~/.event4u/agent-config/``) + regenerate plugin hooks.
|
|
17
|
+
3. If run from inside a consumer project that already has a
|
|
18
|
+
``./agent-config`` wrapper, re-stamp that wrapper from the canonical
|
|
19
|
+
template so an older, fallback-less copy cannot linger and break the
|
|
20
|
+
hooks. Skipped in the source repo and never creates a wrapper.
|
|
17
21
|
|
|
18
22
|
The **Claude marketplace plugin** updates on Claude Code's own cadence,
|
|
19
23
|
independent of npm; ``upgrade`` cannot drive it. ``agent-config doctor``
|
|
@@ -72,12 +76,36 @@ def _agent_config_bin() -> str:
|
|
|
72
76
|
Path(__file__).resolve().parents[2] / "agent-config")
|
|
73
77
|
|
|
74
78
|
|
|
79
|
+
def _maybe_refresh_project_wrapper(project_root: Path, out, err) -> None:
|
|
80
|
+
"""Re-stamp the project-local ``./agent-config`` wrapper after a global
|
|
81
|
+
upgrade — but only when the upgrade was run from inside a consumer
|
|
82
|
+
project that *already* has a wrapper. Never creates one (that is an
|
|
83
|
+
install action, out of scope), never touches the source repo.
|
|
84
|
+
|
|
85
|
+
This closes the gap where ``upgrade`` refreshed the global binary while
|
|
86
|
+
an older, fallback-less project wrapper kept breaking the Claude hooks.
|
|
87
|
+
"""
|
|
88
|
+
from scripts._lib import cli_wrapper
|
|
89
|
+
from scripts._cli.cmd_refresh import _is_source_repo
|
|
90
|
+
|
|
91
|
+
if _is_source_repo(project_root):
|
|
92
|
+
return
|
|
93
|
+
if not (project_root / "agent-config").is_file():
|
|
94
|
+
return # not a consumer project root (or never installed) — leave it
|
|
95
|
+
if not cli_wrapper.needs_refresh(project_root):
|
|
96
|
+
return
|
|
97
|
+
wrapper = cli_wrapper.install_cli_wrapper(project_root)
|
|
98
|
+
if wrapper is not None:
|
|
99
|
+
print(f"✅ refreshed stale project wrapper: {wrapper}", file=out)
|
|
100
|
+
|
|
101
|
+
|
|
75
102
|
def main(
|
|
76
103
|
argv: Optional[list[str]] = None,
|
|
77
104
|
*,
|
|
78
105
|
runner: Runner = _default_runner,
|
|
79
106
|
fetcher=update_check.fetch_latest_from_npm,
|
|
80
107
|
installed: Optional[str] = None,
|
|
108
|
+
project_root: Optional[Path] = None,
|
|
81
109
|
out=sys.stdout,
|
|
82
110
|
err=sys.stderr,
|
|
83
111
|
) -> int:
|
|
@@ -128,6 +156,8 @@ def main(
|
|
|
128
156
|
f"{' '.join(cmd)}", file=err)
|
|
129
157
|
return 1
|
|
130
158
|
|
|
159
|
+
_maybe_refresh_project_wrapper(project_root or Path.cwd(), out, err)
|
|
160
|
+
|
|
131
161
|
print("✅ agent-config upgraded. Run `agent-config doctor` to verify "
|
|
132
162
|
"PATH + plugin parity.", file=out)
|
|
133
163
|
return 0
|
package/scripts/_dispatch.bash
CHANGED
|
@@ -782,6 +782,16 @@ cmd_council() {
|
|
|
782
782
|
exec env PYTHONPATH="$PACKAGE_ROOT" python3 "$script" "$sub" "$@"
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
+
# `use --profile=<id>` — switch the active experience/profile. Writes
|
|
786
|
+
# profile.id into the canonical .agent-settings.yml; the explicit
|
|
787
|
+
# profile-switch seam named by ADR-040 (road-to-6.0.0-a Step 8).
|
|
788
|
+
cmd_use() {
|
|
789
|
+
require_python3
|
|
790
|
+
local script
|
|
791
|
+
script="$(resolve_script "scripts/profile_use.py")" || return 1
|
|
792
|
+
exec env PYTHONPATH="$PACKAGE_ROOT" python3 "$script" "$@"
|
|
793
|
+
}
|
|
794
|
+
|
|
785
795
|
# `agent-config update` — flip the agent_config_version pin in
|
|
786
796
|
# .agent-settings.yml. See scripts/_cli/cmd_update.py (P3.1 of
|
|
787
797
|
# road-to-portable-runtime-and-update-check.md).
|
|
@@ -928,6 +938,7 @@ main() {
|
|
|
928
938
|
mcp:check) cmd_mcp_check "$@" ;;
|
|
929
939
|
mcp:setup) cmd_mcp_setup "$@" ;;
|
|
930
940
|
mcp:run) cmd_mcp_run "$@" ;;
|
|
941
|
+
use) cmd_use "$@" ;;
|
|
931
942
|
roadmap:progress) cmd_roadmap_progress "$@" ;;
|
|
932
943
|
roadmap:progress-check) cmd_roadmap_progress_check "$@" ;;
|
|
933
944
|
hooks:install) cmd_hooks_install "$@" ;;
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Install / refresh the consumer-facing ``./agent-config`` wrapper.
|
|
2
|
+
|
|
3
|
+
Python counterpart to ``install_cli_wrapper`` in ``scripts/install.sh``.
|
|
4
|
+
The wrapper is gitignored and meant to be regenerated on every install,
|
|
5
|
+
but the normal update cadence (``upgrade``, ``refresh --project``) never
|
|
6
|
+
re-ran the bash installer — so an older, fallback-less wrapper could
|
|
7
|
+
linger in a consumer project and break every Claude hook (the hook
|
|
8
|
+
resolves the master CLI *through* this wrapper). These helpers let the
|
|
9
|
+
update commands re-stamp the wrapper from the canonical template.
|
|
10
|
+
|
|
11
|
+
The template is the single source of truth (``templates/agent-config-wrapper.sh``);
|
|
12
|
+
the installer copies it verbatim with no substitution, so refreshing is a
|
|
13
|
+
plain copy + ``chmod``.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import shutil
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
# scripts/_lib/cli_wrapper.py → parents[2] is the package root.
|
|
22
|
+
_PACKAGE_ROOT = Path(__file__).resolve().parents[2]
|
|
23
|
+
_TEMPLATE = _PACKAGE_ROOT / "templates" / "agent-config-wrapper.sh"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def template_path() -> Path:
|
|
27
|
+
"""Absolute path to the canonical wrapper template."""
|
|
28
|
+
return _TEMPLATE
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _target(project_root: Path) -> Path:
|
|
32
|
+
return Path(project_root) / "agent-config"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def needs_refresh(project_root: Path) -> bool:
|
|
36
|
+
"""True when the project wrapper is missing or differs from the template.
|
|
37
|
+
|
|
38
|
+
Returns ``False`` when the template itself is unavailable (corrupt /
|
|
39
|
+
maintainer-only checkout) — there is nothing to refresh *to*.
|
|
40
|
+
"""
|
|
41
|
+
if not _TEMPLATE.is_file():
|
|
42
|
+
return False
|
|
43
|
+
target = _target(project_root)
|
|
44
|
+
if not target.is_file():
|
|
45
|
+
return True
|
|
46
|
+
try:
|
|
47
|
+
return target.read_text(encoding="utf-8") != _TEMPLATE.read_text(
|
|
48
|
+
encoding="utf-8")
|
|
49
|
+
except OSError:
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def install_cli_wrapper(project_root: Path) -> Optional[Path]:
|
|
54
|
+
"""Copy the canonical wrapper template to ``<project_root>/agent-config``.
|
|
55
|
+
|
|
56
|
+
Returns the written target path, or ``None`` when the template is
|
|
57
|
+
missing (corrupt package / maintainer checkout without templates).
|
|
58
|
+
"""
|
|
59
|
+
if not _TEMPLATE.is_file():
|
|
60
|
+
return None
|
|
61
|
+
target = _target(project_root)
|
|
62
|
+
shutil.copyfile(_TEMPLATE, target)
|
|
63
|
+
target.chmod(0o755)
|
|
64
|
+
return target
|
|
@@ -36,6 +36,14 @@
|
|
|
36
36
|
# 2 usage / file missing
|
|
37
37
|
# 3 required tool missing (ffprobe / ffmpeg)
|
|
38
38
|
# 4 no audio stream in the file
|
|
39
|
+
#
|
|
40
|
+
# Runtime requirements (trust boundary — AI-council note 2026-06-02):
|
|
41
|
+
# - ffmpeg + ffprobe on PATH (enforced, exit 3).
|
|
42
|
+
# - POSIX awk. Both GNU awk (CI/Linux) and BSD awk (macOS) are supported;
|
|
43
|
+
# the window arrays are passed via ENVIRON, not -v, because BSD awk
|
|
44
|
+
# rejects literal newlines in a -v value. The honesty invariant
|
|
45
|
+
# (interval <=> warning) is regression-guarded by
|
|
46
|
+
# tests/test_probe_audio.py::test_corpus_sweep_honesty_invariant.
|
|
39
47
|
|
|
40
48
|
set -euo pipefail
|
|
41
49
|
|
|
@@ -99,7 +107,9 @@ sil_bounds="$(ffmpeg -hide_banner -nostats -i "${song}" \
|
|
|
99
107
|
/silence_start/ { for(i=1;i<=NF;i++) if($i=="silence_start:") s=$(i+1) }
|
|
100
108
|
/silence_end/ { for(i=1;i<=NF;i++) if($i=="silence_end:") { e=$(i+1); printf "%.3f\n", (s+e)/2 } }
|
|
101
109
|
' 2>/dev/null || true)"
|
|
102
|
-
|
|
110
|
+
# Trailing \n so `wc -l` counts the last line: command substitution strips
|
|
111
|
+
# the trailing newline, and two boundaries without it count as one.
|
|
112
|
+
n_sil="$(printf '%s\n' "${sil_bounds}" | sed '/^$/d' | wc -l | tr -d ' ')"
|
|
103
113
|
|
|
104
114
|
# --- 4. choose method + build section boundaries ------------------------
|
|
105
115
|
# A method needs >= 3 sections (>= 2 internal boundaries) to count as
|
|
@@ -125,7 +135,7 @@ if [ -z "${method}" ]; then
|
|
|
125
135
|
prev=en[k]
|
|
126
136
|
}
|
|
127
137
|
}')"
|
|
128
|
-
n_rms="$(printf '%s' "${rms_bounds}" | sed '/^$/d' | wc -l | tr -d ' ')"
|
|
138
|
+
n_rms="$(printf '%s\n' "${rms_bounds}" | sed '/^$/d' | wc -l | tr -d ' ')"
|
|
129
139
|
if [ "${n_rms}" -ge 2 ]; then
|
|
130
140
|
method="rms"
|
|
131
141
|
boundaries="$(printf '%s\n' "${rms_bounds}" | sed '/^$/d' | sort -n | uniq)"
|
|
@@ -145,10 +155,15 @@ fi
|
|
|
145
155
|
# = mean of the RMS windows whose start falls inside it.
|
|
146
156
|
printf '%s' "${boundaries}" \
|
|
147
157
|
| sed '/^$/d' \
|
|
148
|
-
|
|
|
149
|
-
|
|
158
|
+
| _PROBE_WINS="$(printf '%b' "${win_starts}")" _PROBE_ENS="$(printf '%b' "${win_energy}")" \
|
|
159
|
+
awk -v d="${duration}" -v method="${method}" -v warning="${warning}" '
|
|
150
160
|
BEGIN {
|
|
151
|
-
|
|
161
|
+
# Read the window arrays from the environment, not -v: BSD awk (macOS)
|
|
162
|
+
# rejects literal newlines inside a -v value ("newline in string"),
|
|
163
|
+
# while GNU awk (CI/Linux) tolerates them. ENVIRON is POSIX and
|
|
164
|
+
# portable across both. (Surfaced 2026-06-02 once ffmpeg landed on a
|
|
165
|
+
# macOS authoring host — CI on gawk never hit it.)
|
|
166
|
+
nw=split(ENVIRON["_PROBE_WINS"], ws, "\n"); split(ENVIRON["_PROBE_ENS"], es, "\n")
|
|
152
167
|
# build edges
|
|
153
168
|
ne=0; edges[ne++]=0
|
|
154
169
|
}
|
|
@@ -62,16 +62,25 @@ concerns:
|
|
|
62
62
|
script: scripts/profile_staleness_hook.py
|
|
63
63
|
args: []
|
|
64
64
|
fail_closed: false
|
|
65
|
+
# Defense-in-depth twin of the update-command wrapper refresh
|
|
66
|
+
# (cmd_upgrade / cmd_refresh). On session_start, re-stamps a stale
|
|
67
|
+
# project-local ./agent-config wrapper from the canonical template so
|
|
68
|
+
# an outdated, fallback-less copy cannot keep breaking the hooks.
|
|
69
|
+
# Never creates a wrapper, never touches the source repo, fail-open.
|
|
70
|
+
wrapper-freshness:
|
|
71
|
+
script: scripts/wrapper_freshness_hook.py
|
|
72
|
+
args: []
|
|
73
|
+
fail_closed: false
|
|
65
74
|
|
|
66
75
|
platforms:
|
|
67
76
|
augment:
|
|
68
|
-
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness]
|
|
77
|
+
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness]
|
|
69
78
|
session_end: [chat-history]
|
|
70
79
|
stop: [chat-history, verify-before-complete]
|
|
71
80
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff]
|
|
72
81
|
|
|
73
82
|
claude:
|
|
74
|
-
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness]
|
|
83
|
+
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness]
|
|
75
84
|
session_end: [chat-history]
|
|
76
85
|
stop: [chat-history, verify-before-complete]
|
|
77
86
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
@@ -92,7 +101,7 @@ platforms:
|
|
|
92
101
|
# Decision matrix + upstream blockers tracked in
|
|
93
102
|
# agents/settings/contexts/chat-history-platform-hooks.md § Cowork.
|
|
94
103
|
cowork:
|
|
95
|
-
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness]
|
|
104
|
+
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness]
|
|
96
105
|
session_end: [chat-history]
|
|
97
106
|
stop: [chat-history, verify-before-complete]
|
|
98
107
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
@@ -106,7 +115,7 @@ platforms:
|
|
|
106
115
|
# IDE-only — CLI-only users fall back to /checkpoint per
|
|
107
116
|
# agents/settings/contexts/chat-history-platform-hooks.md.
|
|
108
117
|
cursor:
|
|
109
|
-
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness]
|
|
118
|
+
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness]
|
|
110
119
|
session_end: [chat-history]
|
|
111
120
|
stop: [chat-history, verify-before-complete]
|
|
112
121
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
@@ -121,7 +130,7 @@ platforms:
|
|
|
121
130
|
# both map to session_start. TaskCancel maps to stop because the
|
|
122
131
|
# session is interrupted with partial state (mirrors Augment Stop).
|
|
123
132
|
cline:
|
|
124
|
-
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness]
|
|
133
|
+
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness]
|
|
125
134
|
session_end: [chat-history]
|
|
126
135
|
stop: [chat-history, verify-before-complete]
|
|
127
136
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
@@ -140,7 +149,7 @@ platforms:
|
|
|
140
149
|
# surface to record verification commands; documented limitation).
|
|
141
150
|
# minimal-safe-diff is omitted entirely on Windsurf for the same reason.
|
|
142
151
|
windsurf:
|
|
143
|
-
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness]
|
|
152
|
+
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness]
|
|
144
153
|
stop: [chat-history, verify-before-complete]
|
|
145
154
|
user_prompt_submit: [chat-history, verify-before-complete]
|
|
146
155
|
|
|
@@ -155,7 +164,7 @@ platforms:
|
|
|
155
164
|
# turn-check semantics. AfterAgent fires when the agent loop ends
|
|
156
165
|
# — this is our `stop` slot.
|
|
157
166
|
gemini:
|
|
158
|
-
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness]
|
|
167
|
+
session_start: [chat-history, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness]
|
|
159
168
|
session_end: [chat-history]
|
|
160
169
|
stop: [chat-history, verify-before-complete]
|
|
161
170
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""`agent-config use --profile=<id>` — switch the active experience.
|
|
3
|
+
|
|
4
|
+
The explicit profile-switch entry point named by the Execution-Model ADR
|
|
5
|
+
(`docs/decisions/ADR-040-execution-model-projection-time-filtering.md`) and
|
|
6
|
+
wired in `road-to-6.0.0-a-positioning-and-validation` Phase 2 / Step 8.
|
|
7
|
+
|
|
8
|
+
In 6.0.0-A this writes `profile.id` into the canonical project
|
|
9
|
+
`.agent-settings.yml` and prints what changed — it does **NOT** narrow what
|
|
10
|
+
gets projected into the tool trees. Pack-scoped surfacing (projection-time
|
|
11
|
+
filtering) activates in 6.0.0-B behind a staged, opt-in rollout. This command
|
|
12
|
+
is the stable seam that 6.0.0-B hooks projection into.
|
|
13
|
+
|
|
14
|
+
Comment-preserving: the canonical settings file is hand-editable and richly
|
|
15
|
+
commented, so the write is a surgical text edit of the `profile:` block, not a
|
|
16
|
+
yaml round-trip that would strip comments.
|
|
17
|
+
|
|
18
|
+
CLI:
|
|
19
|
+
agent-config use --profile=<id>
|
|
20
|
+
agent-config use --profile <id>
|
|
21
|
+
|
|
22
|
+
Valid ids (the six seed profiles, docs/contracts/profile-system.md):
|
|
23
|
+
developer · content_creator · founder · agency · finance · ops
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import re
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
33
|
+
from _lib.agent_settings import ( # noqa: E402
|
|
34
|
+
canonical_settings_write_path,
|
|
35
|
+
find_project_root,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
VALID_PROFILES = (
|
|
39
|
+
"developer",
|
|
40
|
+
"content_creator",
|
|
41
|
+
"founder",
|
|
42
|
+
"agency",
|
|
43
|
+
"finance",
|
|
44
|
+
"ops",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Matches a top-level `profile:` block and the `id:` leaf under it. The id
|
|
48
|
+
# value may be bare, single-, or double-quoted; we only rewrite the value.
|
|
49
|
+
_PROFILE_ID_RE = re.compile(
|
|
50
|
+
r"(?m)^(?P<head>profile:[ \t]*\n(?:[ \t]+#[^\n]*\n|[ \t]*\n)*"
|
|
51
|
+
r"[ \t]+id:[ \t]*)(?P<val>[^\n#]*)",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _resolve_write_path() -> Path:
|
|
56
|
+
cwd = Path.cwd()
|
|
57
|
+
root = find_project_root(cwd) or cwd
|
|
58
|
+
return canonical_settings_write_path(root)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _set_profile_id(text: str, profile_id: str) -> tuple[str, str | None]:
|
|
62
|
+
"""Return (new_text, previous_id). Append a block if none exists."""
|
|
63
|
+
m = _PROFILE_ID_RE.search(text)
|
|
64
|
+
if m:
|
|
65
|
+
previous = m.group("val").strip().strip("'\"") or None
|
|
66
|
+
new_text = text[: m.start("val")] + profile_id + text[m.end("val") :]
|
|
67
|
+
return new_text, previous
|
|
68
|
+
# No profile block — append one. Keep a single trailing newline.
|
|
69
|
+
block = f"\n# --- Profile (experience) ---\nprofile:\n id: {profile_id}\n"
|
|
70
|
+
sep = "" if text.endswith("\n") else "\n"
|
|
71
|
+
return text + sep + block, None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main(argv: list[str] | None = None) -> int:
|
|
75
|
+
parser = argparse.ArgumentParser(
|
|
76
|
+
prog="agent-config use",
|
|
77
|
+
description="Switch the active experience/profile (writes profile.id).",
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--profile",
|
|
81
|
+
metavar="ID",
|
|
82
|
+
help=f"Experience to switch to. One of: {', '.join(VALID_PROFILES)}.",
|
|
83
|
+
)
|
|
84
|
+
args = parser.parse_args(argv)
|
|
85
|
+
|
|
86
|
+
if not args.profile:
|
|
87
|
+
print(
|
|
88
|
+
"❌ `use` requires --profile=<id>. Valid: "
|
|
89
|
+
+ " · ".join(VALID_PROFILES),
|
|
90
|
+
file=sys.stderr,
|
|
91
|
+
)
|
|
92
|
+
return 2
|
|
93
|
+
|
|
94
|
+
profile_id = args.profile.strip()
|
|
95
|
+
if profile_id not in VALID_PROFILES:
|
|
96
|
+
print(
|
|
97
|
+
f"❌ unknown profile `{profile_id}`. Valid: "
|
|
98
|
+
+ " · ".join(VALID_PROFILES),
|
|
99
|
+
file=sys.stderr,
|
|
100
|
+
)
|
|
101
|
+
return 2
|
|
102
|
+
|
|
103
|
+
path = _resolve_write_path()
|
|
104
|
+
text = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
105
|
+
new_text, previous = _set_profile_id(text, profile_id)
|
|
106
|
+
|
|
107
|
+
if previous == profile_id and path.exists():
|
|
108
|
+
print(f"✅ Already on experience `{profile_id}` — no change ({path}).")
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
path.write_text(new_text, encoding="utf-8")
|
|
113
|
+
|
|
114
|
+
arrow = f"`{previous}` → `{profile_id}`" if previous else f"`{profile_id}`"
|
|
115
|
+
print(f"✅ Experience set to {arrow} in {path}.")
|
|
116
|
+
print(
|
|
117
|
+
"ℹ️ 6.0.0-A records the choice only — it does not yet narrow what is "
|
|
118
|
+
"projected into .claude/ .cursor/ .augment/. Pack-scoped surfacing "
|
|
119
|
+
"(ADR-040) activates in 6.0.0-B behind a staged, opt-in rollout."
|
|
120
|
+
)
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""session_start concern — keep the project-local ``./agent-config`` fresh.
|
|
3
|
+
|
|
4
|
+
Defense-in-depth twin of the update-command refresh (``upgrade`` /
|
|
5
|
+
``refresh --project``). On every session_start the dispatcher runs this in
|
|
6
|
+
the consumer workspace; if a ``./agent-config`` wrapper exists there and
|
|
7
|
+
differs from the canonical template, it is re-stamped so an outdated,
|
|
8
|
+
fallback-less copy cannot keep breaking the hooks.
|
|
9
|
+
|
|
10
|
+
Bootstrapping note: this can only heal a wrapper functional enough to
|
|
11
|
+
invoke the dispatcher in the first place (the current template's global +
|
|
12
|
+
npx fallbacks guarantee that). A *completely* broken wrapper never reaches
|
|
13
|
+
this concern — that recovery path is ``agent-config upgrade`` /
|
|
14
|
+
``refresh --project``.
|
|
15
|
+
|
|
16
|
+
Contract: never creates a wrapper where none exists (that is an install
|
|
17
|
+
action); never touches the agent-config source repo; always fail-open
|
|
18
|
+
(exit 0) — hook self-heal must not block a session.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
# Make `scripts` importable when invoked as a bare script path.
|
|
29
|
+
_REPO = Path(__file__).resolve().parent.parent
|
|
30
|
+
if str(_REPO) not in sys.path:
|
|
31
|
+
sys.path.insert(0, str(_REPO))
|
|
32
|
+
|
|
33
|
+
EXIT_ALLOW = 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _project_root() -> Path:
|
|
37
|
+
env = os.environ.get("CLAUDE_PROJECT_DIR") or os.environ.get(
|
|
38
|
+
"AGENT_CONFIG_PROJECT_DIR")
|
|
39
|
+
if env:
|
|
40
|
+
return Path(env)
|
|
41
|
+
return Path.cwd()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main(argv: list[str] | None = None) -> int:
|
|
45
|
+
ap = argparse.ArgumentParser(
|
|
46
|
+
description="Project ./agent-config wrapper freshness (session_start).")
|
|
47
|
+
ap.add_argument("--root", default=None)
|
|
48
|
+
args, _ = ap.parse_known_args(argv)
|
|
49
|
+
|
|
50
|
+
# Drain stdin (the dispatcher passes a JSON envelope); we do not need it.
|
|
51
|
+
try:
|
|
52
|
+
if not sys.stdin.isatty():
|
|
53
|
+
sys.stdin.read()
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
from scripts._lib import cli_wrapper
|
|
59
|
+
from scripts._cli.cmd_refresh import _is_source_repo
|
|
60
|
+
except Exception: # pragma: no cover — defensive; never block the loop
|
|
61
|
+
return EXIT_ALLOW
|
|
62
|
+
|
|
63
|
+
root = Path(args.root) if args.root else _project_root()
|
|
64
|
+
try:
|
|
65
|
+
if _is_source_repo(root):
|
|
66
|
+
return EXIT_ALLOW
|
|
67
|
+
if not (root / "agent-config").is_file():
|
|
68
|
+
return EXIT_ALLOW # no wrapper here — never create one
|
|
69
|
+
if not cli_wrapper.needs_refresh(root):
|
|
70
|
+
return EXIT_ALLOW
|
|
71
|
+
wrapper = cli_wrapper.install_cli_wrapper(root)
|
|
72
|
+
except Exception:
|
|
73
|
+
return EXIT_ALLOW # fail-open — never block the session
|
|
74
|
+
|
|
75
|
+
if wrapper is not None:
|
|
76
|
+
print(f"[wrapper] re-stamped stale ./agent-config at {wrapper}",
|
|
77
|
+
file=sys.stderr)
|
|
78
|
+
sys.stdout.write(json.dumps({
|
|
79
|
+
"decision": "allow",
|
|
80
|
+
"reason": f"refreshed stale ./agent-config wrapper at {wrapper}",
|
|
81
|
+
}))
|
|
82
|
+
return EXIT_ALLOW
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
raise SystemExit(main())
|