@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.
- package/README.md +13 -20
- package/dist/assets/hooks/agent-context/detect.ts +7 -7
- package/dist/assets/hooks/agent-context/parsers/claude.ts +1 -1
- package/dist/assets/hooks/scripts/commit-msg.sh +1 -3
- package/dist/assets/hooks/scripts/post-commit.sh +1 -3
- package/dist/assets/hooks/scripts/pre-push.sh +1 -4
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +1 -2
- package/dist/assets/telemetry/browser-runtime.js +651 -43
- package/dist/assets/telemetry/runtime.cjs +62 -68
- package/dist/commands/design-verify.d.ts +11 -3
- package/dist/commands/design-verify.js +147 -4
- package/dist/commands/hooks.d.ts +1 -5
- package/dist/commands/hooks.js +10 -112
- package/dist/commands/install-session-hooks.d.ts +2 -2
- package/dist/commands/install-session-hooks.js +71 -21
- package/dist/commands/mcp.js +1 -34
- package/dist/commands/pr.js +2 -7
- package/dist/commands/setup.d.ts +1 -2
- package/dist/commands/setup.js +18 -272
- package/dist/commands/submit.js +1 -2
- package/dist/commands/verify-hosted.d.ts +13 -0
- package/dist/commands/verify-hosted.js +49 -1
- package/dist/commands/verify-precompute.d.ts +19 -0
- package/dist/commands/verify-precompute.js +328 -0
- package/dist/index.js +37 -38
- package/dist/schema.d.ts +1 -2
- package/dist/schema.js +1 -2
- package/dist/triage/prompts.d.ts +0 -7
- package/dist/triage/prompts.js +0 -145
- package/dist/triage/runner.js +3 -22
- package/dist/triage/types.d.ts +1 -8
- package/dist/types.d.ts +8 -8
- package/dist/types.js +1 -1
- package/dist/utils/design-verifier-api.d.ts +71 -0
- package/dist/utils/design-verifier-api.js +96 -0
- package/dist/utils/design-verifier-result.d.ts +7 -2
- package/dist/utils/design-verifier-result.js +67 -21
- package/dist/utils/git.d.ts +5 -1
- package/dist/utils/git.js +19 -9
- package/dist/utils/github-api.js +11 -14
- package/dist/utils/hooks.d.ts +1 -15
- package/dist/utils/hooks.js +1 -127
- package/dist/utils/telemetry.d.ts +2 -0
- package/dist/utils/telemetry.js +81 -8
- package/package.json +2 -1
- package/schemas/cloud-verifier.v1.json +69 -3
- package/schemas/{pr.v2.json → pr.v3.json} +3 -12
- package/dist/commands/traces.d.ts +0 -48
- package/dist/commands/traces.js +0 -92
- package/dist/triage/traces.d.ts +0 -20
- package/dist/triage/traces.js +0 -311
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
876
|
-
|
|
877
|
-
|
|
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
|
|
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
|
|
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: "
|
|
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: '
|
|
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',
|
package/dist/triage/prompts.d.ts
CHANGED
|
@@ -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;
|