@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.
Files changed (140) hide show
  1. package/.claude-plugin/marketplace.json +4 -426
  2. package/.claude-plugin/skills/install-agent-config/SKILL.md +32 -0
  3. package/CHANGELOG.md +25 -0
  4. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  5. package/dist/cli/registry.js +1 -0
  6. package/dist/cli/registry.js.map +1 -1
  7. package/dist/discovery/deprecation-report.md +1 -1
  8. package/dist/discovery/discovery-manifest.json +1 -1
  9. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  10. package/dist/discovery/discovery-manifest.summary.md +1 -1
  11. package/dist/discovery/orphan-report.md +1 -1
  12. package/dist/discovery/packs.json +1 -1
  13. package/dist/discovery/trust-report.md +1 -1
  14. package/dist/discovery/workspaces.json +1 -1
  15. package/dist/mcp/registry-manifest.json +1 -1
  16. package/docs/contracts/surface-matrix.md +103 -0
  17. package/package.json +4 -1
  18. package/src/agent-src/README.md +64 -0
  19. package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
  20. package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
  21. package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
  22. package/src/agent-src/templates/AGENTS.md +34 -0
  23. package/src/agent-src/templates/scripts/README.md +195 -0
  24. package/src/agent-src/templates/scripts/check_memory.ts +662 -0
  25. package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
  26. package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
  27. package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
  28. package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
  29. package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
  30. package/src/agent-src/templates/scripts/memory_report.ts +868 -0
  31. package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
  32. package/src/agent-src/templates/scripts/memory_status.ts +215 -0
  33. package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
  34. package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
  35. package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
  36. package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
  37. package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
  38. package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
  39. package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
  40. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
  41. package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
  42. package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
  43. package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
  44. package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
  45. package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
  46. package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
  47. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
  48. package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
  49. package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
  50. package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
  51. package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
  52. package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
  53. package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
  54. package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
  55. package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
  56. package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
  57. package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
  58. package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
  59. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
  60. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
  61. package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
  62. package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
  63. package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
  64. package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
  65. package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
  66. package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
  67. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
  68. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
  73. package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
  74. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
  75. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
  76. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
  77. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
  78. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
  79. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
  80. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
  81. package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
  82. package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
  83. package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
  84. package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
  85. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
  86. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
  87. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
  88. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
  89. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
  90. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
  91. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
  92. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
  93. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
  94. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
  95. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
  96. package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
  97. package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
  98. package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
  99. package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
  100. package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
  101. package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
  102. package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
  103. package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
  104. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
  105. package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
  106. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
  107. package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
  108. package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
  109. package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
  110. package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
  111. package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
  112. package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
  113. package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
  114. package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
  115. package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
  116. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
  117. package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
  118. package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
  119. package/src/config/surface-matrix.yml +213 -0
  120. package/src/install/atomic.ts +111 -0
  121. package/src/install/conflict.ts +226 -0
  122. package/src/install/detect.ts +280 -0
  123. package/src/install/paths.ts +90 -0
  124. package/src/install/plan.ts +205 -0
  125. package/src/install/preflight.ts +210 -0
  126. package/src/install/selectedTools.ts +52 -0
  127. package/src/install/toolDetection.ts +120 -0
  128. package/src/install/txlog.ts +161 -0
  129. package/src/install/types.ts +175 -0
  130. package/src/install/wizard-plan.ts +216 -0
  131. package/src/scripts/_cli/cmd_converge.ts +399 -0
  132. package/src/scripts/_cli/cmd_doctor.ts +94 -0
  133. package/src/scripts/_cli/cmd_upgrade.ts +40 -16
  134. package/src/scripts/_dispatch.bash +13 -0
  135. package/src/scripts/condense.ts +67 -36
  136. package/src/scripts/hook_manifest.yaml +16 -7
  137. package/src/scripts/lint_marketplace.ts +54 -56
  138. package/src/scripts/lint_surface_matrix.ts +200 -0
  139. package/src/scripts/prepack-check.mjs +108 -5
  140. package/src/scripts/surface_probe_hook.ts +231 -0
@@ -2,11 +2,16 @@
2
2
  /**
3
3
  * prepack-check.mjs — guard the published artifact.
4
4
  *
5
- * Runs during `npm pack` and `npm publish`. Asserts the compiled TS
6
- * CLI binary exists, is executable, and carries the Node shebang
7
- * before the tarball is built. Otherwise a silently-broken `dist/`
8
- * ships and every `npx @event4u/agent-config` greets the consumer
9
- * with a cryptic `Cannot find module` panic.
5
+ * Runs during `npm pack` / `npm publish` AND as a dry-run on every PR
6
+ * (Static Checks workflow). Two gates:
7
+ *
8
+ * 1. Compiled-bin shape — the TS CLI binary exists, is executable, and
9
+ * carries the Node shebang before the tarball is built. Otherwise a
10
+ * silently-broken `dist/` ships and every `npx @event4u/agent-config`
11
+ * greets the consumer with a cryptic `Cannot find module` panic.
12
+ * 2. Shipped-import completeness — every relative import reachable from
13
+ * the shipped `src/` trees resolves to a file the `files` whitelist
14
+ * actually ships (the 8.3.0 ERR_MODULE_NOT_FOUND class).
10
15
  *
11
16
  * Folded from external council pass 2026-05-18 (Phase 1.3 acceptance).
12
17
  *
@@ -56,6 +61,104 @@ if (head !== SHEBANG) {
56
61
 
57
62
  process.stderr.write(`prepack-check: ${BIN} OK\n`);
58
63
 
64
+ // ---------------------------------------------------------------------------
65
+ // Import-completeness guard: every relative import reachable from the
66
+ // SHIPPED src/ trees must itself resolve to a shipped file. Catches the
67
+ // 8.3.0 class of bug where src/scripts/_lib/claude_settings_hooks.ts
68
+ // imported src/install/atomic.js while the `files` whitelist did not ship
69
+ // src/install/ — the global install then crashed doctor/conformance with
70
+ // ERR_MODULE_NOT_FOUND. Deterministic, no network, no npm pack invocation:
71
+ // the shipped set is derived from the package.json `files` whitelist.
72
+ // ---------------------------------------------------------------------------
73
+ import { readdirSync } from 'node:fs';
74
+ import { dirname, join, relative, sep } from 'node:path';
75
+
76
+ const pkg = JSON.parse(readFileSync(resolve('package.json'), 'utf8'));
77
+ const shippedPrefixes = (pkg.files ?? [])
78
+ .filter((f) => f.endsWith('/'))
79
+ .map((f) => f.replace(/\/+$/, '') + '/');
80
+ const shippedFiles = new Set((pkg.files ?? []).filter((f) => !f.endsWith('/')));
81
+
82
+ function isShipped(relPath) {
83
+ const posix = relPath.split(sep).join('/');
84
+ if (shippedFiles.has(posix)) return true;
85
+ return shippedPrefixes.some((p) => posix.startsWith(p));
86
+ }
87
+
88
+ function* walk(dir) {
89
+ let entries;
90
+ try {
91
+ entries = readdirSync(dir, { withFileTypes: true });
92
+ } catch {
93
+ return;
94
+ }
95
+ for (const e of entries) {
96
+ const full = join(dir, e.name);
97
+ if (e.isDirectory()) {
98
+ yield* walk(full);
99
+ } else if (/\.(ts|mts|mjs|js)$/.test(e.name) && !e.name.endsWith('.d.ts')) {
100
+ yield full;
101
+ }
102
+ }
103
+ }
104
+
105
+ const IMPORT_RE = /(?:from\s+|import\s*\(\s*|require\s*\(\s*)['"](\.\.?\/[^'"]+)['"]/g;
106
+
107
+ function resolveCandidates(fromDir, spec) {
108
+ const base = join(fromDir, spec);
109
+ return [
110
+ base,
111
+ base.replace(/\.js$/, '.ts'),
112
+ base.replace(/\.mjs$/, '.mts'),
113
+ `${base}.ts`,
114
+ `${base}.js`,
115
+ join(base, 'index.ts'),
116
+ join(base, 'index.js'),
117
+ ];
118
+ }
119
+
120
+ const importErrors = [];
121
+ const scanRoots = shippedPrefixes.filter((p) => p.startsWith('src/'));
122
+ for (const root of scanRoots) {
123
+ for (const file of walk(resolve(root))) {
124
+ const text = readFileSync(file, 'utf8');
125
+ for (const m of text.matchAll(IMPORT_RE)) {
126
+ const spec = m[1];
127
+ const candidates = resolveCandidates(dirname(file), spec);
128
+ const hit = candidates.find((c) => {
129
+ try {
130
+ return statSync(c).isFile();
131
+ } catch {
132
+ return false;
133
+ }
134
+ });
135
+ const fileRel = relative(resolve('.'), file);
136
+ if (hit === undefined) {
137
+ importErrors.push(`${fileRel}: unresolvable relative import '${spec}'`);
138
+ continue;
139
+ }
140
+ const hitRel = relative(resolve('.'), hit);
141
+ if (!isShipped(hitRel)) {
142
+ importErrors.push(
143
+ `${fileRel}: imports '${spec}' → ${hitRel}, which is NOT in the ` +
144
+ 'package.json `files` whitelist (would crash the global install ' +
145
+ 'with ERR_MODULE_NOT_FOUND)',
146
+ );
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ if (importErrors.length > 0) {
153
+ for (const e of importErrors) {
154
+ process.stderr.write(`prepack-check: ${e}\n`);
155
+ }
156
+ die(`${importErrors.length} shipped import(s) escape the files whitelist.`);
157
+ }
158
+ process.stderr.write(
159
+ `prepack-check: import-completeness OK (${scanRoots.length} shipped src tree(s) scanned)\n`,
160
+ );
161
+
59
162
  // Optional: invoked with --verbose dumps the size for tarball-budget bookkeeping.
60
163
  if (argv.includes('--verbose')) {
61
164
  process.stderr.write(`prepack-check: size=${st.size} bytes\n`);
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Duplicate-surface probe — `session_start` hook
4
+ * (road-to-install-path-convergence Phase 4).
5
+ *
6
+ * Cheap runtime self-detection on every install path, even for users who
7
+ * never run `agent-config doctor`: reads src/config/surface-matrix.yml from
8
+ * the package root, existence-checks each declared duplicate class, and — at
9
+ * most once per day — emits ONE stderr nudge naming the duplicate plus the
10
+ * converge command.
11
+ *
12
+ * Contract:
13
+ * - fail-open, never blocking — returns 0 on every path, including
14
+ * unreadable matrix, corrupted state file, and unwritable state dir.
15
+ * - rate-limited via agents/runtime/state/surface-probe.json in the
16
+ * CONSUMER project (last_check_utc, 24h window — same precedent as
17
+ * _lib/update_check.ts). A corrupted state file counts as "due" and is
18
+ * rewritten.
19
+ * - suppressed entirely when the surface is clean or when
20
+ * install.auto_converge is already true in the global settings (the
21
+ * user has standing consent — converge handles it, no nagging).
22
+ */
23
+
24
+ import fs from 'node:fs';
25
+ import os from 'node:os';
26
+ import path from 'node:path';
27
+ import { fileURLToPath, pathToFileURL } from 'node:url';
28
+ import { parse as parseYaml } from 'yaml';
29
+
30
+ import { is_replay_mode } from './hooks/state_io.js';
31
+
32
+ const CHECK_WINDOW_MS = 24 * 60 * 60 * 1000;
33
+ const STATE_REL = ['agents', 'runtime', 'state', 'surface-probe.json'] as const;
34
+
35
+ function _project_root(): string {
36
+ const env = process.env['CLAUDE_PROJECT_DIR'] || process.env['AGENT_CONFIG_PROJECT_DIR'];
37
+ if (env) {
38
+ return env;
39
+ }
40
+ return process.cwd();
41
+ }
42
+
43
+ function _package_root(): string {
44
+ const env = process.env['AGENT_CONFIG_PACKAGE_ROOT'];
45
+ if (env) {
46
+ return env;
47
+ }
48
+ // surface_probe_hook.ts lives at src/scripts/; parents[2] = package root.
49
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
50
+ }
51
+
52
+ function _readStdinIfNotTty(): void {
53
+ try {
54
+ if (!process.stdin.isTTY) {
55
+ fs.readFileSync(0, 'utf-8');
56
+ }
57
+ } catch {
58
+ /* ignore */
59
+ }
60
+ }
61
+
62
+ function _exists(p: string): boolean {
63
+ try {
64
+ fs.statSync(p);
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ interface ProbeOptions {
72
+ project_root?: string;
73
+ package_root?: string;
74
+ home?: string;
75
+ global_settings_path?: string;
76
+ now_ms?: number;
77
+ }
78
+
79
+ /** Fail-open read of install.auto_converge from the global settings file. */
80
+ function _auto_converge(global_settings_path: string): boolean {
81
+ try {
82
+ const raw = parseYaml(fs.readFileSync(global_settings_path, 'utf-8'), { version: '1.1' }) as
83
+ | Record<string, unknown>
84
+ | null;
85
+ const install = raw?.['install'];
86
+ if (install && typeof install === 'object' && !Array.isArray(install)) {
87
+ return (install as Record<string, unknown>)['auto_converge'] === true;
88
+ }
89
+ } catch {
90
+ /* absent / unreadable → false */
91
+ }
92
+ return false;
93
+ }
94
+
95
+ function _default_global_settings(home: string): string {
96
+ return path.join(home, '.event4u', 'agent-config', 'agent-settings.yml');
97
+ }
98
+
99
+ /** Rate limit: true when the probe already ran inside the window. */
100
+ function _rate_limited(state_path: string, now_ms: number): boolean {
101
+ try {
102
+ const raw = JSON.parse(fs.readFileSync(state_path, 'utf-8')) as Record<string, unknown>;
103
+ const last = raw['last_check_utc'];
104
+ if (typeof last === 'number' && now_ms - last < CHECK_WINDOW_MS && last <= now_ms) {
105
+ return true;
106
+ }
107
+ } catch {
108
+ /* missing / corrupted → due */
109
+ }
110
+ return false;
111
+ }
112
+
113
+ function _stamp(state_path: string, now_ms: number): void {
114
+ try {
115
+ fs.mkdirSync(path.dirname(state_path), { recursive: true });
116
+ fs.writeFileSync(state_path, `${JSON.stringify({ last_check_utc: now_ms })}\n`, 'utf-8');
117
+ } catch {
118
+ /* unwritable state dir → fail open, probe just runs again next time */
119
+ }
120
+ }
121
+
122
+ interface Finding {
123
+ tool: string;
124
+ command: string;
125
+ }
126
+
127
+ function _detect(pkg_root: string, home: string): Finding[] {
128
+ const matrix_path = path.join(pkg_root, 'src', 'config', 'surface-matrix.yml');
129
+ let tools: Record<string, unknown> = {};
130
+ try {
131
+ const raw = parseYaml(fs.readFileSync(matrix_path, 'utf-8'), { version: '1.1' }) as Record<
132
+ string,
133
+ unknown
134
+ >;
135
+ const t = raw?.['tools'];
136
+ if (t && typeof t === 'object' && !Array.isArray(t)) {
137
+ tools = t as Record<string, unknown>;
138
+ }
139
+ } catch {
140
+ return []; // unreadable matrix → fail open, no nudge
141
+ }
142
+
143
+ const expand = (p: string): string =>
144
+ p.startsWith('~/') ? path.join(home, p.slice(2)) : path.join(pkg_root, p);
145
+
146
+ const findings: Finding[] = [];
147
+ for (const [tool_id, entry] of Object.entries(tools)) {
148
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
149
+ continue;
150
+ }
151
+ const e = entry as Record<string, unknown>;
152
+ const dup = e['duplicate'] as Record<string, unknown> | undefined;
153
+ const detect = dup?.['detect'] as Record<string, unknown> | undefined;
154
+ const all_of = detect?.['all_of'];
155
+ if (!Array.isArray(all_of) || all_of.length === 0) {
156
+ continue;
157
+ }
158
+ const present = all_of.every((p) => typeof p === 'string' && _exists(expand(p)));
159
+ if (!present) {
160
+ continue;
161
+ }
162
+ const conv = e['converge'] as Record<string, unknown> | undefined;
163
+ findings.push({ tool: tool_id, command: String(conv?.['command'] ?? '') });
164
+ }
165
+ return findings;
166
+ }
167
+
168
+ export function main(argv?: string[], opts: ProbeOptions = {}): number {
169
+ void argv;
170
+ _readStdinIfNotTty();
171
+
172
+ // Replay contract (state_io): fixture re-execution must not mutate
173
+ // state or emit nudges — the probe is a live-session UX concern only.
174
+ if (is_replay_mode()) {
175
+ return 0;
176
+ }
177
+
178
+ try {
179
+ const project_root = opts.project_root ?? _project_root();
180
+ const pkg_root = opts.package_root ?? _package_root();
181
+ const home = opts.home ?? os.homedir();
182
+ const global_settings = opts.global_settings_path ?? _default_global_settings(home);
183
+ const now_ms = opts.now_ms ?? Date.now();
184
+
185
+ const state_path = path.join(project_root, ...STATE_REL);
186
+ if (_rate_limited(state_path, now_ms)) {
187
+ return 0;
188
+ }
189
+ _stamp(state_path, now_ms);
190
+
191
+ if (_auto_converge(global_settings)) {
192
+ return 0; // standing consent — converge owns the cleanup, no nag
193
+ }
194
+
195
+ const findings = _detect(pkg_root, home);
196
+ if (findings.length === 0) {
197
+ return 0; // clean surface — fully silent
198
+ }
199
+
200
+ const tools = findings.map((f) => f.tool).join(', ');
201
+ const fix = findings.find((f) => f.command !== '')?.command ?? '';
202
+ process.stderr.write(
203
+ `[surface] duplicate install surface: ${tools} — run: agent-config converge` +
204
+ `${fix ? ` (or: ${fix})` : ''}\n`,
205
+ );
206
+ return 0;
207
+ } catch {
208
+ return 0; // fail-open — a probe must never block a session
209
+ }
210
+ }
211
+
212
+ function _isCliEntry(): boolean {
213
+ if (process.argv[1] === undefined) {
214
+ return false;
215
+ }
216
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
217
+ if (import.meta.url === argvUrl) {
218
+ return true;
219
+ }
220
+ try {
221
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
222
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
223
+ return here === argv;
224
+ } catch {
225
+ return false;
226
+ }
227
+ }
228
+
229
+ if (_isCliEntry()) {
230
+ process.exit(main(process.argv.slice(2)));
231
+ }