@event4u/agent-config 8.4.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 (126) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  4. package/dist/discovery/deprecation-report.md +1 -1
  5. package/dist/discovery/discovery-manifest.json +1 -1
  6. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  7. package/dist/discovery/discovery-manifest.summary.md +1 -1
  8. package/dist/discovery/orphan-report.md +1 -1
  9. package/dist/discovery/packs.json +1 -1
  10. package/dist/discovery/trust-report.md +1 -1
  11. package/dist/discovery/workspaces.json +1 -1
  12. package/dist/mcp/registry-manifest.json +1 -1
  13. package/package.json +4 -1
  14. package/src/agent-src/README.md +64 -0
  15. package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
  16. package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
  17. package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
  18. package/src/agent-src/templates/AGENTS.md +34 -0
  19. package/src/agent-src/templates/scripts/README.md +195 -0
  20. package/src/agent-src/templates/scripts/check_memory.ts +662 -0
  21. package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
  22. package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
  23. package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
  24. package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
  25. package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
  26. package/src/agent-src/templates/scripts/memory_report.ts +868 -0
  27. package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
  28. package/src/agent-src/templates/scripts/memory_status.ts +215 -0
  29. package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
  30. package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
  31. package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
  32. package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
  33. package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
  34. package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
  35. package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
  36. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
  37. package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
  38. package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
  39. package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
  40. package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
  41. package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
  42. package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
  43. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
  44. package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
  45. package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
  46. package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
  47. package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
  48. package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
  49. package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
  50. package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
  51. package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
  52. package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
  53. package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
  54. package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
  55. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
  56. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
  57. package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
  58. package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
  59. package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
  60. package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
  61. package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
  62. package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
  63. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
  64. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
  65. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
  66. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
  67. package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
  68. package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
  73. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
  74. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
  75. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
  76. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
  77. package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
  78. package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
  79. package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
  80. package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
  81. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
  82. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
  83. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
  84. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
  85. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
  86. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
  87. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
  88. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
  89. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
  90. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
  91. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
  92. package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
  93. package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
  94. package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
  95. package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
  96. package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
  97. package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
  98. package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
  99. package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
  100. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
  101. package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
  102. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
  103. package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
  104. package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
  105. package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
  106. package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
  107. package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
  108. package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
  109. package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
  110. package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
  111. package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
  112. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
  113. package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
  114. package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
  115. package/src/install/atomic.ts +111 -0
  116. package/src/install/conflict.ts +226 -0
  117. package/src/install/detect.ts +280 -0
  118. package/src/install/paths.ts +90 -0
  119. package/src/install/plan.ts +205 -0
  120. package/src/install/preflight.ts +210 -0
  121. package/src/install/selectedTools.ts +52 -0
  122. package/src/install/toolDetection.ts +120 -0
  123. package/src/install/txlog.ts +161 -0
  124. package/src/install/types.ts +175 -0
  125. package/src/install/wizard-plan.ts +216 -0
  126. package/src/scripts/prepack-check.mjs +108 -5
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Plan builder — Phase A3 port of `scripts/install.py:_build_install_plan`.
3
+ *
4
+ * Pure: walks one or more source directories, computes the target path
5
+ * + SHA-256 of every file, and returns a declarative {@link InstallPlan}.
6
+ * The applier (Phase A4) consumes the plan verbatim — it never recomputes
7
+ * paths or re-reads the source tree.
8
+ *
9
+ * Mirrors:
10
+ * - `_sha256_of_file(path)` → {@link sha256File}
11
+ * - `_file_entry(path, kind, ...)` → {@link fileEntry}
12
+ * - `_files_by_tool_from_deploy(...)` → buildInstallPlan loop
13
+ * - `_copy_dir_dereferencing_symlinks` walk → {@link walkSourceTree}
14
+ *
15
+ * v4 simplifies the Python shape: instead of three helper tables
16
+ * (`GLOBAL_DEPLOY_SOURCES` × `USER_SCOPE_PATHS` × `_bridge_marker`) the
17
+ * planner receives a flat list of `(srcDir, destDir, toolId, kind)`
18
+ * tuples. The mapping table lives in a higher layer (B0 dispatcher) so
19
+ * the planner stays a pure file walker.
20
+ */
21
+
22
+ import { createHash } from 'node:crypto';
23
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
24
+ import { join, relative, resolve } from 'node:path';
25
+
26
+ import type {
27
+ ConflictPolicy,
28
+ FileEntry,
29
+ FileKind,
30
+ InstallPlan,
31
+ InstallTarget,
32
+ } from './types.js';
33
+
34
+ /**
35
+ * One (source-dir → dest-dir) mapping fed to {@link buildInstallPlan}.
36
+ *
37
+ * Mirrors one row of `GLOBAL_DEPLOY_SOURCES[tool_id]` plus its
38
+ * `USER_SCOPE_PATHS[tool_id]` anchor resolved to an absolute path.
39
+ */
40
+ export interface PlanSource {
41
+ /** Tool ID this source belongs to (e.g. `augment`, `cursor`). */
42
+ readonly toolId: string;
43
+ /** Absolute source directory to walk. */
44
+ readonly srcDir: string;
45
+ /** Absolute destination directory for the walked files. */
46
+ readonly destDir: string;
47
+ /** Kind recorded on every {@link FileEntry} produced from this source. */
48
+ readonly kind: FileKind;
49
+ }
50
+
51
+ /** Inputs the planner needs to walk source trees and produce an {@link InstallPlan}. */
52
+ export interface PlanInputs {
53
+ /** Where the install lands (global vs project). */
54
+ readonly target: InstallTarget;
55
+ /** Absolute install root anchor (passed through to {@link InstallPlan.root}). */
56
+ readonly root: string;
57
+ /** One or more `(srcDir, destDir, toolId, kind)` mappings. */
58
+ readonly sources: ReadonlyArray<PlanSource>;
59
+ /** Conflict policy active for this run. */
60
+ readonly policy: ConflictPolicy;
61
+ }
62
+
63
+ /**
64
+ * Hex SHA-256 of `path` content, or `null` if unreadable.
65
+ *
66
+ * 1:1 port of `_sha256_of_file`. Bridges intentionally skip this and
67
+ * pass `null` (their content is a pointer, not committed bytes).
68
+ */
69
+ export function sha256File(path: string): string | null {
70
+ try {
71
+ const data = readFileSync(path);
72
+ return createHash('sha256').update(data).digest('hex');
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Build one v2 `files[]` entry.
80
+ *
81
+ * 1:1 port of `_file_entry(path, kind, hash_content=...)`. `hashContent`
82
+ * toggles SHA-256 computation: deployed/marker pass `true`, bridges pass
83
+ * `false` (sha256 stays `null`).
84
+ */
85
+ export function fileEntry(
86
+ path: string,
87
+ kind: FileKind,
88
+ options: { hashContent: boolean },
89
+ ): FileEntry {
90
+ return {
91
+ path,
92
+ kind,
93
+ sha256: options.hashContent ? sha256File(path) : null,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Recursively list every file under `dir` (depth-first, sorted).
99
+ *
100
+ * Symlinks are followed via `statSync` (matches Python's
101
+ * `_copy_dir_dereferencing_symlinks` behaviour). Missing directories
102
+ * return `[]` rather than throwing — caller already validated `srcDir`.
103
+ */
104
+ export function walkSourceTree(dir: string): readonly string[] {
105
+ const out: string[] = [];
106
+ walkInto(dir, out);
107
+ return out;
108
+ }
109
+
110
+ function walkInto(dir: string, out: string[]): void {
111
+ let entries: string[];
112
+ try {
113
+ entries = readdirSync(dir).sort();
114
+ } catch {
115
+ return;
116
+ }
117
+ for (const name of entries) {
118
+ const full = join(dir, name);
119
+ let isDir: boolean;
120
+ try {
121
+ isDir = statSync(full).isDirectory();
122
+ } catch {
123
+ continue;
124
+ }
125
+ if (isDir) {
126
+ walkInto(full, out);
127
+ } else {
128
+ out.push(full);
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Build a declarative install plan.
135
+ *
136
+ * For each {@link PlanSource}, walks `srcDir` and emits one {@link FileEntry}
137
+ * per file with `path = destDir + (file path relative to srcDir)` and a
138
+ * fresh SHA-256 of the source file (computed once, never recomputed by
139
+ * the applier). Bridges (`kind=bridge`) keep `sha256=null` per the
140
+ * Python schema.
141
+ *
142
+ * The applier (Phase A4) is path-only — it does not re-walk `srcDir`
143
+ * and does not re-hash. The plan is the source of truth.
144
+ */
145
+ export function buildInstallPlan(inputs: PlanInputs): InstallPlan {
146
+ const filesByTool: Record<string, FileEntry[]> = {};
147
+
148
+ for (const source of inputs.sources) {
149
+ const bucket = filesByTool[source.toolId] ?? [];
150
+ const files = walkSourceTree(source.srcDir);
151
+ for (const srcFile of files) {
152
+ const rel = relative(source.srcDir, srcFile);
153
+ const destPath = resolve(source.destDir, rel);
154
+ bucket.push(
155
+ fileEntry(srcFile, source.kind, {
156
+ hashContent: source.kind !== 'bridge',
157
+ }),
158
+ );
159
+ // Override `path` so the entry reflects the *target* (the
160
+ // applier writes here), not the source we read from.
161
+ bucket[bucket.length - 1] = {
162
+ ...bucket[bucket.length - 1]!,
163
+ path: destPath,
164
+ };
165
+ }
166
+ filesByTool[source.toolId] = bucket;
167
+ }
168
+
169
+ // Ensure every requested toolId appears, even with zero files —
170
+ // mirrors `_files_by_tool_from_deploy` emitting `[]` so a shrinking
171
+ // install actually shrinks the recorded inventory.
172
+ for (const source of inputs.sources) {
173
+ if (!(source.toolId in filesByTool)) {
174
+ filesByTool[source.toolId] = [];
175
+ }
176
+ }
177
+
178
+ return {
179
+ version: 2,
180
+ target: inputs.target,
181
+ root: inputs.root,
182
+ filesByTool,
183
+ mergedKeysByTool: {},
184
+ policy: inputs.policy,
185
+ };
186
+ }
187
+
188
+ /**
189
+ * True when the plan would not modify any file on disk.
190
+ *
191
+ * Wizard renders the "nothing to do" screen on `true` (Phase B2).
192
+ */
193
+ export function isEmptyPlan(plan: InstallPlan): boolean {
194
+ for (const entries of Object.values(plan.filesByTool)) {
195
+ if (entries.length > 0) {
196
+ return false;
197
+ }
198
+ }
199
+ for (const entries of Object.values(plan.mergedKeysByTool)) {
200
+ if (entries.length > 0) {
201
+ return false;
202
+ }
203
+ }
204
+ return true;
205
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Pre-flight validation stage — road-to-flow-learnings Phase 0.
3
+ *
4
+ * Pure-ish (read-only filesystem probes, no writes) validation that runs
5
+ * BEFORE {@link buildInstallPlan} output is applied. Each probe produces a
6
+ * typed {@link PreflightFinding} instead of crashing, so callers (the init
7
+ * CLI's `--validate-only`, the fleet runner, the wizard) can render the
8
+ * full finding list and decide.
9
+ *
10
+ * Severity contract:
11
+ * - `blocking` — the install would fail or corrupt state; `--validate-only`
12
+ * exits non-zero on any blocking finding.
13
+ * - `warning` — the install can proceed but the operator should look.
14
+ * - `info` — context surfaced for the report, never gating.
15
+ */
16
+
17
+ import { accessSync, constants, existsSync, statSync } from 'node:fs';
18
+ import * as fs from 'node:fs';
19
+ import { dirname } from 'node:path';
20
+
21
+ import type { PlanInputs } from './plan.js';
22
+ import { buildInstallPlan } from './plan.js';
23
+ import { computeConflicts } from './conflict.js';
24
+ import { detectToolPresence } from './detect.js';
25
+
26
+ /** Stable identifiers for the four pre-flight probes. */
27
+ export type PreflightCheckId =
28
+ | 'permissions'
29
+ | 'disk-space'
30
+ | 'conflicts'
31
+ | 'host-detection';
32
+
33
+ /** One typed pre-flight finding — never a thrown error. */
34
+ export interface PreflightFinding {
35
+ readonly id: PreflightCheckId;
36
+ readonly severity: 'blocking' | 'warning' | 'info';
37
+ /** Absolute path the finding refers to (when path-scoped). */
38
+ readonly path?: string;
39
+ readonly message: string;
40
+ readonly remedy: string;
41
+ }
42
+
43
+ /** Tunables — defaults are deliberately conservative. */
44
+ export interface PreflightOptions {
45
+ /** Minimum free bytes required on the volume holding `root`. */
46
+ readonly diskFloorBytes?: number;
47
+ }
48
+
49
+ /** Default free-disk floor: 50 MiB — far above any real payload size. */
50
+ export const DEFAULT_DISK_FLOOR_BYTES = 50 * 1024 * 1024;
51
+
52
+ /** Walk up from `p` to the nearest existing ancestor (inclusive). */
53
+ function nearestExistingAncestor(p: string): string {
54
+ let cur = p;
55
+ while (!existsSync(cur)) {
56
+ const parent = dirname(cur);
57
+ if (parent === cur) return cur;
58
+ cur = parent;
59
+ }
60
+ return cur;
61
+ }
62
+
63
+ /** Probe 1 — every distinct destination root must be writable (or creatable). */
64
+ export function checkPermissions(inputs: PlanInputs): PreflightFinding[] {
65
+ const out: PreflightFinding[] = [];
66
+ const roots = new Set<string>([inputs.root, ...inputs.sources.map((s) => s.destDir)]);
67
+ for (const root of roots) {
68
+ const probe = nearestExistingAncestor(root);
69
+ try {
70
+ accessSync(probe, constants.W_OK);
71
+ } catch {
72
+ out.push({
73
+ id: 'permissions',
74
+ severity: 'blocking',
75
+ path: root,
76
+ message: `target root is not writable (nearest existing ancestor: ${probe})`,
77
+ remedy: `fix permissions on ${probe} or choose a different target`,
78
+ });
79
+ }
80
+ if (existsSync(root)) {
81
+ try {
82
+ if (!statSync(root).isDirectory()) {
83
+ out.push({
84
+ id: 'permissions',
85
+ severity: 'blocking',
86
+ path: root,
87
+ message: 'target root exists but is not a directory',
88
+ remedy: `move or remove the file at ${root}`,
89
+ });
90
+ }
91
+ } catch {
92
+ // statSync raced with a concurrent delete — treat as absent.
93
+ }
94
+ }
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /** Probe 2 — free-disk floor on the volume holding the install root. */
100
+ export function checkDiskSpace(
101
+ inputs: PlanInputs,
102
+ opts: PreflightOptions = {},
103
+ ): PreflightFinding[] {
104
+ const floor = opts.diskFloorBytes ?? DEFAULT_DISK_FLOOR_BYTES;
105
+ const probe = nearestExistingAncestor(inputs.root);
106
+ // statfsSync is Node >= 18.15; treat absence as "check unavailable",
107
+ // never as a pass or a fail.
108
+ const statfsSync = (fs as unknown as {
109
+ statfsSync?: (p: string) => { bavail: number; bsize: number };
110
+ }).statfsSync;
111
+ if (typeof statfsSync !== 'function') {
112
+ return [
113
+ {
114
+ id: 'disk-space',
115
+ severity: 'info',
116
+ path: probe,
117
+ message: 'free-disk check unavailable on this Node runtime',
118
+ remedy: '',
119
+ },
120
+ ];
121
+ }
122
+ try {
123
+ const st = statfsSync(probe);
124
+ const free = st.bavail * st.bsize;
125
+ if (free < floor) {
126
+ return [
127
+ {
128
+ id: 'disk-space',
129
+ severity: 'blocking',
130
+ path: probe,
131
+ message: `free disk ${free} B is below the ${floor} B floor`,
132
+ remedy: 'free disk space on the target volume, then re-run',
133
+ },
134
+ ];
135
+ }
136
+ } catch {
137
+ return [
138
+ {
139
+ id: 'disk-space',
140
+ severity: 'info',
141
+ path: probe,
142
+ message: 'free-disk probe failed (statfs error) — check skipped',
143
+ remedy: '',
144
+ },
145
+ ];
146
+ }
147
+ return [];
148
+ }
149
+
150
+ /** Probe 3 — surface plan conflicts as findings (reuses `computeConflicts`). */
151
+ export function checkConflicts(inputs: PlanInputs): PreflightFinding[] {
152
+ const plan = buildInstallPlan(inputs);
153
+ const conflicts = computeConflicts(plan);
154
+ return conflicts.map((c) => ({
155
+ id: 'conflicts' as const,
156
+ severity: 'warning' as const,
157
+ path: c.path,
158
+ message: `existing file conflicts with planned ${c.kind} content`,
159
+ remedy: 'resolve interactively, or pass --force to overwrite',
160
+ }));
161
+ }
162
+
163
+ /** Probe 4 — host-detection sanity: report what the target tree looks like. */
164
+ export function checkHostDetection(inputs: PlanInputs): PreflightFinding[] {
165
+ const presence = detectToolPresence(inputs.root);
166
+ const detected = Object.entries(presence)
167
+ .filter(([, v]) => v === true)
168
+ .map(([k]) => k);
169
+ if (detected.length === 0) {
170
+ return [
171
+ {
172
+ id: 'host-detection',
173
+ severity: 'info',
174
+ path: inputs.root,
175
+ message: 'no AI-tool directories detected at the target root',
176
+ remedy: 'expected for a first global install; verify the target path is right',
177
+ },
178
+ ];
179
+ }
180
+ return [
181
+ {
182
+ id: 'host-detection',
183
+ severity: 'info',
184
+ path: inputs.root,
185
+ message: `detected AI-tool surfaces: ${detected.join(', ')}`,
186
+ remedy: '',
187
+ },
188
+ ];
189
+ }
190
+
191
+ /**
192
+ * Run the full pre-flight suite. Never throws for probe-level problems —
193
+ * every issue is a typed finding. Ordering is stable (probe order above).
194
+ */
195
+ export function runPreflight(
196
+ inputs: PlanInputs,
197
+ opts: PreflightOptions = {},
198
+ ): PreflightFinding[] {
199
+ return [
200
+ ...checkPermissions(inputs),
201
+ ...checkDiskSpace(inputs, opts),
202
+ ...checkConflicts(inputs),
203
+ ...checkHostDetection(inputs),
204
+ ];
205
+ }
206
+
207
+ /** True when any finding blocks the install (`--validate-only` exit contract). */
208
+ export function hasBlockingFinding(findings: readonly PreflightFinding[]): boolean {
209
+ return findings.some((f) => f.severity === 'blocking');
210
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Persisted record of the AI tools the user actually selected in the wizard,
3
+ * at `~/.event4u/agent-config/wizard-tools.json`. This is the user's prior
4
+ * *selection* — distinct from `installed.lock` (every tool agent-config was
5
+ * deployed to, which for an `--tools=all` install is the full set) and from
6
+ * machine tool *detection*.
7
+ *
8
+ * Written by the apply route on a real (non-dry-run) wizard-v2 apply, and
9
+ * read by `detect-tools` so Step 1 pre-selects exactly the prior selection on
10
+ * a repeat run. Empty / missing → first run → Step 1 falls back to
11
+ * pre-selecting every installed tool.
12
+ *
13
+ * `AGENT_CONFIG_WIZARD_TOOLS` overrides the path (tests only).
14
+ */
15
+
16
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
17
+ import { homedir } from 'node:os';
18
+ import { resolve, dirname } from 'node:path';
19
+
20
+ function selectedToolsPath(): string {
21
+ const override = process.env['AGENT_CONFIG_WIZARD_TOOLS'];
22
+ if (override !== undefined && override.length > 0) return override;
23
+ return resolve(homedir(), '.event4u', 'agent-config', 'wizard-tools.json');
24
+ }
25
+
26
+ export function writeSelectedTools(tools: string[]): void {
27
+ try {
28
+ const path = selectedToolsPath();
29
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
30
+ writeFileSync(path, `${JSON.stringify({ tools }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
31
+ } catch {
32
+ // Best-effort: a failed write just means the next run falls back to
33
+ // pre-selecting installed tools.
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Return the recorded selection, or `[]` when absent / unreadable. Optionally
39
+ * intersect with the known-tool set so a stale id never surfaces.
40
+ */
41
+ export function readSelectedTools(knownIds?: ReadonlySet<string>): string[] {
42
+ try {
43
+ const raw = readFileSync(selectedToolsPath(), 'utf8');
44
+ const parsed = JSON.parse(raw) as { tools?: unknown };
45
+ const tools = Array.isArray(parsed.tools)
46
+ ? parsed.tools.filter((t): t is string => typeof t === 'string')
47
+ : [];
48
+ return knownIds === undefined ? tools : tools.filter((t) => knownIds.has(t));
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Native AI-tool presence detection (road-to-wizard-ux-improvements § Phase 2).
3
+ *
4
+ * Distinct from `detectToolPresence` (detect.ts), which checks the *project*
5
+ * for agent-config bridge dirs ("we set up a bridge here"). This module
6
+ * answers "is the AI tool itself installed on this machine?" by probing the
7
+ * user's home dir, well-known app bundles, and `$PATH` — so the wizard's
8
+ * Step-1 list can pre-select detected tools (first run) and show a per-tool
9
+ * installed/not-installed badge.
10
+ *
11
+ * The signal table is intentionally data-driven and best-effort: a tool with
12
+ * no reliable signal (e.g. a VS Code extension) simply reports `false`
13
+ * (badge = not installed, no pre-select) — false negatives are acceptable,
14
+ * false positives are not. Extend `TOOL_SIGNALS` as new tools/signals are
15
+ * learned; that is the single place to maintain (the accepted trade-off).
16
+ */
17
+
18
+ import { existsSync } from 'node:fs';
19
+ import { homedir } from 'node:os';
20
+ import { delimiter, join } from 'node:path';
21
+
22
+ /** Detection signals for one tool. A tool is "installed" if ANY signal hits. */
23
+ interface ToolSignal {
24
+ /** Paths relative to the user's home dir (dir or file). */
25
+ readonly homePaths?: readonly string[];
26
+ /** Absolute paths (e.g. macOS `/Applications/*.app` bundles). */
27
+ readonly absPaths?: readonly string[];
28
+ /** Executable names looked up on `$PATH`. */
29
+ readonly bins?: readonly string[];
30
+ }
31
+
32
+ /**
33
+ * Best-effort signal table keyed by the tool id used in the wizard's
34
+ * `VALID_TOOLS` (src/ui/wizard/state.ts). Keep ids in sync with that list.
35
+ */
36
+ const TOOL_SIGNALS: Readonly<Record<string, ToolSignal>> = {
37
+ 'claude-code': { bins: ['claude'], homePaths: ['.claude', '.claude.json', '.config/claude'] },
38
+ 'claude-desktop': { absPaths: ['/Applications/Claude.app'], homePaths: ['Library/Application Support/Claude', '.config/Claude'] },
39
+ 'cursor': { bins: ['cursor'], absPaths: ['/Applications/Cursor.app'], homePaths: ['.cursor'] },
40
+ 'windsurf': { bins: ['windsurf'], absPaths: ['/Applications/Windsurf.app'], homePaths: ['.codeium/windsurf'] },
41
+ 'cline': { homePaths: ['Documents/Cline'] },
42
+ 'gemini-cli': { bins: ['gemini'], homePaths: ['.gemini'] },
43
+ 'copilot': { bins: ['copilot'], homePaths: ['.config/github-copilot'] },
44
+ 'augment': { bins: ['auggie', 'augment'], homePaths: ['.augment'] },
45
+ 'aider': { bins: ['aider'], homePaths: ['.aider.conf.yml'] },
46
+ 'codex': { bins: ['codex'], homePaths: ['.codex'] },
47
+ 'roocode': {},
48
+ 'continue': { homePaths: ['.continue'] },
49
+ 'kilocode': {},
50
+ 'zed': { bins: ['zed'], absPaths: ['/Applications/Zed.app'], homePaths: ['.config/zed'] },
51
+ 'jetbrains': { homePaths: ['Library/Application Support/JetBrains', '.config/JetBrains'] },
52
+ 'kiro': { bins: ['kiro'], absPaths: ['/Applications/Kiro.app'], homePaths: ['.kiro'] },
53
+ 'qoder': { absPaths: ['/Applications/Qoder.app'], homePaths: ['.qoder'] },
54
+ 'opencode': { bins: ['opencode'], homePaths: ['.opencode', '.config/opencode'] },
55
+ 'trae': { absPaths: ['/Applications/Trae.app'], homePaths: ['.trae'] },
56
+ 'antigravity': { absPaths: ['/Applications/Antigravity.app'], homePaths: ['.antigravity'] },
57
+ 'codebuddy': { homePaths: ['.codebuddy'] },
58
+ 'droid': { bins: ['droid'], homePaths: ['.factory', '.droid'] },
59
+ 'warp': { absPaths: ['/Applications/Warp.app', '/Applications/Warp.app/Contents'], homePaths: ['.warp', '.local/share/warp-terminal'] },
60
+ };
61
+
62
+ export interface DetectToolsOptions {
63
+ /** User home dir (defaults to `os.homedir()`). Injected for tests. */
64
+ readonly home?: string;
65
+ /** `$PATH` value (defaults to `process.env.PATH`). Injected for tests. */
66
+ readonly pathEnv?: string | undefined;
67
+ }
68
+
69
+ /** Tool ids this module knows how to probe (the wizard's full tool set). */
70
+ export function knownToolIds(): readonly string[] {
71
+ return Object.keys(TOOL_SIGNALS);
72
+ }
73
+
74
+ /**
75
+ * Is an executable named `name` resolvable on `$PATH`? Exported for one-off
76
+ * presence checks (e.g. `rtk` on the Editor-and-tooling step). Defaults to
77
+ * `process.env.PATH`.
78
+ */
79
+ export function isBinaryOnPath(name: string, pathEnv?: string): boolean {
80
+ return binOnPath(name, pathEnv ?? process.env['PATH'] ?? '');
81
+ }
82
+
83
+ function binOnPath(name: string, pathEnv: string): boolean {
84
+ if (pathEnv.length === 0) return false;
85
+ for (const dir of pathEnv.split(delimiter)) {
86
+ if (dir.length === 0) continue;
87
+ if (existsSync(join(dir, name))) return true;
88
+ // Windows-style executables.
89
+ if (existsSync(join(dir, `${name}.exe`)) || existsSync(join(dir, `${name}.cmd`))) return true;
90
+ }
91
+ return false;
92
+ }
93
+
94
+ function isToolInstalled(signal: ToolSignal, home: string, pathEnv: string): boolean {
95
+ for (const rel of signal.homePaths ?? []) {
96
+ if (existsSync(join(home, rel))) return true;
97
+ }
98
+ for (const abs of signal.absPaths ?? []) {
99
+ if (existsSync(abs)) return true;
100
+ }
101
+ for (const bin of signal.bins ?? []) {
102
+ if (binOnPath(bin, pathEnv)) return true;
103
+ }
104
+ return false;
105
+ }
106
+
107
+ /**
108
+ * Probe every known tool and return a `{ <toolId>: installed }` map. Tools
109
+ * with no reliable signal report `false`. Pure w.r.t. injected `home`/`pathEnv`
110
+ * (only reads the filesystem), so it's safe to call per wizard boot.
111
+ */
112
+ export function detectInstalledTools(opts: DetectToolsOptions = {}): Record<string, boolean> {
113
+ const home = opts.home ?? homedir();
114
+ const pathEnv = opts.pathEnv ?? process.env['PATH'] ?? '';
115
+ const out: Record<string, boolean> = {};
116
+ for (const [id, signal] of Object.entries(TOOL_SIGNALS)) {
117
+ out[id] = isToolInstalled(signal, home, pathEnv);
118
+ }
119
+ return out;
120
+ }