@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
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* prepack-check.mjs — guard the published artifact.
|
|
4
4
|
*
|
|
5
|
-
* Runs during `npm pack`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
+
}
|