@haystackeditor/cli 0.15.26 → 0.15.27

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 (49) hide show
  1. package/README.md +39 -3
  2. package/dist/assets/hooks/package-lock.json +598 -0
  3. package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +2051 -0
  4. package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +4 -0
  5. package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +3411 -0
  6. package/dist/assets/telemetry/browser-runtime.js +861 -0
  7. package/dist/assets/telemetry/runtime.cjs +1509 -0
  8. package/dist/commands/cloud-verifier-behaviors.d.ts +27 -0
  9. package/dist/commands/cloud-verifier-behaviors.js +218 -0
  10. package/dist/commands/cloud-verifier-data-store-census.d.ts +47 -0
  11. package/dist/commands/cloud-verifier-data-store-census.js +539 -0
  12. package/dist/commands/cloud-verifier-data-store-drift.d.ts +42 -0
  13. package/dist/commands/cloud-verifier-data-store-drift.js +158 -0
  14. package/dist/commands/cloud-verifier-identity-census.d.ts +88 -0
  15. package/dist/commands/cloud-verifier-identity-census.js +4057 -0
  16. package/dist/commands/cloud-verifier-materialization.d.ts +16 -0
  17. package/dist/commands/cloud-verifier-materialization.js +704 -0
  18. package/dist/commands/cloud-verifier-pascal-selector-census.d.ts +29 -0
  19. package/dist/commands/cloud-verifier-pascal-selector-census.js +1382 -0
  20. package/dist/commands/cloud-verifier-python-manifest-selector-census.d.ts +27 -0
  21. package/dist/commands/cloud-verifier-python-manifest-selector-census.js +2015 -0
  22. package/dist/commands/cloud-verifier-specialized-operational-census.d.ts +51 -0
  23. package/dist/commands/cloud-verifier-specialized-operational-census.js +11432 -0
  24. package/dist/commands/cloud-verifier-universe.d.ts +31 -0
  25. package/dist/commands/cloud-verifier-universe.js +10178 -0
  26. package/dist/commands/design-verify.d.ts +23 -0
  27. package/dist/commands/design-verify.js +142 -0
  28. package/dist/commands/hooks.js +2 -2
  29. package/dist/commands/prepare-universe-review.d.ts +115 -0
  30. package/dist/commands/prepare-universe-review.js +1092 -0
  31. package/dist/commands/production-source-deny-policy.d.ts +15 -0
  32. package/dist/commands/production-source-deny-policy.js +100 -0
  33. package/dist/commands/scaffold-provisional-universe.d.ts +468 -0
  34. package/dist/commands/scaffold-provisional-universe.js +808 -0
  35. package/dist/commands/schema-cmd.js +32 -0
  36. package/dist/commands/skills.d.ts +2 -2
  37. package/dist/commands/skills.js +415 -47
  38. package/dist/commands/telemetry.d.ts +53 -0
  39. package/dist/commands/telemetry.js +832 -0
  40. package/dist/index.js +134 -12
  41. package/dist/triage/runner.js +7 -6
  42. package/dist/utils/design-verifier-api.d.ts +129 -0
  43. package/dist/utils/design-verifier-api.js +175 -0
  44. package/dist/utils/design-verifier-result.d.ts +52 -0
  45. package/dist/utils/design-verifier-result.js +299 -0
  46. package/dist/utils/haystack-api.d.ts +2 -0
  47. package/dist/utils/haystack-api.js +10 -3
  48. package/dist/utils/hooks.js +2 -1
  49. package/package.json +7 -2
@@ -14,8 +14,40 @@ import { SCHEMA_VERSIONS } from '../schema.js';
14
14
  // Derive valid schema names from the registry so adding a new schema only
15
15
  // requires editing SCHEMA_VERSIONS (PR008 — single source of truth).
16
16
  const VALID = Object.keys(SCHEMA_VERSIONS);
17
+ /**
18
+ * Commands whose *input* payload callers reasonably expect to find here.
19
+ *
20
+ * `haystack schema` publishes `--json` output schemas only. Answering a request
21
+ * for one of these with a bare "Unknown schema" sends the caller hunting for a
22
+ * schema that was never on this surface, so name the real contract instead.
23
+ */
24
+ const INPUT_PAYLOAD_CONTRACTS = {
25
+ 'scaffold-provisional-universe': {
26
+ schemaName: 'cloud-verifier',
27
+ contract: 'the map-cloud-verifier-universe skill, references/output-contract.md '
28
+ + '("Scope and production authority")',
29
+ },
30
+ 'validate-universe': {
31
+ schemaName: 'cloud-verifier',
32
+ contract: 'the map-cloud-verifier-universe skill, references/output-contract.md',
33
+ },
34
+ };
17
35
  export function schemaCommand(name) {
18
36
  if (!VALID.includes(name)) {
37
+ const known = INPUT_PAYLOAD_CONTRACTS[name];
38
+ if (known) {
39
+ console.error(chalk.red(`No published schema for "${name}".`)
40
+ + `\n\n${chalk.bold('haystack schema')} publishes JSON Schemas for command `
41
+ + `${chalk.bold('--json output')} only; "${name}" takes a strict ${chalk.bold('--input')} `
42
+ + `payload, whose contract is not published here.`
43
+ + `\n\nInput contract: ${known.contract}`
44
+ + (known.schemaName
45
+ ? `\nOutput schema: ${chalk.dim(`haystack schema ${known.schemaName}`)}`
46
+ : '')
47
+ + `\n\nThe command validates its input strictly and reports every violation with a `
48
+ + `field path, so authoring against the contract and rerunning is the intended loop.`);
49
+ process.exit(2);
50
+ }
19
51
  console.error(chalk.red(`Unknown schema: "${name}".`) +
20
52
  `\n\nAvailable: ${VALID.join(', ')}`);
21
53
  process.exit(2);
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Skills command - Copies Haystack skill files into the project
3
- * for AI agent discovery (Claude Code, Codex CLI, Cursor)
2
+ * Skills command - installs Haystack-owned skill files while preserving any
3
+ * customer-owned skills that share the discovery directories.
4
4
  */
5
5
  export declare function installSkills(options: {
6
6
  cli?: string;
@@ -1,79 +1,447 @@
1
1
  /**
2
- * Skills command - Copies Haystack skill files into the project
3
- * for AI agent discovery (Claude Code, Codex CLI, Cursor)
2
+ * Skills command - installs Haystack-owned skill files while preserving any
3
+ * customer-owned skills that share the discovery directories.
4
4
  */
5
+ import { createHash, randomUUID } from 'node:crypto';
6
+ import { chmodSync, copyFileSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync, } from 'node:fs';
7
+ import { dirname, isAbsolute, join, posix, relative, sep } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
5
9
  import chalk from 'chalk';
6
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
7
- import { join, dirname } from 'path';
8
- import { fileURLToPath } from 'url';
10
+ import { findGitRoot } from '../utils/hooks.js';
9
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
- /** Directory containing bundled skill files */
12
+ const MANIFEST_NAME = '.haystack-managed.json';
13
+ const MANIFEST_SCHEMA_VERSION = 1;
14
+ const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/;
15
+ /** Directory containing bundled skill files. */
11
16
  function getSkillsSourceDir() {
12
- // In dist/commands/skills.js → assets are at dist/assets/skills/
17
+ // In dist/commands/skills.js, assets are at dist/assets/skills/.
13
18
  return join(__dirname, '..', 'assets', 'skills');
14
19
  }
15
20
  const SKILL_FILES = [
16
- 'setup-haystack.md',
17
21
  'submit.md',
18
22
  'map-your-system.md',
19
23
  ];
20
- function installSkillFiles(targetDir) {
21
- const sourceDir = getSkillsSourceDir();
22
- if (!existsSync(sourceDir)) {
23
- console.error(chalk.red(`Skills source directory not found: ${sourceDir}`));
24
+ const SKILL_DIRECTORIES = [
25
+ 'map-cloud-verifier-universe',
26
+ ];
27
+ // The manifest-free installer predates every managed directory payload. Only
28
+ // these fixed files can be classified as legacy Haystack content without an
29
+ // ownership receipt; all other existing payload paths are user-owned.
30
+ const LEGACY_MANAGED_FILE_PATHS = new Set([
31
+ 'setup-haystack.md',
32
+ 'submit.md',
33
+ 'map-your-system.md',
34
+ ]);
35
+ // Keep historical roots here after removing a bundled asset. They are the
36
+ // only namespaces an earlier valid manifest may authorize us to clean up.
37
+ const MANAGED_FILE_ROOTS = new Set(['setup-haystack.md', ...SKILL_FILES]);
38
+ const MANAGED_DIRECTORY_ROOTS = new Set(SKILL_DIRECTORIES);
39
+ const CLAUDE_MANAGED_FILE_ROOTS = new Set([
40
+ 'setup-haystack.md',
41
+ 'map-your-system.md',
42
+ 'map-cloud-verifier-universe.md',
43
+ ]);
44
+ const CLAUDE_COMMANDS = new Map([
45
+ [
46
+ 'map-your-system.md',
47
+ '# Map Your System for Haystack QA\n\nFollow .agents/skills/map-your-system.md to create .haystack/system.yml — verified facts about how this system builds, boots, seeds data, and logs in, so Haystack QA can run it.\n',
48
+ ],
49
+ [
50
+ 'map-cloud-verifier-universe.md',
51
+ '# Map a Cloud Verifier Universe\n\nFollow .agents/skills/map-cloud-verifier-universe/SKILL.md to map this application into a production-derived hermetic universe for on-demand Cloud Verifier.\n',
52
+ ],
53
+ ]);
54
+ function isRecord(value) {
55
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
56
+ }
57
+ function entryExists(path) {
58
+ try {
59
+ lstatSync(path);
60
+ return true;
61
+ }
62
+ catch (error) {
63
+ if (error.code === 'ENOENT')
64
+ return false;
65
+ throw error;
66
+ }
67
+ }
68
+ function hasExactKeys(record, expected) {
69
+ const actual = Object.keys(record).sort();
70
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
71
+ }
72
+ function sha256(contents) {
73
+ return `sha256:${createHash('sha256').update(contents).digest('hex')}`;
74
+ }
75
+ function manifestDigest(files) {
76
+ return sha256(JSON.stringify(files.map(({ path, sha256: digest }) => ({ path, sha256: digest }))));
77
+ }
78
+ function compareText(a, b) {
79
+ return a < b ? -1 : a > b ? 1 : 0;
80
+ }
81
+ function inside(root, candidate) {
82
+ const rel = relative(root, candidate);
83
+ return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`));
84
+ }
85
+ function validateManagedPath(path) {
86
+ if (typeof path !== 'string' || path.length === 0 || path.includes('\\') || path.includes('\0')) {
87
+ return false;
88
+ }
89
+ if (isAbsolute(path) || posix.normalize(path) !== path)
24
90
  return false;
91
+ const components = path.split('/');
92
+ if (components.some((component) => component === '' || component === '.' || component === '..')) {
93
+ return false;
94
+ }
95
+ const [root] = components;
96
+ return ((components.length === 1 && MANAGED_FILE_ROOTS.has(root)) ||
97
+ (components.length > 1 && MANAGED_DIRECTORY_ROOTS.has(root)));
98
+ }
99
+ function destinationPath(targetDir, managedPath) {
100
+ return join(targetDir, ...managedPath.split('/'));
101
+ }
102
+ function assertExistingDirectoryInside(root, path) {
103
+ const stat = lstatSync(path);
104
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
105
+ throw new Error(`Refusing skills install: destination ancestor is not a real directory: ${path}`);
106
+ }
107
+ const real = realpathSync(path);
108
+ if (!inside(root, real)) {
109
+ throw new Error(`Refusing skills install: destination escapes the repository: ${path}`);
25
110
  }
26
- // Ensure target directory exists
27
- if (!existsSync(targetDir)) {
28
- mkdirSync(targetDir, { recursive: true });
111
+ }
112
+ /** Check every existing destination component without changing the repository. */
113
+ function preflightDestination(root, relativeTarget) {
114
+ let current = root;
115
+ for (const component of relativeTarget) {
116
+ current = join(current, component);
117
+ if (!entryExists(current))
118
+ return;
119
+ assertExistingDirectoryInside(root, current);
120
+ }
121
+ }
122
+ /** Create one destination component at a time, rechecking each one for symlinks. */
123
+ function ensureSafeDirectory(root, relativeTarget) {
124
+ let current = root;
125
+ for (const component of relativeTarget) {
126
+ current = join(current, component);
127
+ if (!entryExists(current))
128
+ mkdirSync(current, { mode: 0o755 });
129
+ assertExistingDirectoryInside(root, current);
130
+ }
131
+ return current;
132
+ }
133
+ function assertManagedLeafSafe(root, targetDir, managedPath) {
134
+ let current = targetDir;
135
+ const components = managedPath.split('/');
136
+ components.forEach((component, index) => {
137
+ current = join(current, component);
138
+ if (!entryExists(current))
139
+ return;
140
+ const stat = lstatSync(current);
141
+ if (stat.isSymbolicLink()) {
142
+ throw new Error(`Refusing skills install: managed path contains a symlink: ${managedPath}`);
143
+ }
144
+ if (index < components.length - 1 && !stat.isDirectory()) {
145
+ throw new Error(`Refusing skills install: managed path ancestor is not a directory: ${managedPath}`);
146
+ }
147
+ if (index === components.length - 1 && !stat.isFile()) {
148
+ throw new Error(`Refusing skills install: managed file path is not a regular file: ${managedPath}`);
149
+ }
150
+ const real = realpathSync(current);
151
+ if (!inside(root, real)) {
152
+ throw new Error(`Refusing skills install: managed path escapes the repository: ${managedPath}`);
153
+ }
154
+ });
155
+ }
156
+ function collectSourceDirectory(sourceDir, relativePrefix, payload) {
157
+ const sourceStat = lstatSync(sourceDir);
158
+ if (sourceStat.isSymbolicLink() || !sourceStat.isDirectory()) {
159
+ throw new Error(`Bundled skill directory is not a real directory: ${sourceDir}`);
160
+ }
161
+ for (const entry of readdirSync(sourceDir, { withFileTypes: true }).sort((a, b) => compareText(a.name, b.name))) {
162
+ const source = join(sourceDir, entry.name);
163
+ const managedPath = posix.join(relativePrefix, entry.name);
164
+ const stat = lstatSync(source);
165
+ if (stat.isSymbolicLink()) {
166
+ throw new Error(`Bundled skills may not contain symlinks: ${source}`);
167
+ }
168
+ if (stat.isDirectory()) {
169
+ collectSourceDirectory(source, managedPath, payload);
170
+ }
171
+ else if (stat.isFile()) {
172
+ const contents = readFileSync(source);
173
+ payload.set(managedPath, {
174
+ path: managedPath,
175
+ sha256: sha256(contents),
176
+ contents,
177
+ mode: stat.mode & 0o777,
178
+ });
179
+ }
180
+ else {
181
+ throw new Error(`Bundled skills may contain only regular files and directories: ${source}`);
182
+ }
183
+ }
184
+ }
185
+ function bundledSkillsPayload() {
186
+ const sourceDir = getSkillsSourceDir();
187
+ if (!entryExists(sourceDir)) {
188
+ throw new Error(`Skills source directory not found: ${sourceDir}`);
29
189
  }
30
- let copied = 0;
190
+ const payload = new Map();
31
191
  for (const file of SKILL_FILES) {
32
- const src = join(sourceDir, file);
33
- if (existsSync(src)) {
34
- const dest = join(targetDir, file);
35
- writeFileSync(dest, readFileSync(src, 'utf-8'));
36
- copied++;
192
+ const source = join(sourceDir, file);
193
+ if (!entryExists(source))
194
+ continue;
195
+ const stat = lstatSync(source);
196
+ if (stat.isSymbolicLink() || !stat.isFile()) {
197
+ throw new Error(`Bundled skill file is not a regular file: ${source}`);
37
198
  }
199
+ const contents = readFileSync(source);
200
+ payload.set(file, {
201
+ path: file,
202
+ sha256: sha256(contents),
203
+ contents,
204
+ mode: stat.mode & 0o777,
205
+ });
206
+ }
207
+ for (const directory of SKILL_DIRECTORIES) {
208
+ const source = join(sourceDir, directory);
209
+ if (!entryExists(source))
210
+ continue;
211
+ collectSourceDirectory(source, directory, payload);
212
+ }
213
+ if (payload.size === 0)
214
+ throw new Error(`No bundled skills found in ${sourceDir}`);
215
+ return payload;
216
+ }
217
+ function textPayload(files) {
218
+ return new Map([...files.entries()].map(([path, text]) => {
219
+ const contents = Buffer.from(text, 'utf8');
220
+ return [path, { path, sha256: sha256(contents), contents, mode: 0o644 }];
221
+ }));
222
+ }
223
+ function parseManifest(targetDir, allowedPath) {
224
+ const path = join(targetDir, MANIFEST_NAME);
225
+ if (!entryExists(path))
226
+ return null;
227
+ const stat = lstatSync(path);
228
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.size > 1024 * 1024) {
229
+ throw new Error(`Refusing skills install: managed manifest is not a trusted regular file: ${path}`);
230
+ }
231
+ let parsed;
232
+ try {
233
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
234
+ }
235
+ catch {
236
+ throw new Error(`Refusing skills install: managed manifest is malformed: ${path}`);
237
+ }
238
+ if (!isRecord(parsed) ||
239
+ !hasExactKeys(parsed, ['bundle_digest', 'files', 'schema_version']) ||
240
+ parsed.schema_version !== MANIFEST_SCHEMA_VERSION ||
241
+ typeof parsed.bundle_digest !== 'string' ||
242
+ !SHA256_PATTERN.test(parsed.bundle_digest) ||
243
+ !Array.isArray(parsed.files)) {
244
+ throw new Error(`Refusing skills install: managed manifest has an unsupported schema: ${path}`);
245
+ }
246
+ const files = [];
247
+ for (const value of parsed.files) {
248
+ if (!isRecord(value) ||
249
+ !hasExactKeys(value, ['path', 'sha256']) ||
250
+ !allowedPath(value.path) ||
251
+ typeof value.sha256 !== 'string' ||
252
+ !SHA256_PATTERN.test(value.sha256)) {
253
+ throw new Error(`Refusing skills install: managed manifest contains an unsafe entry: ${path}`);
254
+ }
255
+ files.push({ path: value.path, sha256: value.sha256 });
256
+ }
257
+ const sorted = [...files].sort((a, b) => compareText(a.path, b.path));
258
+ if (files.some((file, index) => file.path !== sorted[index]?.path) ||
259
+ files.some((file, index) => index > 0 && file.path === files[index - 1]?.path) ||
260
+ manifestDigest(files) !== parsed.bundle_digest) {
261
+ throw new Error(`Refusing skills install: managed manifest is inconsistent: ${path}`);
262
+ }
263
+ return {
264
+ schema_version: MANIFEST_SCHEMA_VERSION,
265
+ bundle_digest: parsed.bundle_digest,
266
+ files,
267
+ };
268
+ }
269
+ function copyTreeWithoutFollowingSymlinks(sourceDir, destinationDir) {
270
+ for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
271
+ const source = join(sourceDir, entry.name);
272
+ const destination = join(destinationDir, entry.name);
273
+ const stat = lstatSync(source);
274
+ if (stat.isSymbolicLink()) {
275
+ symlinkSync(readlinkSync(source), destination);
276
+ }
277
+ else if (stat.isDirectory()) {
278
+ mkdirSync(destination, { mode: stat.mode & 0o777 });
279
+ copyTreeWithoutFollowingSymlinks(source, destination);
280
+ }
281
+ else if (stat.isFile()) {
282
+ copyFileSync(source, destination);
283
+ chmodSync(destination, stat.mode & 0o777);
284
+ }
285
+ else {
286
+ throw new Error(`Refusing skills install: destination contains an unsupported file type: ${source}`);
287
+ }
288
+ }
289
+ }
290
+ function writePayloadToStage(stageDir, payload, previouslyManaged, manifest) {
291
+ for (const managedPath of previouslyManaged) {
292
+ const stagedPath = destinationPath(stageDir, managedPath);
293
+ if (entryExists(stagedPath))
294
+ rmSync(stagedPath, { force: true });
295
+ }
296
+ for (const file of payload.values()) {
297
+ const destination = destinationPath(stageDir, file.path);
298
+ const parent = dirname(destination);
299
+ if (!entryExists(parent))
300
+ mkdirSync(parent, { recursive: true, mode: 0o755 });
301
+ if (entryExists(destination)) {
302
+ throw new Error(`Refusing skills install: new managed file collides with a user file: ${file.path}`);
303
+ }
304
+ writeFileSync(destination, file.contents, { mode: file.mode });
305
+ }
306
+ writeFileSync(join(stageDir, MANIFEST_NAME), `${JSON.stringify(manifest, null, 2)}\n`, {
307
+ mode: 0o644,
308
+ });
309
+ }
310
+ function replaceDirectoryFromStage(targetDir, stageDir) {
311
+ const parent = dirname(targetDir);
312
+ const backupDir = join(parent, `.haystack-${posix.basename(targetDir)}-backup-${randomUUID()}`);
313
+ const targetExisted = entryExists(targetDir);
314
+ let oldMoved = false;
315
+ let newMoved = false;
316
+ try {
317
+ if (targetExisted) {
318
+ renameSync(targetDir, backupDir);
319
+ oldMoved = true;
320
+ }
321
+ renameSync(stageDir, targetDir);
322
+ newMoved = true;
323
+ if (oldMoved)
324
+ rmSync(backupDir, { recursive: true });
325
+ }
326
+ catch (error) {
327
+ try {
328
+ if (newMoved && entryExists(targetDir))
329
+ renameSync(targetDir, stageDir);
330
+ if (oldMoved && entryExists(backupDir) && !entryExists(targetDir)) {
331
+ renameSync(backupDir, targetDir);
332
+ }
333
+ }
334
+ finally {
335
+ if (entryExists(stageDir))
336
+ rmSync(stageDir, { recursive: true, force: true });
337
+ }
338
+ throw error;
38
339
  }
39
- return copied > 0;
340
+ }
341
+ function installManagedPayload(root, relativeTarget, payload, allowedPath) {
342
+ const parent = ensureSafeDirectory(root, relativeTarget.slice(0, -1));
343
+ const targetDir = join(parent, relativeTarget[relativeTarget.length - 1]);
344
+ if (entryExists(targetDir))
345
+ assertExistingDirectoryInside(root, targetDir);
346
+ const previous = entryExists(targetDir) ? parseManifest(targetDir, allowedPath) : null;
347
+ const previouslyManaged = new Set(previous?.files.map((file) => file.path) ?? []);
348
+ if (previous) {
349
+ for (const file of previous.files) {
350
+ assertManagedLeafSafe(root, targetDir, file.path);
351
+ const existing = destinationPath(targetDir, file.path);
352
+ if (entryExists(existing) && sha256(readFileSync(existing)) !== file.sha256) {
353
+ throw new Error(`Refusing skills install: managed file was modified after installation: ${file.path}`);
354
+ }
355
+ }
356
+ }
357
+ for (const file of payload.values()) {
358
+ assertManagedLeafSafe(root, targetDir, file.path);
359
+ const existing = destinationPath(targetDir, file.path);
360
+ if (!entryExists(existing))
361
+ continue;
362
+ if ((previous && !previouslyManaged.has(file.path)) ||
363
+ (!previous && !LEGACY_MANAGED_FILE_PATHS.has(file.path))) {
364
+ throw new Error(`Refusing skills install: new managed file collides with a user file: ${file.path}`);
365
+ }
366
+ // One-time migration from the legacy installer: only its three historical
367
+ // fixed files may be adopted when no ownership manifest was written.
368
+ previouslyManaged.add(file.path);
369
+ }
370
+ const files = [...payload.values()]
371
+ .map(({ path, sha256: digest }) => ({ path, sha256: digest }))
372
+ .sort((a, b) => compareText(a.path, b.path));
373
+ const nextManifest = {
374
+ schema_version: MANIFEST_SCHEMA_VERSION,
375
+ bundle_digest: manifestDigest(files),
376
+ files,
377
+ };
378
+ const targetMode = entryExists(targetDir) ? lstatSync(targetDir).mode & 0o777 : 0o755;
379
+ const stageDir = mkdtempSync(join(parent, `.haystack-${relativeTarget.at(-1)}-stage-`));
380
+ try {
381
+ if (entryExists(targetDir))
382
+ copyTreeWithoutFollowingSymlinks(targetDir, stageDir);
383
+ writePayloadToStage(stageDir, payload, previouslyManaged, nextManifest);
384
+ chmodSync(stageDir, targetMode);
385
+ replaceDirectoryFromStage(targetDir, stageDir);
386
+ }
387
+ catch (error) {
388
+ if (entryExists(stageDir))
389
+ rmSync(stageDir, { recursive: true, force: true });
390
+ throw error;
391
+ }
392
+ return nextManifest;
393
+ }
394
+ function parseCli(value) {
395
+ if (value === undefined)
396
+ return undefined;
397
+ if (value === 'claude' || value === 'codex' || value === 'cursor' || value === 'manual') {
398
+ return value;
399
+ }
400
+ throw new Error(`Unsupported coding CLI "${value}". Use claude, codex, cursor, or manual.`);
401
+ }
402
+ function claudeManagedPath(path) {
403
+ return typeof path === 'string' && CLAUDE_MANAGED_FILE_ROOTS.has(path);
40
404
  }
41
405
  export async function installSkills(options) {
42
406
  console.log(chalk.cyan('\nšŸ“¦ Installing Haystack skills...\n'));
43
- const projectRoot = process.cwd();
44
- // Install to .agents/skills/ for generic agent discovery
407
+ const cli = parseCli(options.cli);
408
+ const gitRoot = findGitRoot();
409
+ if (!gitRoot) {
410
+ throw new Error('Skills must be installed from inside a Git repository.');
411
+ }
412
+ const projectRoot = realpathSync(gitRoot);
413
+ // Preflight all selected destinations before writing either one.
414
+ preflightDestination(projectRoot, ['.agents', 'skills']);
415
+ if (cli === 'claude')
416
+ preflightDestination(projectRoot, ['.claude', 'commands']);
45
417
  const agentsSkillsDir = join(projectRoot, '.agents', 'skills');
46
- console.log(chalk.gray(`Copying skills to ${agentsSkillsDir}\n`));
47
- const success = installSkillFiles(agentsSkillsDir);
48
- if (!success) {
49
- console.error(chalk.red('Failed to install skill files.'));
50
- process.exit(1);
51
- }
52
- // Also create .claude/commands/ entry for Claude Code slash commands
53
- const claudeCommandsDir = join(projectRoot, '.claude', 'commands');
54
- if (!existsSync(claudeCommandsDir)) {
55
- mkdirSync(claudeCommandsDir, { recursive: true });
56
- }
57
- const claudeCommandPath = join(claudeCommandsDir, 'setup-haystack.md');
58
- writeFileSync(claudeCommandPath, '# Set Up Haystack\n\nFollow .agents/skills/setup-haystack.md to set up Haystack for this repo.\n');
59
- writeFileSync(join(claudeCommandsDir, 'map-your-system.md'), '# Map Your System for Haystack QA\n\nFollow .agents/skills/map-your-system.md to create .haystack/system.yml — verified facts about how this system builds, boots, seeds data, and logs in, so Haystack QA can run it.\n');
60
- showSuccessMessage();
61
- }
62
- function showSuccessMessage() {
418
+ console.log(chalk.gray(`Copying portable skills to ${agentsSkillsDir}\n`));
419
+ const skillsManifest = installManagedPayload(projectRoot, ['.agents', 'skills'], bundledSkillsPayload(), validateManagedPath);
420
+ if (cli === 'claude') {
421
+ installManagedPayload(projectRoot, ['.claude', 'commands'], textPayload(CLAUDE_COMMANDS), claudeManagedPath);
422
+ }
423
+ showSuccessMessage(cli, projectRoot, skillsManifest.bundle_digest);
424
+ }
425
+ function showSuccessMessage(cli, projectRoot, digest) {
63
426
  console.log(chalk.green('\nāœ… Haystack skills installed!\n'));
64
- console.log(chalk.white('Available skills:'));
65
- console.log(chalk.cyan(' /setup-haystack ') + chalk.gray('- Create .haystack.json with AI assistance'));
427
+ console.log(chalk.gray(`Repository: ${projectRoot}`));
428
+ console.log(chalk.gray(`Bundle: ${digest}`));
429
+ console.log(chalk.white('\nAvailable skills:'));
66
430
  console.log(chalk.cyan(' /submit ') + chalk.gray('- Submit a PR via Haystack'));
67
431
  console.log(chalk.cyan(' /map-your-system ') + chalk.gray('- Write .haystack/system.yml so Haystack QA can run this system'));
68
- console.log();
69
- console.log(chalk.white('Run in your coding CLI:'));
70
- console.log(chalk.cyan(' /setup-haystack'));
432
+ console.log(chalk.cyan(' /map-cloud-verifier-universe') + chalk.gray(' - Map a production-derived hermetic Cloud Verifier universe'));
433
+ if (cli === 'claude') {
434
+ console.log(chalk.gray('\nClaude Code command shims were installed in .claude/commands.'));
435
+ }
436
+ else if (cli === 'manual') {
437
+ console.log(chalk.gray('\nPoint your coding agent at .agents/skills for portable skill discovery.'));
438
+ }
71
439
  console.log();
72
440
  }
73
441
  export async function listSkills() {
74
442
  console.log(chalk.white('\nAvailable Haystack skills:\n'));
75
- console.log(chalk.cyan(' /setup-haystack ') + chalk.gray('- Master setup - creates .haystack.json'));
76
443
  console.log(chalk.cyan(' /submit ') + chalk.gray('- Submit a PR via Haystack'));
77
444
  console.log(chalk.cyan(' /map-your-system ') + chalk.gray('- Write .haystack/system.yml so Haystack QA can run this system'));
445
+ console.log(chalk.cyan(' /map-cloud-verifier-universe') + chalk.gray(' - Map a production-derived hermetic Cloud Verifier universe'));
78
446
  console.log();
79
447
  }
@@ -0,0 +1,53 @@
1
+ declare const MANIFEST_SCHEMA_VERSION = "haystack-node-telemetry-instrumentation-v3";
2
+ export interface TelemetryInstrumentOptions {
3
+ entry: string;
4
+ sourceRoot?: string;
5
+ json?: boolean;
6
+ /**
7
+ * Experimental exact symbol allow-list. Entries use the same structured
8
+ * sourcePath#qualifiedName coordinates emitted in the instrumentation
9
+ * manifest. No fuzzy name matching is performed.
10
+ */
11
+ includeSymbols?: Array<{
12
+ sourcePath: string;
13
+ qualifiedName: string;
14
+ }>;
15
+ }
16
+ export interface InstrumentedFileSummary {
17
+ output_path: string;
18
+ source_path: string;
19
+ probes: {
20
+ branches: number;
21
+ parameters: number;
22
+ bindings: number;
23
+ returns: number;
24
+ throws: number;
25
+ };
26
+ }
27
+ export interface TelemetryInstrumentationResult {
28
+ schema_version: typeof MANIFEST_SCHEMA_VERSION;
29
+ build_id: string;
30
+ dist_directory: string;
31
+ entry: string;
32
+ runtime: string;
33
+ instrumented_files: InstrumentedFileSummary[];
34
+ sites: InstrumentedSite[];
35
+ skipped_unmapped_files: string[];
36
+ already_instrumented: boolean;
37
+ }
38
+ export interface InstrumentedSite {
39
+ probe_key: string;
40
+ site_id: string;
41
+ source_path: string;
42
+ qualified_name: string;
43
+ probe_kind: 'condition' | 'parameter' | 'binding' | 'return' | 'throw';
44
+ owner_key: string;
45
+ structural_key: string;
46
+ label?: string;
47
+ allowed_fields: string[];
48
+ }
49
+ export declare function instrumentNodeTelemetryBuild(distDirectory: string, options: TelemetryInstrumentOptions): TelemetryInstrumentationResult;
50
+ export declare function telemetryInstrumentCommand(distDirectory: string, options: TelemetryInstrumentOptions & {
51
+ includeSymbolsFile?: string;
52
+ }): Promise<void>;
53
+ export {};