@haystackeditor/cli 0.15.27 → 0.15.28

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 (52) hide show
  1. package/README.md +13 -20
  2. package/dist/assets/hooks/agent-context/detect.ts +7 -7
  3. package/dist/assets/hooks/agent-context/parsers/claude.ts +1 -1
  4. package/dist/assets/hooks/scripts/commit-msg.sh +1 -3
  5. package/dist/assets/hooks/scripts/post-commit.sh +1 -3
  6. package/dist/assets/hooks/scripts/pre-push.sh +1 -4
  7. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +1 -2
  8. package/dist/assets/telemetry/browser-runtime.js +651 -43
  9. package/dist/assets/telemetry/runtime.cjs +62 -68
  10. package/dist/commands/design-verify.d.ts +11 -3
  11. package/dist/commands/design-verify.js +147 -4
  12. package/dist/commands/hooks.d.ts +1 -5
  13. package/dist/commands/hooks.js +10 -112
  14. package/dist/commands/install-session-hooks.d.ts +2 -2
  15. package/dist/commands/install-session-hooks.js +71 -21
  16. package/dist/commands/mcp.js +1 -34
  17. package/dist/commands/pr.js +2 -7
  18. package/dist/commands/setup.d.ts +1 -2
  19. package/dist/commands/setup.js +18 -272
  20. package/dist/commands/submit.js +1 -2
  21. package/dist/commands/verify-hosted.d.ts +13 -0
  22. package/dist/commands/verify-hosted.js +49 -1
  23. package/dist/commands/verify-precompute.d.ts +19 -0
  24. package/dist/commands/verify-precompute.js +328 -0
  25. package/dist/index.js +37 -38
  26. package/dist/schema.d.ts +1 -2
  27. package/dist/schema.js +1 -2
  28. package/dist/triage/prompts.d.ts +0 -7
  29. package/dist/triage/prompts.js +0 -145
  30. package/dist/triage/runner.js +3 -22
  31. package/dist/triage/types.d.ts +1 -8
  32. package/dist/types.d.ts +8 -8
  33. package/dist/types.js +1 -1
  34. package/dist/utils/design-verifier-api.d.ts +71 -0
  35. package/dist/utils/design-verifier-api.js +96 -0
  36. package/dist/utils/design-verifier-result.d.ts +7 -2
  37. package/dist/utils/design-verifier-result.js +67 -21
  38. package/dist/utils/git.d.ts +5 -1
  39. package/dist/utils/git.js +19 -9
  40. package/dist/utils/github-api.js +11 -14
  41. package/dist/utils/hooks.d.ts +1 -15
  42. package/dist/utils/hooks.js +1 -127
  43. package/dist/utils/telemetry.d.ts +2 -0
  44. package/dist/utils/telemetry.js +81 -8
  45. package/package.json +2 -1
  46. package/schemas/cloud-verifier.v1.json +69 -3
  47. package/schemas/{pr.v2.json → pr.v3.json} +3 -12
  48. package/dist/commands/traces.d.ts +0 -48
  49. package/dist/commands/traces.js +0 -92
  50. package/dist/triage/traces.d.ts +0 -20
  51. package/dist/triage/traces.js +0 -311
  52. package/schemas/traces.v1.json +0 -53
@@ -0,0 +1,328 @@
1
+ import { spawn, execFileSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { cpSync, existsSync, mkdtempSync, rmSync } from 'node:fs';
4
+ import { createGzip } from 'node:zlib';
5
+ import { isAbsolute, join, resolve } from 'node:path';
6
+ import { tmpdir } from 'node:os';
7
+ import chalk from 'chalk';
8
+ import { NotLoggedInError, resolveAuthContext } from '../utils/auth.js';
9
+ import { findGitRoot, parseRemoteUrl, resolveDiffBaseRef, } from '../utils/git.js';
10
+ import { HaystackApiError, haystackJson } from '../utils/haystack-api.js';
11
+ import { trackPrecomputeEvent } from '../utils/telemetry.js';
12
+ const TOTAL_WALL_MS = 5_000;
13
+ const REMOTE_COMMAND_MS = 750;
14
+ const COMMIT_SHA = /^[0-9a-f]{40}$/;
15
+ const REPOSITORY_COMPONENT = /^[A-Za-z0-9_.-]{1,100}$/;
16
+ const RUN_ID = /^cv_[0-9a-f]{48}$/;
17
+ const FIXED_COMMIT_ENV = {
18
+ GIT_AUTHOR_NAME: 'haystack-stop-hook',
19
+ GIT_AUTHOR_EMAIL: 'stop-hook@haystack.invalid',
20
+ GIT_AUTHOR_DATE: '2000-01-01T00:00:00Z',
21
+ GIT_COMMITTER_NAME: 'haystack-stop-hook',
22
+ GIT_COMMITTER_EMAIL: 'stop-hook@haystack.invalid',
23
+ GIT_COMMITTER_DATE: '2000-01-01T00:00:00Z',
24
+ };
25
+ class PrecomputeFailure extends Error {
26
+ constructor(message) {
27
+ super(message);
28
+ this.name = 'PrecomputeFailure';
29
+ }
30
+ }
31
+ function remainingMs(deadline) {
32
+ const remaining = deadline - Date.now();
33
+ if (remaining <= 0) {
34
+ throw new PrecomputeFailure('Precompute timed out.');
35
+ }
36
+ return remaining;
37
+ }
38
+ function gitOutput(args, gitRoot, deadline, options = {}) {
39
+ const timeout = Math.max(1, Math.min(remainingMs(deadline), options.timeoutCapMs ?? TOTAL_WALL_MS));
40
+ try {
41
+ return execFileSync('git', args, {
42
+ cwd: gitRoot,
43
+ encoding: 'utf8',
44
+ env: options.env ?? process.env,
45
+ stdio: ['pipe', 'pipe', 'pipe'],
46
+ timeout,
47
+ }).trim();
48
+ }
49
+ catch {
50
+ throw new PrecomputeFailure('Could not derive the repository snapshot.');
51
+ }
52
+ }
53
+ function resolveOriginRepository(gitRoot) {
54
+ try {
55
+ const remote = parseRemoteUrl('origin');
56
+ if (!REPOSITORY_COMPONENT.test(remote.owner)
57
+ || !REPOSITORY_COMPONENT.test(remote.repo)) {
58
+ throw new Error('invalid repository identity');
59
+ }
60
+ return { owner: remote.owner, repository: remote.repo };
61
+ }
62
+ catch {
63
+ // Keep remote URLs and local paths out of errors.
64
+ throw new PrecomputeFailure('No supported origin repository was found.');
65
+ }
66
+ }
67
+ function resolveOriginDefaultBranch(gitRoot, deadline) {
68
+ try {
69
+ const remoteHead = gitOutput(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], gitRoot, deadline);
70
+ if (remoteHead.startsWith('origin/') && remoteHead.length > 'origin/'.length) {
71
+ return remoteHead.slice('origin/'.length);
72
+ }
73
+ }
74
+ catch {
75
+ // A clone does not have to carry origin/HEAD; ask the remote below.
76
+ }
77
+ const remoteSummary = gitOutput(['remote', 'show', 'origin'], gitRoot, deadline, {
78
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
79
+ timeoutCapMs: REMOTE_COMMAND_MS,
80
+ });
81
+ const match = remoteSummary.match(/^\s*HEAD branch:\s*(\S+)\s*$/m);
82
+ if (!match) {
83
+ throw new PrecomputeFailure('Could not resolve the origin default branch.');
84
+ }
85
+ return match[1];
86
+ }
87
+ function deriveBaseSha(gitRoot, deadline) {
88
+ const defaultBranch = resolveOriginDefaultBranch(gitRoot, deadline);
89
+ const baseRef = resolveDiffBaseRef(defaultBranch, {
90
+ cwd: gitRoot,
91
+ timeoutMs: Math.max(1, Math.min(remainingMs(deadline), REMOTE_COMMAND_MS)),
92
+ });
93
+ const baseSha = gitOutput(['merge-base', 'HEAD', baseRef], gitRoot, deadline);
94
+ if (!COMMIT_SHA.test(baseSha)) {
95
+ throw new PrecomputeFailure('Could not derive the base commit.');
96
+ }
97
+ return baseSha;
98
+ }
99
+ function deriveWorktreeTreeSha(gitRoot, deadline) {
100
+ const scratchDirectory = mkdtempSync(join(tmpdir(), 'haystack-precompute-index-'));
101
+ const temporaryIndex = join(scratchDirectory, 'index');
102
+ try {
103
+ const gitIndexPath = gitOutput(['rev-parse', '--git-path', 'index'], gitRoot, deadline);
104
+ const sourceIndex = isAbsolute(gitIndexPath)
105
+ ? gitIndexPath
106
+ : resolve(gitRoot, gitIndexPath);
107
+ const indexEnv = { ...process.env, GIT_INDEX_FILE: temporaryIndex };
108
+ if (existsSync(sourceIndex)) {
109
+ // blast_cache_worktree_tree_sha uses `cp -p`: preserving timestamps keeps
110
+ // git's racy-clean handling byte-identical to the bash implementation.
111
+ cpSync(sourceIndex, temporaryIndex, { preserveTimestamps: true });
112
+ }
113
+ else {
114
+ gitOutput(['read-tree', 'HEAD'], gitRoot, deadline, { env: indexEnv });
115
+ }
116
+ gitOutput(['add', '-A'], gitRoot, deadline, { env: indexEnv });
117
+ const treeSha = gitOutput(['write-tree'], gitRoot, deadline, { env: indexEnv });
118
+ if (!COMMIT_SHA.test(treeSha)) {
119
+ throw new PrecomputeFailure('Could not derive the working-tree snapshot.');
120
+ }
121
+ return treeSha;
122
+ }
123
+ finally {
124
+ rmSync(scratchDirectory, { recursive: true, force: true });
125
+ }
126
+ }
127
+ function deriveWorkCommit(gitRoot, baseSha, treeSha, deadline) {
128
+ const headSha = gitOutput(['rev-parse', 'HEAD'], gitRoot, deadline);
129
+ const headTreeSha = gitOutput(['rev-parse', 'HEAD^{tree}'], gitRoot, deadline);
130
+ if (!COMMIT_SHA.test(headSha) || !COMMIT_SHA.test(headTreeSha)) {
131
+ throw new PrecomputeFailure('Could not resolve the current commit.');
132
+ }
133
+ // A clean checkout of the base already has the exact required identity.
134
+ if (baseSha === headSha && treeSha === headTreeSha)
135
+ return headSha;
136
+ const workCommit = gitOutput(['commit-tree', treeSha, '-p', baseSha, '-m', 'front-half-snapshot'], gitRoot, deadline, { env: { ...process.env, ...FIXED_COMMIT_ENV } });
137
+ if (!COMMIT_SHA.test(workCommit)) {
138
+ throw new PrecomputeFailure('Could not derive the working commit.');
139
+ }
140
+ return workCommit;
141
+ }
142
+ function slimInputHash() {
143
+ // Mirrors printf '%s\0%s\0%s\0%s\0' slim codex-harness none none.
144
+ return createHash('sha256')
145
+ .update(Buffer.from('slim\0codex-harness\0none\0none\0'))
146
+ .digest('hex');
147
+ }
148
+ async function buildCompressedPatch(gitRoot, baseSha, workCommit, deadline) {
149
+ return new Promise((resolvePatch, rejectPatch) => {
150
+ const child = spawn('git', ['diff', '--binary', baseSha, workCommit], {
151
+ cwd: gitRoot,
152
+ stdio: ['ignore', 'pipe', 'pipe'],
153
+ });
154
+ const gzip = createGzip();
155
+ const chunks = [];
156
+ let compressedBytes = 0;
157
+ let childClosed = false;
158
+ let gzipEnded = false;
159
+ let childSucceeded = false;
160
+ let settled = false;
161
+ const timer = setTimeout(() => {
162
+ child.kill('SIGKILL');
163
+ fail(new PrecomputeFailure('Precompute timed out.'));
164
+ }, remainingMs(deadline));
165
+ const clear = () => clearTimeout(timer);
166
+ const fail = (error) => {
167
+ if (settled)
168
+ return;
169
+ settled = true;
170
+ clear();
171
+ child.kill('SIGKILL');
172
+ gzip.destroy();
173
+ rejectPatch(error);
174
+ };
175
+ const finish = () => {
176
+ if (settled || !childClosed || !gzipEnded)
177
+ return;
178
+ if (!childSucceeded) {
179
+ fail(new PrecomputeFailure('Could not build the working-tree patch.'));
180
+ return;
181
+ }
182
+ settled = true;
183
+ clear();
184
+ resolvePatch(Buffer.concat(chunks, compressedBytes));
185
+ };
186
+ child.stderr.resume();
187
+ child.once('error', () => fail(new PrecomputeFailure('Could not build the working-tree patch.')));
188
+ child.once('close', code => {
189
+ childClosed = true;
190
+ childSucceeded = code === 0;
191
+ finish();
192
+ });
193
+ gzip.on('data', (chunk) => {
194
+ compressedBytes += chunk.length;
195
+ chunks.push(chunk);
196
+ });
197
+ gzip.once('error', () => fail(new PrecomputeFailure('Could not compress the working-tree patch.')));
198
+ gzip.once('end', () => {
199
+ gzipEnded = true;
200
+ finish();
201
+ });
202
+ child.stdout.pipe(gzip);
203
+ });
204
+ }
205
+ export async function derivePrecomputeRequest(gitRoot, owner, repository, deadline = Date.now() + TOTAL_WALL_MS) {
206
+ const baseSha = deriveBaseSha(gitRoot, deadline);
207
+ const treeSha = deriveWorktreeTreeSha(gitRoot, deadline);
208
+ const workCommit = deriveWorkCommit(gitRoot, baseSha, treeSha, deadline);
209
+ const patchGzip = await buildCompressedPatch(gitRoot, baseSha, workCommit, deadline);
210
+ const cacheKey = [
211
+ baseSha.slice(0, 12),
212
+ workCommit.slice(0, 12),
213
+ treeSha.slice(0, 16),
214
+ 'slim',
215
+ slimInputHash(),
216
+ ].join('-');
217
+ return {
218
+ treeSha,
219
+ request: {
220
+ owner,
221
+ repository,
222
+ baseCommit: baseSha,
223
+ workCommit,
224
+ cacheKey,
225
+ patchGzBase64: patchGzip.toString('base64'),
226
+ patchSha256: createHash('sha256').update(patchGzip).digest('hex'),
227
+ },
228
+ };
229
+ }
230
+ function precomputeResult(response) {
231
+ if (!response || typeof response !== 'object' || Array.isArray(response)) {
232
+ throw new PrecomputeFailure('The precompute service returned an invalid response.');
233
+ }
234
+ const { status, runId } = response;
235
+ if (status === 'queued' && typeof runId === 'string' && RUN_ID.test(runId)) {
236
+ return { outcome: 'enqueued', runId };
237
+ }
238
+ if (status === 'cached')
239
+ return { outcome: 'cached' };
240
+ if (status === 'existing')
241
+ return { outcome: 'existing' };
242
+ throw new PrecomputeFailure('The precompute service returned an invalid response.');
243
+ }
244
+ function normalModeFailure(error) {
245
+ if (error instanceof PrecomputeFailure)
246
+ return error;
247
+ if (error instanceof NotLoggedInError) {
248
+ return new PrecomputeFailure('Authentication is required. Run `haystack login` first.');
249
+ }
250
+ if (error instanceof HaystackApiError) {
251
+ if (error.status === 401 || error.status === 403) {
252
+ return new PrecomputeFailure('Authentication was rejected. Run `haystack login` and try again.');
253
+ }
254
+ if (error.status === 429) {
255
+ return new PrecomputeFailure('Precompute requests are temporarily rate limited. Try again later.');
256
+ }
257
+ return new PrecomputeFailure(`The precompute request failed (HTTP ${error.status}).`);
258
+ }
259
+ return new PrecomputeFailure('The precompute request failed.');
260
+ }
261
+ function printResult(outcome, runId) {
262
+ if (outcome === 'enqueued') {
263
+ console.log(chalk.green(`Precompute request queued${runId ? ` (${runId})` : ''}.`));
264
+ }
265
+ else if (outcome === 'cached') {
266
+ console.log(chalk.green('Precompute result is already cached.'));
267
+ }
268
+ else if (outcome === 'existing') {
269
+ console.log(chalk.green('A matching precompute request already exists.'));
270
+ }
271
+ }
272
+ export async function verifyPrecomputeCommand(options) {
273
+ const startedAt = Date.now();
274
+ const deadline = startedAt + TOTAL_WALL_MS;
275
+ let outcome = 'error';
276
+ try {
277
+ const gitRoot = findGitRoot();
278
+ if (!gitRoot) {
279
+ throw new PrecomputeFailure('Run this command from a git repository.');
280
+ }
281
+ const repository = resolveOriginRepository(gitRoot);
282
+ const derivation = await derivePrecomputeRequest(gitRoot, repository.owner, repository.repository, deadline);
283
+ let token;
284
+ try {
285
+ const auth = await resolveAuthContext({
286
+ owner: repository.owner,
287
+ repo: repository.repository,
288
+ });
289
+ token = auth.token;
290
+ }
291
+ catch (error) {
292
+ if (error instanceof NotLoggedInError) {
293
+ outcome = 'skipped_no_auth';
294
+ if (options.hook)
295
+ return;
296
+ }
297
+ throw error;
298
+ }
299
+ const controller = new AbortController();
300
+ const fetchTimer = setTimeout(() => controller.abort(), remainingMs(deadline));
301
+ let response;
302
+ try {
303
+ response = await haystackJson('/api/agent/cloud-verifier/front-half', token, {
304
+ method: 'POST',
305
+ headers: { 'Content-Type': 'application/json' },
306
+ body: JSON.stringify(derivation.request),
307
+ signal: controller.signal,
308
+ });
309
+ }
310
+ finally {
311
+ clearTimeout(fetchTimer);
312
+ }
313
+ const result = precomputeResult(response);
314
+ outcome = result.outcome;
315
+ if (!options.hook)
316
+ printResult(outcome, result.runId);
317
+ }
318
+ catch (error) {
319
+ if (!options.hook)
320
+ throw normalModeFailure(error);
321
+ }
322
+ finally {
323
+ const wallMs = Date.now() - startedAt;
324
+ trackPrecomputeEvent(outcome, wallMs, Math.max(1, TOTAL_WALL_MS - wallMs));
325
+ if (options.hook)
326
+ process.exitCode = 0;
327
+ }
328
+ }
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ import { cloudVerifierDataStoreDriftCommand } from './commands/cloud-verifier-da
36
36
  import { cloudVerifierBehaviorsValidateCommand } from './commands/cloud-verifier-behaviors.js';
37
37
  import { prepareUniverseReviewCommand } from './commands/prepare-universe-review.js';
38
38
  import { scaffoldProvisionalUniverseCommand } from './commands/scaffold-provisional-universe.js';
39
- import { hooksInstall, hooksStatus, hooksUpdate } from './commands/hooks.js';
39
+ import { hooksInstall, hooksStatus } from './commands/hooks.js';
40
40
  import { submitCommand } from './commands/submit.js';
41
41
  import { installSessionHooks, sessionHooksStatus } from './commands/install-session-hooks.js';
42
42
  import { listPolicies, addPolicy, removePolicy, initPolicies, addInstruction } from './commands/policy.js';
@@ -49,7 +49,6 @@ import { prStatusCommand } from './commands/pr-status.js';
49
49
  import { prReadCommand } from './commands/pr.js';
50
50
  import { inboxListCommand } from './commands/inbox.js';
51
51
  import { askHaystackCommand } from './commands/ask.js';
52
- import { tracesGetCommand, tracesListCommand } from './commands/traces.js';
53
52
  import { telemetryInstrumentCommand } from './commands/telemetry.js';
54
53
  import { setupCommand } from './commands/setup.js';
55
54
  import { schemaCommand, listSchemas } from './commands/schema-cmd.js';
@@ -134,7 +133,6 @@ program
134
133
  // Defining --no-auto-merge also accepts --auto-merge; default is "ask" unless
135
134
  // one is explicitly passed (detected via getOptionValueSource below).
136
135
  .option('--no-auto-merge', 'Set auto-merge in the written config (--auto-merge / --no-auto-merge; omit to be asked)')
137
- .option('--skip-entire', 'Skip the Entire CLI / git-hooks (session tracking) step')
138
136
  .option('--answers <file>', 'JSON file of {questionId: value} pre-supplied answers')
139
137
  .addHelpText('after', `
140
138
  Steps: verify GitHub App → select repos → scan (rules/policies/instructions) →
@@ -143,7 +141,7 @@ review → write .haystack/pr-rules.yml + review-policy.md + .haystack.json.
143
141
  Three ways to run:
144
142
  • Interactive (default): prompts in your terminal.
145
143
  • Autonomous/CI: supply every decision via flags, e.g.
146
- haystack setup --repo owner/name --yes --no-auto-merge --skip-entire
144
+ haystack setup --repo owner/name --yes --no-auto-merge
147
145
  • Coding agent: --json speaks NDJSON on stdout and reads answers on stdin, so an
148
146
  agent can auto-answer or relay questions to the user. Pre-supplied answers
149
147
  (flags / --answers) skip the matching question.
@@ -165,7 +163,6 @@ Examples:
165
163
  repos: options.repo,
166
164
  yes: options.yes,
167
165
  autoMerge: autoMergeExplicit ? options.autoMerge : undefined,
168
- skipEntire: options.skipEntire,
169
166
  answersFile: options.answers,
170
167
  });
171
168
  });
@@ -213,6 +210,18 @@ Examples:
213
210
  `)
214
211
  .action((specimen, options) => runPublicCommand(() => verifyCommand(specimen, options), options.json));
215
212
  const verifyRunArg = 'Run id, chapter id (newest run of that chapter), or "latest" (default)';
213
+ verify
214
+ .command('precompute')
215
+ .description('Precompute reusable verification inputs for the current working tree')
216
+ .option('--hook', 'Run silently as a best-effort repository hook')
217
+ .action(async (options) => {
218
+ await runPublicCommand(async () => {
219
+ const { verifyPrecomputeCommand } = await import('./commands/verify-precompute.js');
220
+ await verifyPrecomputeCommand(options);
221
+ });
222
+ if (options.hook)
223
+ process.exitCode = 0;
224
+ });
216
225
  const hostedVerify = verify
217
226
  .command('hosted')
218
227
  .description('Run the production Cloud Verifier against exact GitHub commits');
@@ -655,7 +664,7 @@ program
655
664
  .addHelpText('after', `
656
665
  This command is designed for AI coding agents to submit PRs.
657
666
 
658
- 1. Runs pre-PR triage (code review, rules, instruction drift) via sub-agents
667
+ 1. Runs pre-PR triage (code review and rules) via sub-agents
659
668
  2. Pushes the current branch to origin
660
669
  3. Creates a pull request on GitHub
661
670
  4. Waits for Haystack analysis results (triggered via GitHub App webhook)
@@ -664,15 +673,14 @@ Pre-PR Triage:
664
673
  Before creating the PR, haystack spawns parallel sub-agents to check for:
665
674
  • Code review bugs (logic errors, null crashes, security issues)
666
675
  • Rule violations (from .haystack/pr-rules.yml)
667
- • Instruction drift (AI agent deviations from user instructions)
668
676
 
669
677
  Use --force to skip triage entirely.
670
678
  Use --no-wait to skip waiting for analysis results.
671
679
 
672
680
  Triage budgets:
673
681
  • --max-turns <n> Raise/lower the per-checker tool-use turn cap.
674
- Defaults: code-review 8, rules-validator 10,
675
- intent-drift 10. Applies the same N to all three.
682
+ Defaults: code-review 8, rules-validator 10.
683
+ Applies the same N to both.
676
684
  • --triage-timeout <sec> Raise/lower the per-checker wall-clock timeout
677
685
  (default: 180s).
678
686
 
@@ -790,7 +798,7 @@ inbox
790
798
  const prProgram = program.command('pr').description('Inspect one pull request');
791
799
  prProgram
792
800
  .command('get <ref>')
793
- .description('Get triage, merge blockers, and trace availability')
801
+ .description('Get triage and merge blockers')
794
802
  .option('--json', 'Output as JSON')
795
803
  .action((ref, options) => runPublicCommand(() => prReadCommand(ref, options), options.json));
796
804
  program
@@ -799,19 +807,6 @@ program
799
807
  .option('--json', 'Output the answer and every consulted customer-facing source as JSON')
800
808
  .option('--session <id>', 'Continue a previous Ask Haystack session')
801
809
  .action((ref, question, options) => runPublicCommand(() => askHaystackCommand(ref, question, options), options.json));
802
- const traces = program.command('traces').description('Inspect customer-owned Entire traces');
803
- traces
804
- .command('list <ref>')
805
- .description('List retained checkpoints for a pull request')
806
- .option('--json', 'Output as JSON')
807
- .action((ref, options) => runPublicCommand(() => tracesListCommand(ref, options), options.json));
808
- traces
809
- .command('get <ref> <checkpoint>')
810
- .description('Get retained transcript chunks for one checkpoint')
811
- .option('--cursor <cursor>', 'Zero-based chunk cursor')
812
- .option('--limit <count>', 'Chunks per page', '20')
813
- .option('--json', 'Output as JSON')
814
- .action((ref, checkpoint, options) => runPublicCommand(() => tracesGetCommand(ref, checkpoint, options), options.json));
815
810
  const telemetry = program.command('telemetry').description('Add privacy-safe production telemetry without an application SDK');
816
811
  telemetry
817
812
  .command('instrument <dist>')
@@ -870,15 +865,27 @@ program
870
865
  .option('--run <run-id>', 'Resume and wait for an existing exact run id')
871
866
  .option('--poll-interval <seconds>', 'Status polling interval', '3')
872
867
  .option('--timeout <seconds>', 'Maximum time to wait; the server run continues after timeout', '7200')
868
+ .option('--case <case-id>', 'Select one exact generated case for universe exploration')
869
+ .option('--universe <role>', 'Select control-before, before, or after')
870
+ .option('--replay', 'Replay the registered case command in the selected universe')
871
+ .option('--exec-json <argv>', 'Run a JSON argv array in the selected universe')
872
+ .option('--cwd <path>', 'Repository-relative working directory for --exec-json')
873
+ .option('--command-timeout <seconds>', 'Maximum selected-universe command runtime')
874
+ .option('--action <action-id>', 'Resume one exact universe action')
873
875
  .addHelpText('after', `
874
876
  Executes the change instead of reading it: infers intent from the diff
875
- alone, designs pre-registered behavioral test cases, runs them against
876
- both sides of the PR in isolated sandboxes, and reports a mechanical
877
- verdict of unexpected behavior changes.
877
+ alone, designs pre-registered behavioral test cases, runs control-before,
878
+ before, and after in isolated sandboxes, and reports anomaly candidates
879
+ outside the registered intent.
878
880
 
879
881
  The command waits for a behavioral verdict by default. Use --no-wait to
880
882
  return the run id immediately, or --run <run-id> to resume waiting.
881
883
 
884
+ After a completed run, inspect or replay one retained case universe without
885
+ receiving provider credentials:
886
+ haystack design-verify owner/repo#123 --run RUN --case case-00001 --universe before --replay
887
+ haystack design-verify owner/repo#123 --run RUN --case case-00001 --universe after --exec-json '["rg","TODO"]'
888
+
882
889
  PR identifier formats:
883
890
  123 PR number (uses current repo)
884
891
  owner/repo#123 Fully qualified
@@ -1188,42 +1195,34 @@ const hooks = program
1188
1195
  .description('Manage git hooks for AI agent quality checks');
1189
1196
  hooks
1190
1197
  .command('install')
1191
- .description('Install Haystack git hooks and Entire CLI')
1192
- .option('--version <version>', 'Entire CLI version (default: pinned)')
1198
+ .description('Install Haystack git hooks')
1193
1199
  .option('-f, --force', 'Overwrite existing hooks')
1194
- .option('--skip-entire', 'Skip Entire binary download')
1195
1200
  .addHelpText('after', `
1196
1201
  This installs:
1197
1202
  • Git hooks for AI agent quality checks (pre-commit, commit-msg, etc.)
1198
1203
  • Agent context detector (identifies AI agent sessions)
1199
1204
  • Truncation checker (prevents code truncation by LLMs)
1200
- • Entire CLI binary for session tracking (powered by https://entire.dev)
1201
1205
 
1202
1206
  Hooks are installed to <repo>/hooks/ and git is configured to use them.
1203
1207
 
1204
1208
  Examples:
1205
- haystack hooks install # Install with pinned Entire version
1206
1209
  haystack hooks install --force # Overwrite existing hooks
1207
- haystack hooks install --skip-entire # Only install Haystack hooks
1208
1210
  `)
1209
1211
  .action(hooksInstall);
1210
1212
  hooks
1211
1213
  .command('status')
1212
1214
  .description('Check hooks installation status')
1213
1215
  .action(hooksStatus);
1214
- hooks
1215
- .command('update')
1216
- .description('Update Entire CLI to the latest version')
1217
- .action(hooksUpdate);
1218
1216
  hooks
1219
1217
  .command('install-session')
1220
- .description('Install session-start hooks for coding CLIs')
1218
+ .description('Install session-start and Stop hooks for coding CLIs')
1221
1219
  .option('--cli <name>', 'Target CLI: claude, codex, gemini, or all')
1222
1220
  .addHelpText('after', `
1223
1221
  Configures your coding CLI to run \`haystack triage --hook\` on session start.
1224
1222
  This shows pending PR analysis results when you open a new terminal session.
1223
+ Claude Code also runs \`haystack verify precompute --hook\` when a session stops.
1225
1224
 
1226
- Claude Code: Native SessionStart hook (.claude/settings.json)
1225
+ Claude Code: Native SessionStart and Stop hooks (.claude/settings.json)
1227
1226
  Codex CLI: AGENTS.md instructions
1228
1227
  Gemini CLI: GEMINI.md instructions
1229
1228
 
package/dist/schema.d.ts CHANGED
@@ -12,11 +12,10 @@
12
12
  export declare const SCHEMA_VERSIONS: {
13
13
  readonly triage: "2.0.0";
14
14
  readonly setup: "1.0.0";
15
- readonly pr: "2.0.0";
15
+ readonly pr: "3.0.0";
16
16
  readonly 'pr-status': "1.0.0";
17
17
  readonly inbox: "1.0.0";
18
18
  readonly ask: "1.0.0";
19
- readonly traces: "1.0.0";
20
19
  readonly submit: "1.0.0";
21
20
  readonly action: "1.0.0";
22
21
  readonly 'cloud-verifier': "1.0.0";
package/dist/schema.js CHANGED
@@ -12,11 +12,10 @@
12
12
  export const SCHEMA_VERSIONS = {
13
13
  triage: '2.0.0',
14
14
  setup: '1.0.0',
15
- pr: '2.0.0',
15
+ pr: '3.0.0',
16
16
  'pr-status': '1.0.0',
17
17
  inbox: '1.0.0',
18
18
  ask: '1.0.0',
19
- traces: '1.0.0',
20
19
  submit: '1.0.0',
21
20
  action: '1.0.0',
22
21
  'cloud-verifier': '1.0.0',
@@ -22,10 +22,3 @@ export declare function buildRulesValidatorPrompt(baseBranch: string, rulesYaml:
22
22
  filename: string;
23
23
  content: string;
24
24
  }[], precomputedDiff?: string | null): string | null;
25
- /**
26
- * Build the intent drift prompt.
27
- * Only runs if relevant trace files exist.
28
- *
29
- * @returns The prompt string, or null if no trace files provided.
30
- */
31
- export declare function buildIntentDriftPrompt(baseBranch: string, traceFiles: string[], outputPath: string, maxTurns: number, timeoutMs: number, precomputedDiff?: string | null): string | null;