@event4u/agent-config 8.3.0 → 8.4.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 +4 -426
- package/.claude-plugin/skills/install-agent-config/SKILL.md +32 -0
- package/CHANGELOG.md +25 -0
- 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/mcp/registry-manifest.json +1 -1
- package/docs/contracts/surface-matrix.md +103 -0
- package/package.json +4 -1
- package/src/agent-src/README.md +64 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
- package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
- package/src/agent-src/templates/AGENTS.md +34 -0
- package/src/agent-src/templates/scripts/README.md +195 -0
- package/src/agent-src/templates/scripts/check_memory.ts +662 -0
- package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
- package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
- package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
- package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
- package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
- package/src/agent-src/templates/scripts/memory_report.ts +868 -0
- package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
- package/src/agent-src/templates/scripts/memory_status.ts +215 -0
- package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
- package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
- package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
- package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
- package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
- package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
- package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
- package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
- package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
- package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
- package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
- package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
- package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
- package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
- package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
- package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
- package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
- package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
- package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
- package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
- package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
- package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
- package/src/config/surface-matrix.yml +213 -0
- package/src/install/atomic.ts +111 -0
- package/src/install/conflict.ts +226 -0
- package/src/install/detect.ts +280 -0
- package/src/install/paths.ts +90 -0
- package/src/install/plan.ts +205 -0
- package/src/install/preflight.ts +210 -0
- package/src/install/selectedTools.ts +52 -0
- package/src/install/toolDetection.ts +120 -0
- package/src/install/txlog.ts +161 -0
- package/src/install/types.ts +175 -0
- package/src/install/wizard-plan.ts +216 -0
- package/src/scripts/_cli/cmd_converge.ts +399 -0
- package/src/scripts/_cli/cmd_doctor.ts +94 -0
- package/src/scripts/_cli/cmd_upgrade.ts +40 -16
- package/src/scripts/_dispatch.bash +13 -0
- package/src/scripts/condense.ts +67 -36
- package/src/scripts/hook_manifest.yaml +16 -7
- package/src/scripts/lint_marketplace.ts +54 -56
- package/src/scripts/lint_surface_matrix.ts +200 -0
- package/src/scripts/prepack-check.mjs +108 -5
- package/src/scripts/surface_probe_hook.ts +231 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* `agent-config converge` — consented cleanup of duplicate install surfaces
|
|
4
|
+
* (road-to-install-path-convergence Phase 3).
|
|
5
|
+
*
|
|
6
|
+
* Reads src/config/surface-matrix.yml, detects every tool whose declared
|
|
7
|
+
* duplicate-class paths are ALL present, and — with consent — performs the
|
|
8
|
+
* per-tool cleanup: the tool's own uninstall command plus tagged-orphan
|
|
9
|
+
* reaping of the matrix-declared cache locations. Emits a convergence report
|
|
10
|
+
* (what was removed, why, rollback hint).
|
|
11
|
+
*
|
|
12
|
+
* Consent model (council Q2, 2026-07-07 — no silent mutation of a
|
|
13
|
+
* user-owned surface):
|
|
14
|
+
*
|
|
15
|
+
* - `install.auto_converge: true` in the GLOBAL settings file
|
|
16
|
+
* (~/.event4u/agent-config/agent-settings.yml) is standing consent. The
|
|
17
|
+
* first successful interactive/explicit converge persists it.
|
|
18
|
+
* - Without the key, a TTY run asks ONE y/N question; a non-TTY run refuses
|
|
19
|
+
* with instructions (automation must opt in via the key or --yes).
|
|
20
|
+
* - `--dry-run` prints the exact actions and exits 0 without consent.
|
|
21
|
+
* - `--yes` grants consent for this run AND persists the key (explicit
|
|
22
|
+
* automation opt-in).
|
|
23
|
+
*
|
|
24
|
+
* Hard floor: converge removes ONLY matrix-declared, package-tagged paths
|
|
25
|
+
* (`converge.reaps`) and runs ONLY matrix-declared uninstall commands. It
|
|
26
|
+
* never deletes user-authored files; any path outside the matrix allowlist
|
|
27
|
+
* is refused.
|
|
28
|
+
*
|
|
29
|
+
* Exit codes: 0 = converged / nothing to do / dry-run · 1 = refused or a
|
|
30
|
+
* step failed · 2 = usage error.
|
|
31
|
+
*/
|
|
32
|
+
import * as fs from 'node:fs';
|
|
33
|
+
import * as os from 'node:os';
|
|
34
|
+
import * as path from 'node:path';
|
|
35
|
+
import process from 'node:process';
|
|
36
|
+
import { spawnSync } from 'node:child_process';
|
|
37
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
|
+
|
|
39
|
+
import * as YAML from 'yaml';
|
|
40
|
+
|
|
41
|
+
import * as user_global_paths from '../_lib/user_global_paths.js';
|
|
42
|
+
|
|
43
|
+
type Dict = Record<string, unknown>;
|
|
44
|
+
|
|
45
|
+
interface OutSink {
|
|
46
|
+
write: (t: string) => void;
|
|
47
|
+
}
|
|
48
|
+
const _stdout: OutSink = { write: (t) => process.stdout.write(t) };
|
|
49
|
+
const _stderr: OutSink = { write: (t) => process.stderr.write(t) };
|
|
50
|
+
|
|
51
|
+
function _print(out: OutSink, line = ''): void {
|
|
52
|
+
out.write(`${line}\n`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type Runner = (cmd: string[]) => number;
|
|
56
|
+
|
|
57
|
+
function _default_runner(cmd: string[]): number {
|
|
58
|
+
const r = spawnSync(cmd[0] as string, cmd.slice(1), { stdio: 'inherit' });
|
|
59
|
+
if (r.error) {
|
|
60
|
+
_stderr.write(`agent-config converge: cannot run ${cmd[0]}: ${String(r.error)}\n`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
return r.status ?? 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
67
|
+
|
|
68
|
+
/** Resolve the @event4u/agent-config package root (this repo). */
|
|
69
|
+
function _package_root(): string {
|
|
70
|
+
return path.resolve(path.dirname(_HERE), '..', '..', '..');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const SETTINGS_FILENAME = 'agent-settings.yml';
|
|
74
|
+
const CONSENT_KEY_PATH = ['install', 'auto_converge'] as const;
|
|
75
|
+
|
|
76
|
+
class ArgparseExit extends Error {
|
|
77
|
+
code: number;
|
|
78
|
+
constructor(code: number) {
|
|
79
|
+
super(`exit ${code}`);
|
|
80
|
+
this.code = code;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface Args {
|
|
85
|
+
dry_run: boolean;
|
|
86
|
+
yes: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const PROG = 'agent-config converge';
|
|
90
|
+
const USAGE = `usage: ${PROG} [-h] [--dry-run] [--yes]\n`;
|
|
91
|
+
|
|
92
|
+
function _argError(msg: string): never {
|
|
93
|
+
_stderr.write(USAGE);
|
|
94
|
+
_stderr.write(`${PROG}: error: ${msg}\n`);
|
|
95
|
+
throw new ArgparseExit(2);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _parse(argv: string[]): Args {
|
|
99
|
+
const args: Args = { dry_run: false, yes: false };
|
|
100
|
+
for (const a of argv) {
|
|
101
|
+
if (a === '-h' || a === '--help') {
|
|
102
|
+
_stdout.write(USAGE);
|
|
103
|
+
throw new ArgparseExit(0);
|
|
104
|
+
}
|
|
105
|
+
if (a === '--dry-run') {
|
|
106
|
+
args.dry_run = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (a === '--yes' || a === '-y') {
|
|
110
|
+
args.yes = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
_argError(`unrecognized arguments: ${a}`);
|
|
114
|
+
}
|
|
115
|
+
return args;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface DuplicateFinding {
|
|
119
|
+
tool: string;
|
|
120
|
+
description: string;
|
|
121
|
+
command: string;
|
|
122
|
+
reaps: string[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface MatrixTool {
|
|
126
|
+
duplicate?: { description?: string; detect?: { all_of?: unknown } };
|
|
127
|
+
converge?: { command?: string; reaps?: unknown };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function _load_matrix(matrix_path: string): Record<string, MatrixTool> {
|
|
131
|
+
const raw = YAML.parse(fs.readFileSync(matrix_path, 'utf-8'), { version: '1.1' }) as Dict;
|
|
132
|
+
const tools = raw['tools'];
|
|
133
|
+
if (!tools || typeof tools !== 'object' || Array.isArray(tools)) {
|
|
134
|
+
throw new Error('surface-matrix.yml: `tools` must be a mapping');
|
|
135
|
+
}
|
|
136
|
+
return tools as Record<string, MatrixTool>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _expand(p: string, home: string, pkg_root: string): string {
|
|
140
|
+
return p.startsWith('~/') ? path.join(home, p.slice(2)) : path.join(pkg_root, p);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function _exists(p: string): boolean {
|
|
144
|
+
try {
|
|
145
|
+
fs.statSync(p);
|
|
146
|
+
return true;
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Detect all live duplicate classes per the matrix. */
|
|
153
|
+
export function detect_duplicates(
|
|
154
|
+
tools: Record<string, MatrixTool>,
|
|
155
|
+
home: string,
|
|
156
|
+
pkg_root: string,
|
|
157
|
+
): DuplicateFinding[] {
|
|
158
|
+
const findings: DuplicateFinding[] = [];
|
|
159
|
+
for (const [tool_id, entry] of Object.entries(tools)) {
|
|
160
|
+
const all_of = entry.duplicate?.detect?.all_of;
|
|
161
|
+
if (!Array.isArray(all_of) || all_of.length === 0) {
|
|
162
|
+
continue; // no defined class (pending_evidence is documented, never acted on)
|
|
163
|
+
}
|
|
164
|
+
const present = all_of.every(
|
|
165
|
+
(p) => typeof p === 'string' && _exists(_expand(p, home, pkg_root)),
|
|
166
|
+
);
|
|
167
|
+
if (!present) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const reaps_raw = entry.converge?.reaps;
|
|
171
|
+
const reaps = Array.isArray(reaps_raw)
|
|
172
|
+
? reaps_raw.filter((r): r is string => typeof r === 'string' && r.startsWith('~/'))
|
|
173
|
+
: [];
|
|
174
|
+
findings.push({
|
|
175
|
+
tool: tool_id,
|
|
176
|
+
description: entry.duplicate?.description ?? 'duplicate install surface',
|
|
177
|
+
command: entry.converge?.command ?? '',
|
|
178
|
+
reaps,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return findings;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Read install.auto_converge from the global settings file. */
|
|
185
|
+
export function read_consent(settings_path: string): boolean {
|
|
186
|
+
try {
|
|
187
|
+
const raw = YAML.parse(fs.readFileSync(settings_path, 'utf-8'), { version: '1.1' }) as Dict;
|
|
188
|
+
const install = raw?.[CONSENT_KEY_PATH[0]];
|
|
189
|
+
if (install && typeof install === 'object' && !Array.isArray(install)) {
|
|
190
|
+
return (install as Dict)[CONSENT_KEY_PATH[1]] === true;
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
/* missing / unreadable → no consent */
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Persist install.auto_converge: true into the global settings file. */
|
|
199
|
+
export function persist_consent(settings_path: string, out: OutSink): void {
|
|
200
|
+
let doc: Dict = {};
|
|
201
|
+
try {
|
|
202
|
+
const parsed = YAML.parse(fs.readFileSync(settings_path, 'utf-8'), { version: '1.1' });
|
|
203
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
204
|
+
doc = parsed as Dict;
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
/* absent → create */
|
|
208
|
+
}
|
|
209
|
+
const install =
|
|
210
|
+
doc['install'] && typeof doc['install'] === 'object' && !Array.isArray(doc['install'])
|
|
211
|
+
? (doc['install'] as Dict)
|
|
212
|
+
: {};
|
|
213
|
+
if (install[CONSENT_KEY_PATH[1]] === true) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
install[CONSENT_KEY_PATH[1]] = true;
|
|
217
|
+
doc['install'] = install;
|
|
218
|
+
fs.mkdirSync(path.dirname(settings_path), { recursive: true });
|
|
219
|
+
fs.writeFileSync(settings_path, YAML.stringify(doc), 'utf-8');
|
|
220
|
+
_print(out, `✅ standing consent persisted: install.auto_converge: true (${settings_path})`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** One-time interactive y/N on a TTY; false everywhere else. */
|
|
224
|
+
function _tty_confirm(question: string): boolean {
|
|
225
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
// Synchronous single-line read from the TTY.
|
|
229
|
+
process.stdout.write(`${question} [y/N] `);
|
|
230
|
+
const buf = Buffer.alloc(64);
|
|
231
|
+
let answer = '';
|
|
232
|
+
try {
|
|
233
|
+
const fd = fs.openSync('/dev/tty', 'rs');
|
|
234
|
+
const n = fs.readSync(fd, buf, 0, 64, null);
|
|
235
|
+
fs.closeSync(fd);
|
|
236
|
+
answer = buf.toString('utf-8', 0, n).trim().toLowerCase();
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
return answer === 'y' || answer === 'yes';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface MainOptions {
|
|
244
|
+
runner?: Runner;
|
|
245
|
+
home?: string;
|
|
246
|
+
package_root?: string;
|
|
247
|
+
settings_path?: string;
|
|
248
|
+
confirm?: (question: string) => boolean;
|
|
249
|
+
out?: OutSink;
|
|
250
|
+
err?: OutSink;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function main(argv: string[] | null = null, opts: MainOptions = {}): number {
|
|
254
|
+
let args: Args;
|
|
255
|
+
try {
|
|
256
|
+
args = _parse(argv ?? process.argv.slice(2));
|
|
257
|
+
} catch (e) {
|
|
258
|
+
if (e instanceof ArgparseExit) {
|
|
259
|
+
return e.code;
|
|
260
|
+
}
|
|
261
|
+
throw e;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const out = opts.out ?? _stdout;
|
|
265
|
+
const err = opts.err ?? _stderr;
|
|
266
|
+
const runner = opts.runner ?? _default_runner;
|
|
267
|
+
const home = opts.home ?? os.homedir();
|
|
268
|
+
const pkg_root = opts.package_root ?? _package_root();
|
|
269
|
+
const settings_path =
|
|
270
|
+
opts.settings_path ?? user_global_paths.write_target(SETTINGS_FILENAME);
|
|
271
|
+
const confirm = opts.confirm ?? _tty_confirm;
|
|
272
|
+
|
|
273
|
+
const matrix_path = path.join(pkg_root, 'src', 'config', 'surface-matrix.yml');
|
|
274
|
+
if (!fs.existsSync(matrix_path)) {
|
|
275
|
+
_print(err, `❌ ${matrix_path} not found — reinstall the package (agent-config upgrade)`);
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
278
|
+
let tools: Record<string, MatrixTool>;
|
|
279
|
+
try {
|
|
280
|
+
tools = _load_matrix(matrix_path);
|
|
281
|
+
} catch (e) {
|
|
282
|
+
_print(err, `❌ surface-matrix.yml unreadable: ${e instanceof Error ? e.message : String(e)}`);
|
|
283
|
+
return 1;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const findings = detect_duplicates(tools, home, pkg_root);
|
|
287
|
+
if (findings.length === 0) {
|
|
288
|
+
_print(out, '✅ no duplicate install surfaces detected — nothing to converge.');
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_print(out, `Duplicate install surface(s) detected on ${findings.length} tool(s):`);
|
|
293
|
+
for (const f of findings) {
|
|
294
|
+
_print(out, ` · ${f.tool}: ${f.description}`);
|
|
295
|
+
}
|
|
296
|
+
_print(out);
|
|
297
|
+
|
|
298
|
+
if (args.dry_run) {
|
|
299
|
+
_print(out, 'Dry run — the following actions WOULD run (nothing was touched):');
|
|
300
|
+
for (const f of findings) {
|
|
301
|
+
if (f.command) {
|
|
302
|
+
_print(out, ` → ${f.command}`);
|
|
303
|
+
}
|
|
304
|
+
for (const reap of f.reaps) {
|
|
305
|
+
_print(out, ` → rm -rf ${reap} (package-tagged orphan, matrix-declared)`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Consent gate — standing key, --yes, or one interactive y/N.
|
|
312
|
+
let consented = args.yes || read_consent(settings_path);
|
|
313
|
+
if (!consented) {
|
|
314
|
+
consented = confirm(
|
|
315
|
+
`Converge now? This runs the per-tool uninstall command(s) above and ` +
|
|
316
|
+
`reaps the matrix-declared caches. Persists install.auto_converge: true.`,
|
|
317
|
+
);
|
|
318
|
+
if (!consented) {
|
|
319
|
+
_print(
|
|
320
|
+
err,
|
|
321
|
+
'❌ converge refused — no consent. Re-run interactively, pass --yes, or set\n' +
|
|
322
|
+
` install.auto_converge: true in ${settings_path}`,
|
|
323
|
+
);
|
|
324
|
+
return 1;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
persist_consent(settings_path, out);
|
|
328
|
+
|
|
329
|
+
// Perform the cleanup — matrix-declared actions ONLY.
|
|
330
|
+
let failures = 0;
|
|
331
|
+
const report: string[] = [];
|
|
332
|
+
for (const f of findings) {
|
|
333
|
+
if (f.command) {
|
|
334
|
+
_print(out, `→ ${f.command}`);
|
|
335
|
+
const rc = runner(f.command.split(' '));
|
|
336
|
+
if (rc !== 0) {
|
|
337
|
+
failures += 1;
|
|
338
|
+
_print(err, `⚠️ ${f.tool}: uninstall command exited ${rc} — continuing with reaps.`);
|
|
339
|
+
} else {
|
|
340
|
+
report.push(`${f.tool}: ran \`${f.command}\``);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (const reap of f.reaps) {
|
|
344
|
+
// Hard floor: reap paths come from the matrix allowlist only and
|
|
345
|
+
// are always ~/-anchored (enforced at load; non-~/ entries were
|
|
346
|
+
// filtered in detect_duplicates).
|
|
347
|
+
const abs = _expand(reap, home, pkg_root);
|
|
348
|
+
if (!_exists(abs)) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
fs.rmSync(abs, { recursive: true, force: true });
|
|
353
|
+
report.push(`${f.tool}: reaped package-tagged orphan \`${reap}\``);
|
|
354
|
+
_print(out, `→ reaped ${reap}`);
|
|
355
|
+
} catch (e) {
|
|
356
|
+
failures += 1;
|
|
357
|
+
_print(err, `⚠️ ${f.tool}: could not reap ${reap}: ${e instanceof Error ? e.message : String(e)}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
_print(out);
|
|
363
|
+
_print(out, 'Convergence report:');
|
|
364
|
+
for (const line of report) {
|
|
365
|
+
_print(out, ` ✅ ${line}`);
|
|
366
|
+
}
|
|
367
|
+
if (report.length === 0) {
|
|
368
|
+
_print(out, ' (no actions completed)');
|
|
369
|
+
}
|
|
370
|
+
_print(
|
|
371
|
+
out,
|
|
372
|
+
'Rollback hint: a removed Claude Code plugin can be reinstalled with\n' +
|
|
373
|
+
' claude plugin marketplace add event4u-app/agent-config && ' +
|
|
374
|
+
'claude plugin install agent-config@event4u-agent-config\n' +
|
|
375
|
+
'(the file projection is untouched — content and hooks keep working).',
|
|
376
|
+
);
|
|
377
|
+
return failures > 0 ? 1 : 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function _isCliEntry(): boolean {
|
|
381
|
+
if (process.argv[1] === undefined) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
385
|
+
if (import.meta.url === argvUrl) {
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
390
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
391
|
+
return here === argv;
|
|
392
|
+
} catch {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (_isCliEntry()) {
|
|
398
|
+
process.exit(main());
|
|
399
|
+
}
|
|
@@ -731,6 +731,7 @@ const CHECK_IDS = [
|
|
|
731
731
|
'scope',
|
|
732
732
|
'global-binary',
|
|
733
733
|
'claude-plugin',
|
|
734
|
+
'surface-state',
|
|
734
735
|
'hook-wiring',
|
|
735
736
|
'stale-orphans',
|
|
736
737
|
'manifest-integrity',
|
|
@@ -751,6 +752,7 @@ const GLOBAL_CHECK_IDS: ReadonlySet<string> = new Set([
|
|
|
751
752
|
'scope',
|
|
752
753
|
'global-binary',
|
|
753
754
|
'claude-plugin',
|
|
755
|
+
'surface-state',
|
|
754
756
|
'hook-wiring',
|
|
755
757
|
'stale-orphans',
|
|
756
758
|
'mcp-mode',
|
|
@@ -1075,6 +1077,96 @@ function _check_claude_plugin(): Dict {
|
|
|
1075
1077
|
};
|
|
1076
1078
|
}
|
|
1077
1079
|
|
|
1080
|
+
/**
|
|
1081
|
+
* Matrix-driven duplicate-surface check (road-to-install-path-convergence
|
|
1082
|
+
* Phase 2): reads src/config/surface-matrix.yml and flags every tool whose
|
|
1083
|
+
* declared duplicate-class detect paths ALL exist on this machine.
|
|
1084
|
+
* Generalizes the `claude-plugin` check — which stays as the named first
|
|
1085
|
+
* consumer — to every tool with a defined duplicate class; tools marked
|
|
1086
|
+
* `pending_evidence` are documented but never acted on.
|
|
1087
|
+
*/
|
|
1088
|
+
function _check_surface_state(): Dict {
|
|
1089
|
+
const matrix_path = path.join(_package_root(), 'src', 'config', 'surface-matrix.yml');
|
|
1090
|
+
if (!isFile(matrix_path)) {
|
|
1091
|
+
return {
|
|
1092
|
+
id: 'surface-state',
|
|
1093
|
+
status: 'skipped',
|
|
1094
|
+
message: 'surface-matrix.yml not found in this package',
|
|
1095
|
+
remedy: '',
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
let tools: Dict = {};
|
|
1099
|
+
try {
|
|
1100
|
+
const raw = YAML.parse(readText(matrix_path), { version: '1.1' }) as Dict;
|
|
1101
|
+
const t = raw['tools'];
|
|
1102
|
+
if (t && typeof t === 'object' && !Array.isArray(t)) {
|
|
1103
|
+
tools = t as Dict;
|
|
1104
|
+
}
|
|
1105
|
+
} catch {
|
|
1106
|
+
return {
|
|
1107
|
+
id: 'surface-state',
|
|
1108
|
+
status: 'warn',
|
|
1109
|
+
message: 'surface-matrix.yml unreadable — duplicate-surface state unknown',
|
|
1110
|
+
remedy: 'agent-config upgrade (reinstalls the package data files)',
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const _expand = (p: string): string =>
|
|
1115
|
+
p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : path.join(_package_root(), p);
|
|
1116
|
+
|
|
1117
|
+
const violations: string[] = [];
|
|
1118
|
+
const remedies: string[] = [];
|
|
1119
|
+
let checked = 0;
|
|
1120
|
+
for (const [tool_id, entry] of Object.entries(tools)) {
|
|
1121
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
const dup = (entry as Dict)['duplicate'] as Dict | undefined;
|
|
1125
|
+
const detect = dup ? (dup['detect'] as Dict | undefined) : undefined;
|
|
1126
|
+
const all_of = detect ? (detect['all_of'] as unknown) : undefined;
|
|
1127
|
+
if (!Array.isArray(all_of) || all_of.length === 0) {
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
checked += 1;
|
|
1131
|
+
const present = all_of.every((p) => typeof p === 'string' && pathExistsAny(_expand(p)));
|
|
1132
|
+
if (present) {
|
|
1133
|
+
violations.push(tool_id);
|
|
1134
|
+
const conv = (entry as Dict)['converge'] as Dict | undefined;
|
|
1135
|
+
const cmd = conv ? String(conv['command'] ?? '') : '';
|
|
1136
|
+
if (cmd) {
|
|
1137
|
+
remedies.push(cmd);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
if (violations.length > 0) {
|
|
1143
|
+
return {
|
|
1144
|
+
id: 'surface-state',
|
|
1145
|
+
status: 'fail',
|
|
1146
|
+
message:
|
|
1147
|
+
`duplicate surface on ${violations.length} tool(s): ${violations.join(', ')} — ` +
|
|
1148
|
+
'canonical + secondary install surfaces are both present',
|
|
1149
|
+
remedy: `${remedies.join(' · ')}${remedies.length ? ' — or: ' : ''}agent-config converge`,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
id: 'surface-state',
|
|
1154
|
+
status: 'ok',
|
|
1155
|
+
message: `${checked} declared duplicate class(es) checked, none present`,
|
|
1156
|
+
remedy: '',
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/** exists() that is true for files AND directories (surface detect paths). */
|
|
1161
|
+
function pathExistsAny(p: string): boolean {
|
|
1162
|
+
try {
|
|
1163
|
+
fs.statSync(p);
|
|
1164
|
+
return true;
|
|
1165
|
+
} catch {
|
|
1166
|
+
return false;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1078
1170
|
/**
|
|
1079
1171
|
* Managed hook wiring (single-surface model): the deterministic hook matrix
|
|
1080
1172
|
* must be registered in ~/.claude/settings.json AND the dispatch target
|
|
@@ -1916,6 +2008,7 @@ function _run_checks(
|
|
|
1916
2008
|
scope: () => _check_scope(project_root),
|
|
1917
2009
|
'global-binary': () => _check_global_binary(project_root),
|
|
1918
2010
|
'claude-plugin': _check_claude_plugin,
|
|
2011
|
+
'surface-state': _check_surface_state,
|
|
1919
2012
|
'hook-wiring': _check_hook_wiring,
|
|
1920
2013
|
'stale-orphans': _check_stale_orphans,
|
|
1921
2014
|
'manifest-integrity': () => _check_manifest_integrity(manifest),
|
|
@@ -1989,6 +2082,7 @@ function _run_checks_no_manifest(
|
|
|
1989
2082
|
scope: () => _check_scope(project_root),
|
|
1990
2083
|
'global-binary': () => _check_global_binary(project_root),
|
|
1991
2084
|
'claude-plugin': _check_claude_plugin,
|
|
2085
|
+
'surface-state': _check_surface_state,
|
|
1992
2086
|
'hook-wiring': _check_hook_wiring,
|
|
1993
2087
|
'stale-orphans': _check_stale_orphans,
|
|
1994
2088
|
'manifest-integrity': () => _skipped_manifest_check('manifest-integrity'),
|
|
@@ -90,7 +90,7 @@ import * as installed_lock from '../_lib/installed_lock.js';
|
|
|
90
90
|
import * as update_check from '../_lib/update_check.js';
|
|
91
91
|
import * as cli_wrapper from '../_lib/cli_wrapper.js';
|
|
92
92
|
import * as claude_plugin from '../_lib/claude_plugin.js';
|
|
93
|
-
import { event4u_root } from '../_lib/user_global_paths.js';
|
|
93
|
+
import { event4u_root, write_target } from '../_lib/user_global_paths.js';
|
|
94
94
|
import { project_settings_path } from '../_lib/agent_settings.js';
|
|
95
95
|
import { _is_source_repo } from './cmd_refresh.js';
|
|
96
96
|
|
|
@@ -407,10 +407,11 @@ function _refresh_claude_plugin(
|
|
|
407
407
|
interface Args {
|
|
408
408
|
check: boolean;
|
|
409
409
|
dry_run: boolean;
|
|
410
|
+
converge: boolean;
|
|
410
411
|
}
|
|
411
412
|
|
|
412
413
|
const PROG = 'agent-config upgrade';
|
|
413
|
-
const USAGE = `usage: ${PROG} [-h] [--check] [--dry-run]\n`;
|
|
414
|
+
const USAGE = `usage: ${PROG} [-h] [--check] [--dry-run] [--converge]\n`;
|
|
414
415
|
|
|
415
416
|
function _argError(msg: string): never {
|
|
416
417
|
_stderr.write(USAGE);
|
|
@@ -419,7 +420,7 @@ function _argError(msg: string): never {
|
|
|
419
420
|
}
|
|
420
421
|
|
|
421
422
|
function _parse(argv: string[]): Args {
|
|
422
|
-
const args: Args = { check: false, dry_run: false };
|
|
423
|
+
const args: Args = { check: false, dry_run: false, converge: false };
|
|
423
424
|
const positionals: string[] = [];
|
|
424
425
|
let i = 0;
|
|
425
426
|
while (i < argv.length) {
|
|
@@ -447,6 +448,14 @@ function _parse(argv: string[]): Args {
|
|
|
447
448
|
i += 1;
|
|
448
449
|
continue;
|
|
449
450
|
}
|
|
451
|
+
if (flag === '--converge') {
|
|
452
|
+
if (inlineVal !== null) {
|
|
453
|
+
_argError(`argument --converge: ignored explicit argument '${inlineVal}'`);
|
|
454
|
+
}
|
|
455
|
+
args.converge = true;
|
|
456
|
+
i += 1;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
450
459
|
if (a.startsWith('-') && a !== '-') {
|
|
451
460
|
_argError(`unrecognized arguments: ${a}`);
|
|
452
461
|
}
|
|
@@ -565,19 +574,34 @@ export async function main(argv: string[] | null = null, options: MainOptions =
|
|
|
565
574
|
// the plugin is retired this block disappears with it.
|
|
566
575
|
_refresh_claude_plugin(plugin_steps, runner, out, err);
|
|
567
576
|
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
)
|
|
577
|
+
// Duplicate-surface convergence (road-to-install-path-convergence
|
|
578
|
+
// Phase 3). Consent model (council Q2, 2026-07-07):
|
|
579
|
+
// --converge → explicit consent for this run; persists
|
|
580
|
+
// install.auto_converge: true as standing consent.
|
|
581
|
+
// standing key true → auto-converge (the user opted in earlier).
|
|
582
|
+
// no key + TTY → converge asks ONE y/N when duplicates exist.
|
|
583
|
+
// no key + non-TTY → today's print-only prompt; NEVER a silent
|
|
584
|
+
// mutation of a user-owned surface.
|
|
585
|
+
{
|
|
586
|
+
const converge_mod = await import('./cmd_converge.js');
|
|
587
|
+
const consent = converge_mod.read_consent(write_target('agent-settings.yml'));
|
|
588
|
+
const tty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
589
|
+
if (args.converge || consent || tty) {
|
|
590
|
+
const rc = converge_mod.main(args.converge ? ['--yes'] : [], { out, err });
|
|
591
|
+
if (rc !== 0 && (args.converge || consent)) {
|
|
592
|
+
failed_essential.push('agent-config converge');
|
|
593
|
+
}
|
|
594
|
+
} else if (plugin_steps.length > 0) {
|
|
595
|
+
_print(
|
|
596
|
+
out,
|
|
597
|
+
'ℹ️ Claude Code marketplace plugin detected — deprecated surface. Hooks now ' +
|
|
598
|
+
'live in ~/.claude/settings.json (wired by the global step above), so the ' +
|
|
599
|
+
'plugin only duplicates skills/commands. Remove it with:\n' +
|
|
600
|
+
' claude plugin uninstall ' +
|
|
601
|
+
`${claude_plugin.CLAUDE_PLUGIN_ID}@${claude_plugin.CLAUDE_MARKETPLACE_NAME}\n` +
|
|
602
|
+
' (or: agent-config converge)',
|
|
603
|
+
);
|
|
604
|
+
}
|
|
581
605
|
}
|
|
582
606
|
|
|
583
607
|
// Bring existing settings files up to the NEW template (additive; the
|
|
@@ -105,6 +105,11 @@ Tier 1 — power-user (release shape, audit, migration):
|
|
|
105
105
|
Lists missing, modified, and foreign files.
|
|
106
106
|
Exits 1 on drift, 2 on missing lockfile.
|
|
107
107
|
Flags: --json | --project=<path>
|
|
108
|
+
converge Consented cleanup of duplicate install surfaces
|
|
109
|
+
(surface-matrix driven). Never touches
|
|
110
|
+
user-authored files; persists standing consent
|
|
111
|
+
(install.auto_converge) on first use.
|
|
112
|
+
Flags: --dry-run | --yes
|
|
108
113
|
explain Read-only decision-chain trace.
|
|
109
114
|
Usage: explain config | explain rule <name>
|
|
110
115
|
| explain route "<text>"
|
|
@@ -959,6 +964,13 @@ cmd_doctor() {
|
|
|
959
964
|
exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_doctor.ts" "$@"
|
|
960
965
|
}
|
|
961
966
|
|
|
967
|
+
# `agent-config converge` — consented duplicate-surface cleanup.
|
|
968
|
+
# Exit codes: 0 converged/nothing-to-do/dry-run · 1 refused/failed · 2 usage.
|
|
969
|
+
# See scripts/_cli/cmd_converge.ts.
|
|
970
|
+
cmd_converge() {
|
|
971
|
+
exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_converge.ts" "$@"
|
|
972
|
+
}
|
|
973
|
+
|
|
962
974
|
# `agent-config conformance` — consumer conformance contract
|
|
963
975
|
# (road-to-flow-learnings Phase 0). Doctor --ci semantics plus the five
|
|
964
976
|
# consumer checks (txlog tail, router pointers, dispatcher smoke,
|
|
@@ -1048,6 +1060,7 @@ main() {
|
|
|
1048
1060
|
uninstall) cmd_uninstall "$@" ;;
|
|
1049
1061
|
prune) cmd_prune "$@" ;;
|
|
1050
1062
|
doctor) cmd_doctor "$@" ;;
|
|
1063
|
+
converge) cmd_converge "$@" ;;
|
|
1051
1064
|
conformance) cmd_conformance "$@" ;;
|
|
1052
1065
|
versions) cmd_versions "$@" ;;
|
|
1053
1066
|
explain) cmd_explain "$@" ;;
|