@event4u/agent-config 8.2.0 → 8.4.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/.claude-plugin/marketplace.json +4 -426
- package/.claude-plugin/skills/install-agent-config/SKILL.md +32 -0
- package/CHANGELOG.md +57 -0
- package/MIGRATION.md +18 -0
- package/README.md +86 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- 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/install/install.mjs +586 -386
- package/dist/mcp/registry-manifest.json +2 -2
- package/docs/contracts/CHANGELOG-conventions.md +8 -5
- package/docs/contracts/branch-protection-policy.md +14 -5
- package/docs/contracts/mcp-phase-1-scope.md +75 -0
- package/docs/contracts/mcp-tool-inventory.md +22 -15
- package/docs/contracts/release-pr-gating.md +22 -11
- package/docs/contracts/surface-matrix.md +103 -0
- package/docs/decisions/ADR-111-canonical-glama-listing.md +97 -0
- package/docs/decisions/ADR-112-stdio-lite-stays-read-only.md +92 -0
- package/docs/decisions/ADR-113-ci-native-release-label-trigger.md +201 -0
- package/docs/decisions/INDEX.md +3 -0
- package/docs/distribution/registry-submissions.md +1 -1
- package/docs/getting-started.md +8 -6
- package/docs/installation.md +13 -14
- package/docs/mcp-server.md +32 -20
- package/docs/setup/mcp-cloud-endpoints.md +9 -2
- package/package.json +2 -2
- package/src/config/surface-matrix.yml +213 -0
- package/src/scripts/_cli/cmd_converge.ts +399 -0
- package/src/scripts/_cli/cmd_doctor.ts +203 -9
- package/src/scripts/_cli/cmd_uninstall.ts +22 -0
- package/src/scripts/_cli/cmd_upgrade.ts +101 -16
- package/src/scripts/_dispatch.bash +13 -0
- package/src/scripts/_lib/claude_settings_hooks.ts +251 -0
- package/src/scripts/condense.ts +74 -65
- package/src/scripts/hook_manifest.yaml +16 -7
- package/src/scripts/install +23 -4
- package/src/scripts/install.ts +30 -0
- package/src/scripts/lint_glama_drift.ts +75 -0
- package/src/scripts/lint_marketplace.ts +54 -56
- package/src/scripts/lint_surface_matrix.ts +200 -0
- package/src/scripts/mcp_exec/safety_envelope.ts +105 -0
- package/src/scripts/mcp_registry_submit.sh +14 -3
- package/src/scripts/mcp_server/__main__.ts +6 -8
- package/src/scripts/mcp_server/consumer_tool_catalog.json +78 -3
- package/src/scripts/mcp_server/tools.ts +731 -0
- package/src/scripts/release.ts +203 -39
- package/src/scripts/surface_probe_hook.ts +231 -0
package/src/scripts/condense.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
resolve_logical as _agent_src_resolve_logical,
|
|
40
40
|
strip_source_prefix,
|
|
41
41
|
} from './_lib/agent_src.js';
|
|
42
|
+
import { build_claude_hook_matrix } from './_lib/claude_settings_hooks.js';
|
|
42
43
|
import { project_settings_path, load_agent_settings } from './_lib/agent_settings.js';
|
|
43
44
|
import { info, success, flush_summary, resolve_level } from './_lib/script_output.js';
|
|
44
45
|
import {
|
|
@@ -1809,65 +1810,96 @@ export function generate_subagent_host_contexts(): number {
|
|
|
1809
1810
|
return count;
|
|
1810
1811
|
}
|
|
1811
1812
|
|
|
1813
|
+
// Bootstrap shim (road-to-install-path-convergence): the Claude Code plugin
|
|
1814
|
+
// ships ZERO content skills. It carries only hooks/hooks.json (byte-identical
|
|
1815
|
+
// to the managed settings block, so Claude Code dedupes) plus this single
|
|
1816
|
+
// pointer skill whose description names the canonical install command.
|
|
1817
|
+
export const PLUGIN_POINTER_SLUG = 'install-agent-config';
|
|
1818
|
+
export const PLUGIN_POINTER_SKILL_BODY = `---
|
|
1819
|
+
name: install-agent-config
|
|
1820
|
+
description: >-
|
|
1821
|
+
Install or upgrade the event4u agent-config suite. This marketplace plugin
|
|
1822
|
+
is a bootstrap shim — it ships hooks plus this pointer only; ALL skills,
|
|
1823
|
+
rules, and commands are installed via \`npx -y @event4u/agent-config init\`.
|
|
1824
|
+
Use when the user asks to install, set up, or upgrade agent-config, or
|
|
1825
|
+
wonders why this plugin contains no other skills.
|
|
1826
|
+
---
|
|
1827
|
+
|
|
1828
|
+
# Install agent-config
|
|
1829
|
+
|
|
1830
|
+
This plugin is a **bootstrap shim**. It exists so the marketplace listing
|
|
1831
|
+
stays discoverable and existing plugin installs keep working hooks — the
|
|
1832
|
+
content surface (skills, rules, commands, personas) is distributed as a
|
|
1833
|
+
file projection, not through this plugin.
|
|
1834
|
+
|
|
1835
|
+
## Install (canonical, one command)
|
|
1836
|
+
|
|
1837
|
+
\`\`\`bash
|
|
1838
|
+
npx -y @event4u/agent-config init
|
|
1839
|
+
\`\`\`
|
|
1840
|
+
|
|
1841
|
+
The installer writes the full content projection, registers the dispatcher
|
|
1842
|
+
hooks as a managed block in \`~/.claude/settings.json\` (byte-identical to
|
|
1843
|
+
this plugin's hooks, so nothing double-fires), and keeps everything
|
|
1844
|
+
upgradeable via \`agent-config upgrade\`.
|
|
1845
|
+
|
|
1846
|
+
## Already installed both?
|
|
1847
|
+
|
|
1848
|
+
Run \`agent-config doctor\` to check for duplicate surfaces, and
|
|
1849
|
+
\`agent-config converge\` to clean up consented duplicates.
|
|
1850
|
+
`;
|
|
1851
|
+
|
|
1812
1852
|
export function generate_plugin_command_skills(): number {
|
|
1813
1853
|
if (!_isDir(path.join(MODULE_STATE.PROJECT_ROOT, 'src', 'domains'))) {
|
|
1814
1854
|
return 0;
|
|
1815
1855
|
}
|
|
1816
1856
|
_mkdirp(MODULE_STATE.PLUGIN_SKILLS_DIR);
|
|
1817
1857
|
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
);
|
|
1858
|
+
// 1. Materialize the pointer skill (idempotent overwrite-on-drift).
|
|
1859
|
+
const pointer_dir = path.join(MODULE_STATE.PLUGIN_SKILLS_DIR, PLUGIN_POINTER_SLUG);
|
|
1860
|
+
_mkdirp(pointer_dir);
|
|
1861
|
+
const pointer_file = path.join(pointer_dir, 'SKILL.md');
|
|
1862
|
+
if (_isSymlink(pointer_file)) {
|
|
1863
|
+
fs.unlinkSync(pointer_file);
|
|
1825
1864
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
let count = 0;
|
|
1829
|
-
for (const [source_file, slug] of iterCommands()) {
|
|
1830
|
-
if (skill_names.has(slug)) {
|
|
1831
|
-
continue;
|
|
1832
|
-
}
|
|
1833
|
-
current_slugs.add(slug);
|
|
1834
|
-
const skill_dir = path.join(MODULE_STATE.PLUGIN_SKILLS_DIR, slug);
|
|
1835
|
-
_mkdirp(skill_dir);
|
|
1836
|
-
const skill_file = path.join(skill_dir, 'SKILL.md');
|
|
1837
|
-
if (_existsOrSymlink(skill_file)) {
|
|
1838
|
-
fs.unlinkSync(skill_file);
|
|
1839
|
-
}
|
|
1840
|
-
const rel_path = _relativeToPosix(source_file, MODULE_STATE.PROJECT_ROOT);
|
|
1841
|
-
const rel_target = path.join('../../..', rel_path);
|
|
1842
|
-
fs.symlinkSync(rel_target, skill_file);
|
|
1843
|
-
count += 1;
|
|
1865
|
+
if (!_isFile(pointer_file) || fs.readFileSync(pointer_file, 'utf-8') !== PLUGIN_POINTER_SKILL_BODY) {
|
|
1866
|
+
fs.writeFileSync(pointer_file, PLUGIN_POINTER_SKILL_BODY);
|
|
1844
1867
|
}
|
|
1845
1868
|
|
|
1869
|
+
// 2. Prune every other skill dir so the tree converges to shim shape
|
|
1870
|
+
// from any prior state. Removable = every entry is a symlink (the
|
|
1871
|
+
// generated projection shape — SKILL.md and optional evals links into
|
|
1872
|
+
// src/), or the dir holds only a single SKILL.md file (the old sweep's
|
|
1873
|
+
// rule). Dirs with real extra files are never touched.
|
|
1846
1874
|
let removed_dirs = 0;
|
|
1847
1875
|
for (const item of _iterdirSorted(MODULE_STATE.PLUGIN_SKILLS_DIR)) {
|
|
1848
1876
|
if (!_isDirNoFollow(item) || _isSymlink(item)) {
|
|
1849
1877
|
continue;
|
|
1850
1878
|
}
|
|
1851
|
-
if (
|
|
1879
|
+
if (path.basename(item) === PLUGIN_POINTER_SLUG) {
|
|
1852
1880
|
continue;
|
|
1853
1881
|
}
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1882
|
+
const entries = _iterdirSorted(item);
|
|
1883
|
+
const all_symlinks = entries.length > 0 && entries.every((e) => _isSymlink(e));
|
|
1884
|
+
const single_skill_md =
|
|
1885
|
+
entries.length === 1 &&
|
|
1886
|
+
path.basename(entries[0] as string) === 'SKILL.md' &&
|
|
1887
|
+
_isFile(entries[0] as string);
|
|
1888
|
+
if (all_symlinks || single_skill_md) {
|
|
1889
|
+
for (const e of entries) {
|
|
1890
|
+
fs.unlinkSync(e);
|
|
1861
1891
|
}
|
|
1892
|
+
fs.rmdirSync(item);
|
|
1893
|
+
removed_dirs += 1;
|
|
1862
1894
|
}
|
|
1863
1895
|
}
|
|
1864
1896
|
|
|
1865
|
-
let msg =
|
|
1897
|
+
let msg = ' ✅ Plugin skills: bootstrap shim (1 pointer skill) in .claude-plugin/skills/';
|
|
1866
1898
|
if (removed_dirs) {
|
|
1867
|
-
msg += ` (${removed_dirs}
|
|
1899
|
+
msg += ` (${removed_dirs} content entries pruned)`;
|
|
1868
1900
|
}
|
|
1869
1901
|
info(msg);
|
|
1870
|
-
return
|
|
1902
|
+
return 1;
|
|
1871
1903
|
}
|
|
1872
1904
|
|
|
1873
1905
|
export function generate_persona_symlinks(): number {
|
|
@@ -1963,36 +1995,13 @@ export function generate_plugin_hooks(): number {
|
|
|
1963
1995
|
return 0;
|
|
1964
1996
|
}
|
|
1965
1997
|
|
|
1966
|
-
//
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
const hook_spec = (manifest['schema_version'] as unknown) ?? 1;
|
|
1972
|
-
const platforms = (manifest['platforms'] ?? {}) as Record<string, unknown>;
|
|
1973
|
-
const claude_events = ((platforms['claude'] ?? {}) as Record<string, unknown>) || {};
|
|
1974
|
-
const nativeAliasesAll = (manifest['native_event_aliases'] ?? {}) as Record<string, unknown>;
|
|
1975
|
-
const aliases = ((nativeAliasesAll['claude'] ?? {}) as Record<string, unknown>) || {};
|
|
1976
|
-
// Reverse the native→agent-config map.
|
|
1977
|
-
const ac_to_native: Record<string, string> = {};
|
|
1978
|
-
for (const [native, ac] of Object.entries(aliases)) {
|
|
1979
|
-
ac_to_native[String(ac)] = native;
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1998
|
+
// Single source of truth: the SAME matrix derivation the installer uses
|
|
1999
|
+
// for the managed settings.json block (road-to-claude-code-single-surface
|
|
2000
|
+
// Phase 4) — plugin hooks.json and settings hooks cannot drift by
|
|
2001
|
+
// construction. Backstop: tests/install/claude_hook_matrix_parity.test.ts.
|
|
2002
|
+
const matrix = build_claude_hook_matrix(manifest_path);
|
|
1982
2003
|
const hooks: Record<string, unknown[]> = {};
|
|
1983
|
-
for (const [
|
|
1984
|
-
if (!concerns || (Array.isArray(concerns) && concerns.length === 0)) {
|
|
1985
|
-
continue;
|
|
1986
|
-
}
|
|
1987
|
-
const native = ac_to_native[ac_event];
|
|
1988
|
-
if (native === undefined) {
|
|
1989
|
-
continue;
|
|
1990
|
-
}
|
|
1991
|
-
const command =
|
|
1992
|
-
'BIN="$CLAUDE_PROJECT_DIR/agent-config"; [ -x "$BIN" ] || BIN=agent-config; ' +
|
|
1993
|
-
`"$BIN" dispatch:hook --platform claude --event ${ac_event} ` +
|
|
1994
|
-
`--native-event ${native} --project-dir "$CLAUDE_PROJECT_DIR" ` +
|
|
1995
|
-
`--min-version ${String(hook_spec)}`;
|
|
2004
|
+
for (const [native, command] of Object.entries(matrix)) {
|
|
1996
2005
|
hooks[native] = [{ hooks: [{ type: 'command', command }] }];
|
|
1997
2006
|
}
|
|
1998
2007
|
|
|
@@ -125,17 +125,26 @@ concerns:
|
|
|
125
125
|
script: src/scripts/wrapper_freshness_hook.ts
|
|
126
126
|
args: []
|
|
127
127
|
fail_closed: false
|
|
128
|
+
# road-to-install-path-convergence Phase 4 — duplicate-surface probe.
|
|
129
|
+
# Reads src/config/surface-matrix.yml, existence-checks declared duplicate
|
|
130
|
+
# classes, nudges ONCE per day (state: agents/runtime/state/
|
|
131
|
+
# surface-probe.json). Fail-open, never blocking; silent when clean or
|
|
132
|
+
# when install.auto_converge is already set.
|
|
133
|
+
surface-probe:
|
|
134
|
+
script: src/scripts/surface_probe_hook.ts
|
|
135
|
+
args: []
|
|
136
|
+
fail_closed: false
|
|
128
137
|
|
|
129
138
|
platforms:
|
|
130
139
|
augment:
|
|
131
|
-
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness]
|
|
140
|
+
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
132
141
|
session_end: [chat-history]
|
|
133
142
|
stop: [chat-history, hot-context, verify-before-complete]
|
|
134
143
|
pre_tool_use: [block-no-verify, rtk-wrap, design-slop]
|
|
135
144
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan]
|
|
136
145
|
|
|
137
146
|
claude:
|
|
138
|
-
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness]
|
|
147
|
+
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
139
148
|
session_end: [chat-history]
|
|
140
149
|
stop: [chat-history, hot-context, verify-before-complete]
|
|
141
150
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
@@ -157,7 +166,7 @@ platforms:
|
|
|
157
166
|
# Decision matrix + upstream blockers tracked in
|
|
158
167
|
# agents/settings/contexts/chat-history-platform-hooks.md § Cowork.
|
|
159
168
|
cowork:
|
|
160
|
-
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness]
|
|
169
|
+
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
161
170
|
session_end: [chat-history]
|
|
162
171
|
stop: [chat-history, hot-context, verify-before-complete]
|
|
163
172
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
@@ -172,7 +181,7 @@ platforms:
|
|
|
172
181
|
# IDE-only — CLI-only users fall back to /checkpoint per
|
|
173
182
|
# agents/settings/contexts/chat-history-platform-hooks.md.
|
|
174
183
|
cursor:
|
|
175
|
-
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness]
|
|
184
|
+
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
176
185
|
session_end: [chat-history]
|
|
177
186
|
stop: [chat-history, hot-context, verify-before-complete]
|
|
178
187
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
@@ -187,7 +196,7 @@ platforms:
|
|
|
187
196
|
# both map to session_start. TaskCancel maps to stop because the
|
|
188
197
|
# session is interrupted with partial state (mirrors Augment Stop).
|
|
189
198
|
cline:
|
|
190
|
-
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness]
|
|
199
|
+
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
191
200
|
session_end: [chat-history]
|
|
192
201
|
stop: [chat-history, hot-context, verify-before-complete]
|
|
193
202
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
|
@@ -206,7 +215,7 @@ platforms:
|
|
|
206
215
|
# surface to record verification commands; documented limitation).
|
|
207
216
|
# minimal-safe-diff is omitted entirely on Windsurf for the same reason.
|
|
208
217
|
windsurf:
|
|
209
|
-
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness]
|
|
218
|
+
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness, surface-probe]
|
|
210
219
|
stop: [chat-history, hot-context, verify-before-complete]
|
|
211
220
|
user_prompt_submit: [chat-history, verify-before-complete]
|
|
212
221
|
|
|
@@ -221,7 +230,7 @@ platforms:
|
|
|
221
230
|
# turn-check semantics. AfterAgent fires when the agent loop ends
|
|
222
231
|
# — this is our `stop` slot.
|
|
223
232
|
gemini:
|
|
224
|
-
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness]
|
|
233
|
+
session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
225
234
|
session_end: [chat-history]
|
|
226
235
|
stop: [chat-history, hot-context, verify-before-complete]
|
|
227
236
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
|
package/src/scripts/install
CHANGED
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
# the comma-separated values are unioned.
|
|
28
28
|
# --list-tools Print supported tool IDs with descriptions, then exit
|
|
29
29
|
# --yes, -y Non-interactive mode: do not prompt (default for non-TTY)
|
|
30
|
+
# --no-ui Unattended install: never launch the browser setup
|
|
31
|
+
# wizard and skip the terminal tool picker (tools default
|
|
32
|
+
# to all). Used by `agent-config upgrade`.
|
|
30
33
|
# --force Overwrite existing bridge files
|
|
31
34
|
# --dry-run Show what payload sync would do (does not run bridges)
|
|
32
35
|
# --verbose Detailed payload sync output
|
|
@@ -95,12 +98,13 @@ MINIMAL=false
|
|
|
95
98
|
VALIDATE_ONLY=false
|
|
96
99
|
FLEET_CONFIG=""
|
|
97
100
|
USER_TYPE=""
|
|
101
|
+
NO_UI=false
|
|
98
102
|
|
|
99
103
|
# Single source of truth for valid tool IDs (also referenced by install.sh / install.py).
|
|
100
104
|
VALID_TOOLS="claude-code claude-desktop cursor windsurf cline gemini-cli copilot augment aider codex roocode continue kilocode zed jetbrains kiro all"
|
|
101
105
|
|
|
102
106
|
show_help() {
|
|
103
|
-
sed -n '3,
|
|
107
|
+
sed -n '3,61p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
list_tools() {
|
|
@@ -167,6 +171,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
167
171
|
--ai=*) TOOLS="${TOOLS:+$TOOLS,}${1#*=}"; TOOLS_EXPLICIT=true; shift ;;
|
|
168
172
|
--list-tools) LIST_TOOLS=true; shift ;;
|
|
169
173
|
--yes|-y) YES=true; shift ;;
|
|
174
|
+
--no-ui) NO_UI=true; shift ;;
|
|
170
175
|
--force) FORCE=true; shift ;;
|
|
171
176
|
--dry-run) DRY_RUN=true; shift ;;
|
|
172
177
|
--verbose) VERBOSE=true; shift ;;
|
|
@@ -194,6 +199,15 @@ if $LIST_TOOLS; then
|
|
|
194
199
|
exit 0
|
|
195
200
|
fi
|
|
196
201
|
|
|
202
|
+
# --no-ui = unattended install (the `agent-config upgrade` path). Export the
|
|
203
|
+
# suppression env so every downstream stage (install.ts honors both the flag
|
|
204
|
+
# and AGENT_CONFIG_NO_UI) skips the browser setup wizard — the wizard is an
|
|
205
|
+
# onboarding surface whose foreground server used to block `upgrade` until
|
|
206
|
+
# Ctrl-C.
|
|
207
|
+
if $NO_UI; then
|
|
208
|
+
export AGENT_CONFIG_NO_UI=1
|
|
209
|
+
fi
|
|
210
|
+
|
|
197
211
|
# road-to-global-only-install § Phase 3.3 — consumer-floor gate.
|
|
198
212
|
#
|
|
199
213
|
# Consumer installs are global-only (ADR-020). Explicit --scope=project
|
|
@@ -282,8 +296,8 @@ prompt_tools() {
|
|
|
282
296
|
# When an interactive global install will hand off to the browser wizard,
|
|
283
297
|
# the wizard is the single tool-selection surface — skip the terminal
|
|
284
298
|
# picker so it does not pre-empt (and, via TOOLS_EXPLICIT, suppress) the
|
|
285
|
-
# GUI. Mirrors install.py::_wizard_should_launch
|
|
286
|
-
#
|
|
299
|
+
# GUI. Mirrors install.py::_wizard_should_launch (the --no-ui flag is
|
|
300
|
+
# handled here too — it suppresses both the wizard and the picker).
|
|
287
301
|
# Headless paths (no TTY / CI / AGENT_CONFIG_NO_UI) still get the picker.
|
|
288
302
|
wizard_will_handle_tools=false
|
|
289
303
|
if $GLOBAL && [[ -t 0 && -t 1 && -z "${CI:-}" ]] \
|
|
@@ -291,8 +305,12 @@ if $GLOBAL && [[ -t 0 && -t 1 && -z "${CI:-}" ]] \
|
|
|
291
305
|
wizard_will_handle_tools=true
|
|
292
306
|
fi
|
|
293
307
|
|
|
308
|
+
# --no-ui also skips the terminal picker: the flag means "fully unattended"
|
|
309
|
+
# (its only caller today is `agent-config upgrade`, where a mid-upgrade
|
|
310
|
+
# prompt reads as a hang). Tools fall through to the "all" default below —
|
|
311
|
+
# the same effective selection the wizard-handoff path used.
|
|
294
312
|
if ! $MINIMAL && ! $TOOLS_EXPLICIT && ! $YES && ! $QUIET && ! $LIST_TOOLS \
|
|
295
|
-
&& [[ -t 0 && -t 1 ]] && ! $wizard_will_handle_tools; then
|
|
313
|
+
&& [[ -t 0 && -t 1 ]] && ! $wizard_will_handle_tools && ! $NO_UI; then
|
|
296
314
|
prompt_tools
|
|
297
315
|
TOOLS_EXPLICIT=true
|
|
298
316
|
fi
|
|
@@ -416,6 +434,7 @@ run_bridges() {
|
|
|
416
434
|
[[ -n "$CUSTOM_PATH" ]] && args+=(--custom-path="$CUSTOM_PATH")
|
|
417
435
|
$OFFLINE && args+=(--offline)
|
|
418
436
|
$MINIMAL && args+=(--minimal)
|
|
437
|
+
$NO_UI && args+=(--no-ui)
|
|
419
438
|
args+=(--tools="$TOOLS")
|
|
420
439
|
"${runner[@]}" "${args[@]}"
|
|
421
440
|
}
|
package/src/scripts/install.ts
CHANGED
|
@@ -78,6 +78,7 @@ import * as global_deploy_inventory from './_lib/global_deploy_inventory.js';
|
|
|
78
78
|
import * as installed_tools from './_lib/installed_tools.js';
|
|
79
79
|
import * as user_global_paths from './_lib/user_global_paths.js';
|
|
80
80
|
import * as claude_desktop_bundler from './_lib/claude_desktop_bundler.js';
|
|
81
|
+
import * as claude_settings_hooks from './_lib/claude_settings_hooks.js';
|
|
81
82
|
import { find_project_root_with_anchor, load_agent_settings } from './_lib/agent_settings.js';
|
|
82
83
|
import { detect_module_roots } from './_lib/module_detection.js';
|
|
83
84
|
import {
|
|
@@ -2914,6 +2915,35 @@ function _deploy_global_content(
|
|
|
2914
2915
|
}
|
|
2915
2916
|
_emit_progress({ type: 'reaped', tool: tool_id, count: reaped.length });
|
|
2916
2917
|
results[tool_id] = [written_total, skipped_total, 'deployed', written_paths];
|
|
2918
|
+
|
|
2919
|
+
// Single-surface model (road-to-claude-code-single-surface): the
|
|
2920
|
+
// deterministic hook matrix registers directly in the user-scope
|
|
2921
|
+
// settings file — the marketplace plugin is no longer required for
|
|
2922
|
+
// hooks. Failure here must not sink the content deploy (hooks can
|
|
2923
|
+
// be re-wired via `agent-config refresh --global` / doctor).
|
|
2924
|
+
if (tool_id === 'claude-code') {
|
|
2925
|
+
try {
|
|
2926
|
+
const manifest = path.join(package_root, 'src', 'scripts', 'hook_manifest.yaml');
|
|
2927
|
+
const matrix = claude_settings_hooks.build_claude_hook_matrix(manifest);
|
|
2928
|
+
const res = claude_settings_hooks.ensure_managed_hooks(
|
|
2929
|
+
path.join(anchor, 'settings.json'),
|
|
2930
|
+
matrix,
|
|
2931
|
+
);
|
|
2932
|
+
if (!state.QUIET) {
|
|
2933
|
+
if (res.changed) {
|
|
2934
|
+
success(
|
|
2935
|
+
`claude-code: managed hooks registered in ~/.claude/settings.json ` +
|
|
2936
|
+
`(${res.events.length} event(s))`,
|
|
2937
|
+
);
|
|
2938
|
+
} else {
|
|
2939
|
+
skip('claude-code: managed hooks already registered');
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
} catch (e) {
|
|
2943
|
+
warn(`claude-code: managed-hook registration failed — ${String(e)}`);
|
|
2944
|
+
_emit_progress({ type: 'hooks_failed', tool: tool_id, error: String(e) });
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2917
2947
|
}
|
|
2918
2948
|
return results;
|
|
2919
2949
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Guard against `internal/glama/README.md` drifting from the committed
|
|
4
|
+
* `internal/glama/build` / `internal/glama/run` scripts it documents.
|
|
5
|
+
*
|
|
6
|
+
* Extracts the single canonical command line each script actually runs
|
|
7
|
+
* (the last non-comment, non-`set -euo pipefail` line — `exec ` stripped)
|
|
8
|
+
* and asserts that literal string appears verbatim somewhere in the README.
|
|
9
|
+
* Catches the exact failure mode fixed in road-to-mcp-full-power.md Phase 1:
|
|
10
|
+
* the README described a Python/uv build long after the scripts moved to
|
|
11
|
+
* Node/tsx.
|
|
12
|
+
*
|
|
13
|
+
* Exit codes: 0 no drift, 1 one or more scripts' canonical command is not
|
|
14
|
+
* documented verbatim in the README.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
22
|
+
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
23
|
+
const GLAMA_DIR = path.join(REPO, 'internal', 'glama');
|
|
24
|
+
|
|
25
|
+
interface ScriptSpec {
|
|
26
|
+
file: string;
|
|
27
|
+
/** Strip this exact prefix from the extracted command line before comparing. */
|
|
28
|
+
stripPrefix?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SCRIPTS: readonly ScriptSpec[] = [
|
|
32
|
+
{ file: 'build' },
|
|
33
|
+
{ file: 'run', stripPrefix: 'exec ' },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function _canonicalCommand(scriptPath: string, stripPrefix?: string): string {
|
|
37
|
+
const lines = fs.readFileSync(scriptPath, 'utf8').split('\n');
|
|
38
|
+
const candidates = lines
|
|
39
|
+
.map((l) => l.trim())
|
|
40
|
+
.filter((l) => l.length > 0 && !l.startsWith('#') && l !== 'set -euo pipefail');
|
|
41
|
+
const last = candidates[candidates.length - 1] ?? '';
|
|
42
|
+
return stripPrefix && last.startsWith(stripPrefix) ? last.slice(stripPrefix.length) : last;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function main(): number {
|
|
46
|
+
const readmePath = path.join(GLAMA_DIR, 'README.md');
|
|
47
|
+
const readme = fs.readFileSync(readmePath, 'utf8');
|
|
48
|
+
|
|
49
|
+
let failed = false;
|
|
50
|
+
for (const spec of SCRIPTS) {
|
|
51
|
+
const scriptPath = path.join(GLAMA_DIR, spec.file);
|
|
52
|
+
const cmd = _canonicalCommand(scriptPath, spec.stripPrefix);
|
|
53
|
+
if (!cmd) {
|
|
54
|
+
console.error(`❌ internal/glama/${spec.file}: could not extract a canonical command line`);
|
|
55
|
+
failed = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!readme.includes(cmd)) {
|
|
59
|
+
console.error(
|
|
60
|
+
`❌ drift: internal/glama/${spec.file} runs \`${cmd}\` — not found verbatim in internal/glama/README.md`,
|
|
61
|
+
);
|
|
62
|
+
failed = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (failed) {
|
|
67
|
+
console.error('\nUpdate internal/glama/README.md to match the committed build/run scripts.');
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
process.stdout.write('✅ internal/glama/README.md matches build/run scripts — no drift.\n');
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
process.exit(main());
|
|
@@ -6,16 +6,20 @@
|
|
|
6
6
|
* Wave 4b). Mirrors the Python CLI contract exactly: same cwd-relative scan
|
|
7
7
|
* roots, finding messages, stdout output, and exit codes.
|
|
8
8
|
*
|
|
9
|
-
* Validates the Claude Code Plugin Marketplace manifest against the
|
|
10
|
-
*
|
|
9
|
+
* Validates the Claude Code Plugin Marketplace manifest against the
|
|
10
|
+
* BOOTSTRAP-SHIM shape (road-to-install-path-convergence): the plugin ships
|
|
11
|
+
* ONLY hooks plus a single install-pointer skill — content skills are
|
|
12
|
+
* distributed via the npx file projection, never through this plugin.
|
|
11
13
|
*
|
|
12
14
|
* - Required top-level fields: name, owner, metadata, plugins
|
|
13
15
|
* - owner must have name + email
|
|
14
16
|
* - metadata must have description + version
|
|
15
17
|
* - metadata.version must match package.json (single source of truth)
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* - the union of plugins[].skills[] must be EXACTLY the pointer skill
|
|
19
|
+
* (./.claude-plugin/skills/install-agent-config) — a repopulated
|
|
20
|
+
* content-skill list FAILS by design
|
|
21
|
+
* - the pointer entry must exist on disk and carry a SKILL.md
|
|
22
|
+
* - .claude-plugin/skills/ on disk must contain ONLY the pointer dir
|
|
19
23
|
*
|
|
20
24
|
* Exit codes: 0 = clean, 1 = problems found, 3 = internal error.
|
|
21
25
|
*/
|
|
@@ -26,14 +30,13 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
26
30
|
const ROOT = ".";
|
|
27
31
|
const MARKETPLACE = path.join(ROOT, ".claude-plugin", "marketplace.json");
|
|
28
32
|
const PACKAGE_JSON = path.join(ROOT, "package.json");
|
|
29
|
-
//
|
|
30
|
-
// dist/agent-src/skills
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
];
|
|
33
|
+
// Bootstrap shim: the ONLY skill the plugin may list. Content skills live in
|
|
34
|
+
// the npx file projection (dist/agent-src/skills/ ships in the npm package,
|
|
35
|
+
// not through the marketplace). .claude/skills/ is a gitignored local channel
|
|
36
|
+
// and is intentionally NOT a marketplace source.
|
|
37
|
+
const POINTER_SKILL_ENTRY = "./.claude-plugin/skills/install-agent-config";
|
|
38
|
+
const PLUGIN_SKILLS_DIR = path.join(ROOT, ".claude-plugin", "skills");
|
|
39
|
+
const POINTER_SKILL_DIRNAME = "install-agent-config";
|
|
37
40
|
|
|
38
41
|
type JsonValue =
|
|
39
42
|
| string
|
|
@@ -174,6 +177,18 @@ export function main(): number {
|
|
|
174
177
|
}
|
|
175
178
|
seen.add(p);
|
|
176
179
|
|
|
180
|
+
// Bootstrap-shim invariant: any entry other than the pointer skill is
|
|
181
|
+
// a repopulated content-skill list and must FAIL.
|
|
182
|
+
if (p !== POINTER_SKILL_ENTRY) {
|
|
183
|
+
errors.push(
|
|
184
|
+
`${entry} violates the bootstrap shim: the plugin must not ship ` +
|
|
185
|
+
`content skills (only \`${POINTER_SKILL_ENTRY}\` is allowed), ` +
|
|
186
|
+
`got \`${p}\` — content is distributed via ` +
|
|
187
|
+
"`npx -y @event4u/agent-config init`",
|
|
188
|
+
);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
177
192
|
// Resolve path relative to repo root (strip leading "./" only,
|
|
178
193
|
// NOT every "." and "/" character)
|
|
179
194
|
const rel = removePrefix(p, "./");
|
|
@@ -189,9 +204,7 @@ export function main(): number {
|
|
|
189
204
|
}
|
|
190
205
|
}
|
|
191
206
|
|
|
192
|
-
//
|
|
193
|
-
// sources (dist/agent-src/skills/ + .claude-plugin/skills/) must appear in
|
|
194
|
-
// some plugin's skills[].
|
|
207
|
+
// Shim completeness: the pointer skill must be listed by some plugin.
|
|
195
208
|
const listed = new Set<string>();
|
|
196
209
|
for (const plugin of plugins) {
|
|
197
210
|
if (!isObject(plugin)) {
|
|
@@ -201,43 +214,38 @@ export function main(): number {
|
|
|
201
214
|
if (Array.isArray(skills)) {
|
|
202
215
|
for (const p of skills) {
|
|
203
216
|
if (typeof p === "string") {
|
|
204
|
-
listed.add(
|
|
217
|
+
listed.add(p);
|
|
205
218
|
}
|
|
206
219
|
}
|
|
207
220
|
}
|
|
208
221
|
}
|
|
222
|
+
if (plugins.length > 0 && !listed.has(POINTER_SKILL_ENTRY)) {
|
|
223
|
+
errors.push(
|
|
224
|
+
`bootstrap shim: pointer skill \`${POINTER_SKILL_ENTRY}\` is not ` +
|
|
225
|
+
"listed in any plugin's skills[]",
|
|
226
|
+
);
|
|
227
|
+
}
|
|
209
228
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// source_dir.relative_to(ROOT).as_posix() — ROOT is ".", so this is the
|
|
215
|
-
// dir spelled relative to cwd with POSIX separators.
|
|
216
|
-
const prefix = relPosix(sourceDir, ROOT);
|
|
217
|
-
let entries: fs.Dirent[];
|
|
229
|
+
// Shim disk shape: .claude-plugin/skills/ must contain ONLY the pointer
|
|
230
|
+
// dir — a repopulated symlink tree (e.g. from a stale generator) FAILS.
|
|
231
|
+
if (_exists(PLUGIN_SKILLS_DIR)) {
|
|
232
|
+
let entries: fs.Dirent[] = [];
|
|
218
233
|
try {
|
|
219
|
-
entries = fs.readdirSync(
|
|
234
|
+
entries = fs.readdirSync(PLUGIN_SKILLS_DIR, { withFileTypes: true });
|
|
220
235
|
} catch {
|
|
221
|
-
|
|
236
|
+
/* unreadable → nothing to assert */
|
|
222
237
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
.map((e) =>
|
|
226
|
-
.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const rel = `${prefix}/${d.name}`;
|
|
235
|
-
if (!listed.has(rel)) {
|
|
236
|
-
errors.push(
|
|
237
|
-
"skill exists on disk but is not listed in marketplace.json: " +
|
|
238
|
-
`\`./${rel}\``,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
238
|
+
const extra = entries
|
|
239
|
+
.filter((e) => _isDir(path.join(PLUGIN_SKILLS_DIR, e.name)))
|
|
240
|
+
.map((e) => e.name)
|
|
241
|
+
.filter((name) => name !== POINTER_SKILL_DIRNAME)
|
|
242
|
+
.sort();
|
|
243
|
+
for (const name of extra) {
|
|
244
|
+
errors.push(
|
|
245
|
+
"bootstrap shim: unexpected skill dir on disk (plugin ships no " +
|
|
246
|
+
`content skills): \`.claude-plugin/skills/${name}\` — remove it ` +
|
|
247
|
+
"or re-run the generator",
|
|
248
|
+
);
|
|
241
249
|
}
|
|
242
250
|
}
|
|
243
251
|
|
|
@@ -246,15 +254,9 @@ export function main(): number {
|
|
|
246
254
|
}
|
|
247
255
|
|
|
248
256
|
const pluginCount = plugins.length;
|
|
249
|
-
let skillCount = 0;
|
|
250
|
-
for (const p of plugins) {
|
|
251
|
-
if (isObject(p) && Array.isArray(p["skills"])) {
|
|
252
|
-
skillCount += p["skills"].length;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
257
|
process.stdout.write(
|
|
256
258
|
`✅ marketplace.json (${pluginCount} plugin` +
|
|
257
|
-
`${pluginCount !== 1 ? "s" : ""},
|
|
259
|
+
`${pluginCount !== 1 ? "s" : ""}, bootstrap shim: 1 pointer skill)\n`,
|
|
258
260
|
);
|
|
259
261
|
process.stdout.write(" No issues found.\n");
|
|
260
262
|
return 0;
|
|
@@ -268,10 +270,6 @@ function _isDir(p: string): boolean {
|
|
|
268
270
|
}
|
|
269
271
|
}
|
|
270
272
|
|
|
271
|
-
function relPosix(child: string, base: string): string {
|
|
272
|
-
return path.relative(base, child).split(path.sep).join("/");
|
|
273
|
-
}
|
|
274
|
-
|
|
275
273
|
function _isCliEntry(): boolean {
|
|
276
274
|
if (process.argv[1] === undefined) {
|
|
277
275
|
return false;
|