@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
package/dist/commands/setup.js
CHANGED
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
* 5. Scan for review policies
|
|
10
10
|
* 6. Review discovered items (toggle on/off)
|
|
11
11
|
* 7. Confirm & write .haystack.json to selected repos
|
|
12
|
-
* 8.
|
|
12
|
+
* 8. Offer to install local Haystack hooks
|
|
13
13
|
*/
|
|
14
14
|
import chalk from 'chalk';
|
|
15
15
|
import { execFileSync, execSync } from 'child_process';
|
|
16
|
-
import {
|
|
17
|
-
import { basename, join } from 'path';
|
|
16
|
+
import { basename } from 'path';
|
|
18
17
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
19
18
|
import { hooksInstall } from './hooks.js';
|
|
19
|
+
import { installSessionHooks } from './install-session-hooks.js';
|
|
20
20
|
import { findGitRoot } from '../utils/hooks.js';
|
|
21
|
-
import { trackError
|
|
21
|
+
import { trackError } from '../utils/telemetry.js';
|
|
22
22
|
import { createPrompter, loadAnswersFile, PromptCancelledError } from '../utils/prompter.js';
|
|
23
23
|
/** The active prompter for this `setup` invocation (TTY or JSON). */
|
|
24
24
|
let ui;
|
|
@@ -33,16 +33,11 @@ function buildAnswerMap(options) {
|
|
|
33
33
|
answers.select_repos = options.repos;
|
|
34
34
|
if (typeof options.autoMerge === 'boolean')
|
|
35
35
|
answers.auto_merge = options.autoMerge;
|
|
36
|
-
if (options.skipEntire) {
|
|
37
|
-
answers.install_entire = false;
|
|
38
|
-
answers.upgrade_entire = false;
|
|
39
|
-
answers.install_hooks = false;
|
|
40
|
-
}
|
|
41
36
|
if (options.yes) {
|
|
42
37
|
// Accept the interactive wizard's defaults non-interactively: keep every
|
|
43
38
|
// discovered item (skip the toggle UI), confirm the write, and say yes to
|
|
44
|
-
// the same things the prompts default to (auto-merge
|
|
45
|
-
//
|
|
39
|
+
// the same things the prompts default to (auto-merge and hooks). Use
|
|
40
|
+
// --no-auto-merge to opt out. Only fill ids not
|
|
46
41
|
// already set by an explicit flag or --answers.
|
|
47
42
|
if (!('want_to_toggle' in answers))
|
|
48
43
|
answers.want_to_toggle = false;
|
|
@@ -53,10 +48,6 @@ function buildAnswerMap(options) {
|
|
|
53
48
|
// Consequential default applied without an interactive prompt — say so.
|
|
54
49
|
console.error('--yes: enabling auto-merge (the wizard default). Pass --no-auto-merge to opt out.');
|
|
55
50
|
}
|
|
56
|
-
if (!('install_entire' in answers))
|
|
57
|
-
answers.install_entire = true;
|
|
58
|
-
if (!('upgrade_entire' in answers))
|
|
59
|
-
answers.upgrade_entire = true;
|
|
60
51
|
if (!('install_hooks' in answers))
|
|
61
52
|
answers.install_hooks = true;
|
|
62
53
|
// Don't auto-launch a browser in an unattended --yes run; the feed URL is
|
|
@@ -135,8 +126,7 @@ const BOOTSTRAP_PR_BODY_INTRO = 'This PR was opened automatically by the Haystac
|
|
|
135
126
|
'It adds the Haystack onboarding config:\n' +
|
|
136
127
|
'- `.haystack/pr-rules.yml` — rules Haystack enforces on every PR\n' +
|
|
137
128
|
'- `.haystack/review-policy.md` — path-scoped review policies + review instructions\n' +
|
|
138
|
-
'- `.haystack.json` — your Haystack merge-queue settings\n'
|
|
139
|
-
'- `.entire/settings.json` — enables AI-session trace capture (powers intent-drift triage)\n\n';
|
|
129
|
+
'- `.haystack.json` — your Haystack merge-queue settings\n\n';
|
|
140
130
|
// Auto-merge ON: Haystack bootstrap-merges the PR itself once labeled.
|
|
141
131
|
const BOOTSTRAP_PR_OUTRO_AUTOMERGE = 'Haystack will bootstrap-merge this PR automatically once the labels are applied. ' +
|
|
142
132
|
'You can tweak the files from GitHub or re-run `haystack setup` anytime.';
|
|
@@ -256,64 +246,6 @@ async function createCommitWithFiles(owner, repo, baseSha, files, message, token
|
|
|
256
246
|
}
|
|
257
247
|
return (await commitRes.json()).sha;
|
|
258
248
|
}
|
|
259
|
-
/**
|
|
260
|
-
* Compute the desired `.entire/settings.json` content for a repo: fetch the
|
|
261
|
-
* existing file (if any), strip the legacy `skip_raw_transcript` option, and
|
|
262
|
-
* serialize. Returns `null` when the file already exists and is unchanged
|
|
263
|
-
* (nothing to write). Otherwise returns the content AND the existing blob
|
|
264
|
-
* sha (or null if the file doesn't exist) — so the direct-write path can
|
|
265
|
-
* issue a correct update PUT without a second redundant GET. Pure read +
|
|
266
|
-
* transform — does no writes itself, so it can feed either the bootstrap-PR
|
|
267
|
-
* commit or the direct-write path.
|
|
268
|
-
*
|
|
269
|
-
* Non-404 read failures throw — they must NOT be silently coerced to
|
|
270
|
-
* "file doesn't exist", which would make the subsequent PUT a create and
|
|
271
|
-
* GitHub reject it 422, masking the real transient error.
|
|
272
|
-
*/
|
|
273
|
-
async function buildEntireSettingsContent(repoFullName, token) {
|
|
274
|
-
const [owner, repo] = repoFullName.split('/');
|
|
275
|
-
const headers = ghHeaders(token);
|
|
276
|
-
let settings = { enabled: true };
|
|
277
|
-
let originalContent = '';
|
|
278
|
-
let exists = false;
|
|
279
|
-
let existingSha = null;
|
|
280
|
-
const getResp = await fetch(`${GITHUB_PROXY}/repos/${owner}/${repo}/contents/.entire/settings.json`, { headers });
|
|
281
|
-
if (getResp.ok) {
|
|
282
|
-
exists = true;
|
|
283
|
-
const data = (await getResp.json());
|
|
284
|
-
existingSha = data.sha;
|
|
285
|
-
originalContent = Buffer.from(data.content, 'base64').toString('utf-8');
|
|
286
|
-
try {
|
|
287
|
-
settings = JSON.parse(originalContent);
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
console.log(chalk.dim(` Existing .entire/settings.json in ${repoFullName} is malformed — will repair`));
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
else if (getResp.status !== 404) {
|
|
294
|
-
const errText = await getResp.text().catch(() => '');
|
|
295
|
-
const error = new Error(`Failed to read .entire/settings.json: ${getResp.status} ${errText}`);
|
|
296
|
-
trackError('entire_settings_read_failed', {
|
|
297
|
-
error_message: error.message,
|
|
298
|
-
repo: repoFullName,
|
|
299
|
-
status: getResp.status,
|
|
300
|
-
});
|
|
301
|
-
throw error;
|
|
302
|
-
}
|
|
303
|
-
// Remove legacy skip_raw_transcript if present (v0.5.3-haystack.2+ compacts automatically)
|
|
304
|
-
if (settings.strategy_options && typeof settings.strategy_options === 'object') {
|
|
305
|
-
const strategyOptions = settings.strategy_options;
|
|
306
|
-
delete strategyOptions.skip_raw_transcript;
|
|
307
|
-
if (Object.keys(strategyOptions).length === 0) {
|
|
308
|
-
delete settings.strategy_options;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
const updatedContent = JSON.stringify(settings, null, 2) + '\n';
|
|
312
|
-
// Nothing to do if the file already exists unchanged.
|
|
313
|
-
if (exists && updatedContent === originalContent)
|
|
314
|
-
return null;
|
|
315
|
-
return { content: updatedContent, existingSha };
|
|
316
|
-
}
|
|
317
249
|
async function addBootstrapLabels(owner, repo, prNumber, prUrl, token, autoMerge) {
|
|
318
250
|
// CRITICAL, not best-effort. ONBOARDING_BOOTSTRAP_LABEL is the identity marker
|
|
319
251
|
// that keeps the PR recognized in the user's Analyzing / Good to Merge tabs
|
|
@@ -395,7 +327,7 @@ async function assignActorToPr(owner, repo, prNumber, login, token) {
|
|
|
395
327
|
}
|
|
396
328
|
/**
|
|
397
329
|
* Protected-branch fallback: build ONE commit containing ALL the onboarding
|
|
398
|
-
* files
|
|
330
|
+
* config files via the Git Data API, CREATE
|
|
399
331
|
* a branch ref at it, open a PR, and apply the auto-merge + bootstrap labels.
|
|
400
332
|
*
|
|
401
333
|
* Why every file in one commit: a ruleset whose `pull_request` rule targets
|
|
@@ -403,8 +335,7 @@ async function assignActorToPr(owner, repo, prNumber, login, token) {
|
|
|
403
335
|
* commit to the default branch NOR add a second commit to the onboarding
|
|
404
336
|
* branch afterward. The only thing allowed is *creating* a ref. So all the
|
|
405
337
|
* bootstrap-critical files (`.haystack/pr-rules.yml`, `.haystack/review-policy.md`,
|
|
406
|
-
* `.haystack.json`
|
|
407
|
-
* trace capture intent-drift triage runs on) must land in the single commit the
|
|
338
|
+
* and `.haystack.json`) must land in the single commit the
|
|
408
339
|
* branch is created at. Mirrors the web wizard's PR-fallback path.
|
|
409
340
|
*/
|
|
410
341
|
async function openBootstrapConfigPR(owner, repo, configFiles, token, defaultBranch, autoMergeEnabled, assigneeLogin) {
|
|
@@ -416,36 +347,6 @@ async function openBootstrapConfigPR(owner, repo, configFiles, token, defaultBra
|
|
|
416
347
|
path: f.path,
|
|
417
348
|
content: f.content,
|
|
418
349
|
}));
|
|
419
|
-
// Bundle .entire/settings.json into the same commit — on a ~ALL ruleset
|
|
420
|
-
// we can't add it later. It's repo-level config (independent of whether
|
|
421
|
-
// this dev installs the Entire binary locally), so it rides along here.
|
|
422
|
-
// The Git Data tree API overwrites by path, so existingSha isn't needed here.
|
|
423
|
-
//
|
|
424
|
-
// Best-effort: .entire/settings.json is the OPTIONAL file; .haystack.json
|
|
425
|
-
// is the bootstrap-critical one. A transient non-404 read failure here
|
|
426
|
-
// must NOT abort the whole PR (buildEntireSettingsContent throws on
|
|
427
|
-
// non-404). Catch it, warn, and ship the PR with just .haystack.json —
|
|
428
|
-
// far better than failing setup over the optional file. The customer can
|
|
429
|
-
// re-run `haystack setup` to pick up .entire/settings.json once GitHub
|
|
430
|
-
// is healthy.
|
|
431
|
-
// entireSettings === null means the file already exists unchanged — there's
|
|
432
|
-
// nothing to bundle, but the repo IS correctly configured, so that still
|
|
433
|
-
// counts as bundled. Only a read *failure* (caught below) sets this false.
|
|
434
|
-
let entireSettingsBundled = true;
|
|
435
|
-
try {
|
|
436
|
-
const entireSettings = await buildEntireSettingsContent(`${owner}/${repo}`, token);
|
|
437
|
-
if (entireSettings !== null) {
|
|
438
|
-
files.push({ path: '.entire/settings.json', content: entireSettings.content });
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
catch (err) {
|
|
442
|
-
// buildEntireSettingsContent already tracked this with the real HTTP
|
|
443
|
-
// status before throwing — don't re-emit a duplicate event here. Just
|
|
444
|
-
// warn and ship the PR with .haystack.json only.
|
|
445
|
-
entireSettingsBundled = false;
|
|
446
|
-
console.log(chalk.yellow(` \u26a0 Couldn't read .entire/settings.json for ${owner}/${repo} (${err instanceof Error ? err.message : err}) — ` +
|
|
447
|
-
`opening the bootstrap PR with .haystack.json only. Re-run \`haystack setup\` to add session tracking.`));
|
|
448
|
-
}
|
|
449
350
|
const commitSha = await createCommitWithFiles(owner, repo, baseSha, files, CONFIG_COMMIT_MSG, token);
|
|
450
351
|
await createBranch(owner, repo, branchName, commitSha, token);
|
|
451
352
|
const pr = await createBootstrapPR(owner, repo, branchName, defaultBranch, token, autoMergeEnabled);
|
|
@@ -459,7 +360,7 @@ async function openBootstrapConfigPR(owner, repo, configFiles, token, defaultBra
|
|
|
459
360
|
// the repo hasn't opted out. With auto-merge off the PR is still opened, marked,
|
|
460
361
|
// and analyzed — it just waits for the user to merge it.
|
|
461
362
|
await addBootstrapLabels(owner, repo, pr.number, pr.html_url, token, autoMergeEnabled);
|
|
462
|
-
return { prUrl: pr.html_url
|
|
363
|
+
return { prUrl: pr.html_url };
|
|
463
364
|
}
|
|
464
365
|
/**
|
|
465
366
|
* Write the onboarding config files to a repo by ALWAYS opening a PR
|
|
@@ -1084,8 +985,7 @@ async function stepConfirm(byRepo, token, assigneeLogin) {
|
|
|
1084
985
|
ui.clearProgress();
|
|
1085
986
|
// Onboarding always opens a PR (never a direct commit). With auto-merge on,
|
|
1086
987
|
// Haystack merges it once its labels are processed; with auto-merge off it
|
|
1087
|
-
// waits for the user.
|
|
1088
|
-
// the same commit.
|
|
988
|
+
// waits for the user.
|
|
1089
989
|
bootstrapPRs.set(repoFullName, writeResult.bootstrapPR);
|
|
1090
990
|
// Don't print the GitHub PR URL: the user reviews and merges from their
|
|
1091
991
|
// Haystack feed, not the GitHub PR page. The feed link is the single CTA
|
|
@@ -1172,17 +1072,8 @@ async function runSetupFlow(options) {
|
|
|
1172
1072
|
ui.result({ status: 'cancelled' });
|
|
1173
1073
|
return;
|
|
1174
1074
|
}
|
|
1175
|
-
// Step 7:
|
|
1176
|
-
|
|
1177
|
-
// .entire/settings.json rode along in that PR's commit.
|
|
1178
|
-
// --skip-entire skips the whole step (binary install + hooks) — not just the
|
|
1179
|
-
// install prompt.
|
|
1180
|
-
if (options.skipEntire) {
|
|
1181
|
-
console.log(chalk.dim(' Skipping session tracking (--skip-entire).\n'));
|
|
1182
|
-
}
|
|
1183
|
-
else {
|
|
1184
|
-
await stepInstallEntire(selectedRepos, token, bootstrapPRs);
|
|
1185
|
-
}
|
|
1075
|
+
// Step 7: Offer Haystack's local quality hooks when setup runs in a selected repo.
|
|
1076
|
+
await stepInstallHooks(selectedRepos);
|
|
1186
1077
|
// Close the loop with ONE prominent CTA: the Haystack feed. The Configure
|
|
1187
1078
|
// Haystack PR (and every PR after it) is reviewed and merged from the feed,
|
|
1188
1079
|
// NOT the GitHub PR page, so the feed is the only link we surface. In JSON
|
|
@@ -1217,155 +1108,9 @@ async function runSetupFlow(options) {
|
|
|
1217
1108
|
});
|
|
1218
1109
|
}
|
|
1219
1110
|
// =============================================================================
|
|
1220
|
-
// Step 7: Install
|
|
1111
|
+
// Step 7: Install Haystack git hooks
|
|
1221
1112
|
// =============================================================================
|
|
1222
|
-
|
|
1223
|
-
const ENTIRE_RELEASE_BASE = `https://github.com/haystackeditor/cli/releases/download/${ENTIRE_VERSION}`;
|
|
1224
|
-
function getEntireBinaryName() {
|
|
1225
|
-
const platform = process.platform === 'darwin' ? 'darwin'
|
|
1226
|
-
: process.platform === 'linux' ? 'linux'
|
|
1227
|
-
: null;
|
|
1228
|
-
if (!platform)
|
|
1229
|
-
return null; // unsupported (Windows, FreeBSD, etc.)
|
|
1230
|
-
const arch = process.arch === 'arm64' ? 'arm64'
|
|
1231
|
-
: process.arch === 'x64' ? 'amd64'
|
|
1232
|
-
: null;
|
|
1233
|
-
if (!arch)
|
|
1234
|
-
return null; // unsupported (s390x, ppc64, etc.)
|
|
1235
|
-
return `entire_${platform}_${arch}`;
|
|
1236
|
-
}
|
|
1237
|
-
function isEntireInstalled() {
|
|
1238
|
-
// Check the canonical install location first, then fall back to PATH
|
|
1239
|
-
const haystackBin = process.env.HOME
|
|
1240
|
-
? join(process.env.HOME, '.haystack', 'bin', 'entire')
|
|
1241
|
-
: null;
|
|
1242
|
-
const candidates = haystackBin ? [haystackBin, 'entire'] : ['entire'];
|
|
1243
|
-
for (const bin of candidates) {
|
|
1244
|
-
try {
|
|
1245
|
-
const version = execSync(`"${bin}" --version 2>&1`, { encoding: 'utf-8' }).trim();
|
|
1246
|
-
return {
|
|
1247
|
-
installed: true,
|
|
1248
|
-
isHaystackFork: version.includes('haystack'),
|
|
1249
|
-
version,
|
|
1250
|
-
};
|
|
1251
|
-
}
|
|
1252
|
-
catch {
|
|
1253
|
-
// Try next candidate
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
return { installed: false, isHaystackFork: false, version: '' };
|
|
1257
|
-
}
|
|
1258
|
-
async function installEntireBinary() {
|
|
1259
|
-
const binaryName = getEntireBinaryName();
|
|
1260
|
-
if (!binaryName) {
|
|
1261
|
-
console.log(chalk.yellow(` Entire CLI is not available for ${process.platform}/${process.arch}.`));
|
|
1262
|
-
console.log(chalk.dim(' Supported: macOS (Intel/Apple Silicon), Linux (x86_64/ARM64)'));
|
|
1263
|
-
return false;
|
|
1264
|
-
}
|
|
1265
|
-
const url = `${ENTIRE_RELEASE_BASE}/${binaryName}`;
|
|
1266
|
-
const installDir = process.env.HOME
|
|
1267
|
-
? join(process.env.HOME, '.haystack', 'bin')
|
|
1268
|
-
: '/usr/local/bin';
|
|
1269
|
-
try {
|
|
1270
|
-
mkdirSync(installDir, { recursive: true });
|
|
1271
|
-
const targetPath = join(installDir, 'entire');
|
|
1272
|
-
console.log(chalk.dim(` Downloading ${ENTIRE_VERSION}...`));
|
|
1273
|
-
execFileSync('curl', ['-fsSL', url, '-o', targetPath], { stdio: 'pipe' });
|
|
1274
|
-
execFileSync('chmod', ['+x', targetPath], { stdio: 'pipe' });
|
|
1275
|
-
// Verify using absolute path (don't rely on PATH)
|
|
1276
|
-
const version = execFileSync(targetPath, ['--version'], { encoding: 'utf-8' }).trim();
|
|
1277
|
-
if (!version.includes('haystack')) {
|
|
1278
|
-
console.log(chalk.red(` Unexpected version: ${version}`));
|
|
1279
|
-
return false;
|
|
1280
|
-
}
|
|
1281
|
-
console.log(chalk.green(` ✓ Installed: ${version.split('\n')[0]}`));
|
|
1282
|
-
// Check if install dir is on PATH
|
|
1283
|
-
const pathDirs = (process.env.PATH || '').split(':');
|
|
1284
|
-
if (!pathDirs.includes(installDir)) {
|
|
1285
|
-
console.log(chalk.yellow(`\n Note: ${installDir} is not on your PATH.`));
|
|
1286
|
-
console.log(chalk.dim(` Add to your shell profile: export PATH="${installDir}:$PATH"\n`));
|
|
1287
|
-
}
|
|
1288
|
-
return true;
|
|
1289
|
-
}
|
|
1290
|
-
catch (err) {
|
|
1291
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1292
|
-
console.log(chalk.red(` Failed to install Entire CLI: ${errorMsg}`));
|
|
1293
|
-
console.log(chalk.dim(` Manual install: curl -fsSL ${url} -o ~/.haystack/bin/entire && chmod +x ~/.haystack/bin/entire`));
|
|
1294
|
-
trackError('entire_install_failed', {
|
|
1295
|
-
error_message: errorMsg,
|
|
1296
|
-
error_stack: err instanceof Error ? err.stack : undefined,
|
|
1297
|
-
entire_version: ENTIRE_VERSION,
|
|
1298
|
-
install_dir: installDir,
|
|
1299
|
-
});
|
|
1300
|
-
return false;
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
async function stepInstallEntire(selectedRepos, token, bootstrapPRs) {
|
|
1304
|
-
console.log(chalk.bold('\n Step 7: Session tracking (Entire CLI)\n'));
|
|
1305
|
-
const status = isEntireInstalled();
|
|
1306
|
-
let binaryReady = status.installed && status.isHaystackFork;
|
|
1307
|
-
let userConsented = true; // true if user said yes or was already installed
|
|
1308
|
-
if (binaryReady) {
|
|
1309
|
-
console.log(chalk.dim(` Entire CLI already installed (${status.version.split('\n')[0]})`));
|
|
1310
|
-
}
|
|
1311
|
-
else if (status.installed) {
|
|
1312
|
-
console.log(chalk.dim(` Stock Entire CLI detected: ${status.version.split('\n')[0]}`));
|
|
1313
|
-
const upgrade = await ui.confirm({
|
|
1314
|
-
id: 'upgrade_entire',
|
|
1315
|
-
message: 'Upgrade to Haystack fork? (adds transcript optimization — saves GB of git storage)',
|
|
1316
|
-
default: true,
|
|
1317
|
-
});
|
|
1318
|
-
if (upgrade) {
|
|
1319
|
-
binaryReady = await installEntireBinary();
|
|
1320
|
-
}
|
|
1321
|
-
else {
|
|
1322
|
-
userConsented = false;
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
else {
|
|
1326
|
-
const install = await ui.confirm({
|
|
1327
|
-
id: 'install_entire',
|
|
1328
|
-
message: 'Install Entire CLI? (tracks AI coding sessions for analysis)',
|
|
1329
|
-
default: true,
|
|
1330
|
-
});
|
|
1331
|
-
if (install) {
|
|
1332
|
-
binaryReady = await installEntireBinary();
|
|
1333
|
-
}
|
|
1334
|
-
else {
|
|
1335
|
-
userConsented = false;
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
if (!userConsented) {
|
|
1339
|
-
console.log(chalk.dim(' Skipping session tracking.\n'));
|
|
1340
|
-
}
|
|
1341
|
-
// Onboarding always opens a PR, and .entire/settings.json rides along in that
|
|
1342
|
-
// PR's single commit (see openBootstrapConfigPR), so there's nothing to write
|
|
1343
|
-
// separately here — just report what landed.
|
|
1344
|
-
if (userConsented) {
|
|
1345
|
-
let bundled = 0;
|
|
1346
|
-
for (const repoFullName of selectedRepos) {
|
|
1347
|
-
const bootstrapPR = bootstrapPRs.get(repoFullName);
|
|
1348
|
-
if (bootstrapPR?.entireSettingsBundled) {
|
|
1349
|
-
console.log(chalk.green(` ✓ ${repoFullName}`) + chalk.dim(' (in PR)'));
|
|
1350
|
-
bundled++;
|
|
1351
|
-
}
|
|
1352
|
-
else {
|
|
1353
|
-
// The PR shipped the config files but not .entire/settings.json — its
|
|
1354
|
-
// optional read failed transiently. The warning was already shown when
|
|
1355
|
-
// the PR was opened.
|
|
1356
|
-
console.log(chalk.yellow(` ⚠ ${repoFullName}`) +
|
|
1357
|
-
chalk.dim(' — PR has config files only; re-run setup to add session tracking'));
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
if (bundled > 0) {
|
|
1361
|
-
console.log(chalk.green(`\n ✓ Session tracking configured on ${bundled} repo(s) (transcripts auto-compacted)\n`));
|
|
1362
|
-
trackSetupEvent('entire_configured', { configured: bundled, total: selectedRepos.length });
|
|
1363
|
-
}
|
|
1364
|
-
else {
|
|
1365
|
-
console.log(chalk.yellow('\n ⚠ Could not configure session tracking on any repos.\n'));
|
|
1366
|
-
trackError('entire_config_all_failed', { total: selectedRepos.length });
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1113
|
+
async function stepInstallHooks(selectedRepos) {
|
|
1369
1114
|
// Offer to install local git hooks if cwd is inside one of the selected repos
|
|
1370
1115
|
const gitRoot = findGitRoot();
|
|
1371
1116
|
if (gitRoot) {
|
|
@@ -1378,12 +1123,13 @@ async function stepInstallEntire(selectedRepos, token, bootstrapPRs) {
|
|
|
1378
1123
|
try {
|
|
1379
1124
|
const installHooks = await ui.confirm({
|
|
1380
1125
|
id: 'install_hooks',
|
|
1381
|
-
message: `Install
|
|
1126
|
+
message: `Install local hooks for ${repoName}? (agent checks, session status, verification precompute)`,
|
|
1382
1127
|
default: true,
|
|
1383
1128
|
});
|
|
1384
1129
|
if (installHooks) {
|
|
1385
1130
|
console.log('');
|
|
1386
|
-
await hooksInstall({
|
|
1131
|
+
await hooksInstall({ force: true });
|
|
1132
|
+
await installSessionHooks({ cli: 'claude' });
|
|
1387
1133
|
}
|
|
1388
1134
|
}
|
|
1389
1135
|
catch (err) {
|
package/dist/commands/submit.js
CHANGED
|
@@ -135,7 +135,6 @@ function checkerDisplayName(checker) {
|
|
|
135
135
|
switch (checker) {
|
|
136
136
|
case 'code-review': return 'Code Review';
|
|
137
137
|
case 'rules-validator': return 'Rules Validator';
|
|
138
|
-
case 'intent-drift': return 'Instruction Drift';
|
|
139
138
|
default: return checker;
|
|
140
139
|
}
|
|
141
140
|
}
|
|
@@ -161,7 +160,7 @@ async function resolveTriageOptions(options) {
|
|
|
161
160
|
const triageConfig = config?.triage ?? {};
|
|
162
161
|
const maxTurns = { ...(triageConfig.maxTurns ?? {}) };
|
|
163
162
|
if (typeof options.maxTurns === 'number' && options.maxTurns > 0) {
|
|
164
|
-
for (const name of ['code-review', 'rules-validator'
|
|
163
|
+
for (const name of ['code-review', 'rules-validator']) {
|
|
165
164
|
maxTurns[name] = options.maxTurns;
|
|
166
165
|
}
|
|
167
166
|
}
|
|
@@ -16,6 +16,18 @@ export interface HostedStatusOptions {
|
|
|
16
16
|
timeout?: string;
|
|
17
17
|
json?: boolean;
|
|
18
18
|
}
|
|
19
|
+
export interface HostedRunnerResult {
|
|
20
|
+
verdict: string;
|
|
21
|
+
summary: string;
|
|
22
|
+
casesTotal: number;
|
|
23
|
+
casesFailed: number;
|
|
24
|
+
regressions: Array<{
|
|
25
|
+
caseId: string;
|
|
26
|
+
title: string;
|
|
27
|
+
}>;
|
|
28
|
+
outputLocation: string;
|
|
29
|
+
wallSeconds: number;
|
|
30
|
+
}
|
|
19
31
|
export interface HostedRunState {
|
|
20
32
|
runId: string;
|
|
21
33
|
status: string;
|
|
@@ -25,6 +37,7 @@ export interface HostedRunState {
|
|
|
25
37
|
createdAt?: string;
|
|
26
38
|
completedAt?: string;
|
|
27
39
|
output?: Record<string, unknown>;
|
|
40
|
+
result?: HostedRunnerResult;
|
|
28
41
|
error?: string;
|
|
29
42
|
}
|
|
30
43
|
export interface HostedPollOptions {
|
|
@@ -73,10 +73,22 @@ export function parseHostedRunState(value, expectedRunId) {
|
|
|
73
73
|
const output = outputValue === undefined
|
|
74
74
|
? undefined
|
|
75
75
|
: requireRecord(outputValue, 'Hosted verifier output');
|
|
76
|
+
const resultValue = response.result;
|
|
77
|
+
const result = resultValue === undefined
|
|
78
|
+
? undefined
|
|
79
|
+
: parseHostedRunnerResult(resultValue);
|
|
76
80
|
const createdAt = optionalTimestamp(response, 'createdAt');
|
|
77
81
|
const completedAt = optionalTimestamp(response, 'completedAt');
|
|
78
82
|
if (status === 'complete') {
|
|
79
|
-
|
|
83
|
+
// Two terminal shapes exist: the Workflow receipt envelope (`output`)
|
|
84
|
+
// and the runner-queue result envelope (`result`). Exactly one must
|
|
85
|
+
// validate.
|
|
86
|
+
if (result) {
|
|
87
|
+
if (!repository || !baseCommit || !headCommit || !createdAt) {
|
|
88
|
+
throw new Error('Hosted verifier returned a complete run without a matching terminal receipt.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (!repository
|
|
80
92
|
|| !baseCommit
|
|
81
93
|
|| !headCommit
|
|
82
94
|
|| !createdAt
|
|
@@ -100,9 +112,35 @@ export function parseHostedRunState(value, expectedRunId) {
|
|
|
100
112
|
createdAt,
|
|
101
113
|
completedAt,
|
|
102
114
|
output,
|
|
115
|
+
result,
|
|
103
116
|
error: optionalString(response, 'error'),
|
|
104
117
|
};
|
|
105
118
|
}
|
|
119
|
+
function parseHostedRunnerResult(value) {
|
|
120
|
+
const record = requireRecord(value, 'Hosted verifier runner result');
|
|
121
|
+
const regressionsValue = record.regressions;
|
|
122
|
+
if (typeof record.verdict !== 'string'
|
|
123
|
+
|| typeof record.summary !== 'string'
|
|
124
|
+
|| !Number.isInteger(record.casesTotal)
|
|
125
|
+
|| !Number.isInteger(record.casesFailed)
|
|
126
|
+
|| typeof record.outputLocation !== 'string'
|
|
127
|
+
|| typeof record.wallSeconds !== 'number'
|
|
128
|
+
|| !Array.isArray(regressionsValue)
|
|
129
|
+
|| !regressionsValue.every(item => !!item && typeof item === 'object' && !Array.isArray(item)
|
|
130
|
+
&& typeof item.caseId === 'string'
|
|
131
|
+
&& typeof item.title === 'string')) {
|
|
132
|
+
throw new Error('Hosted verifier runner result is malformed.');
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
verdict: record.verdict,
|
|
136
|
+
summary: record.summary,
|
|
137
|
+
casesTotal: record.casesTotal,
|
|
138
|
+
casesFailed: record.casesFailed,
|
|
139
|
+
regressions: regressionsValue,
|
|
140
|
+
outputLocation: record.outputLocation,
|
|
141
|
+
wallSeconds: record.wallSeconds,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
106
144
|
export function parseHostedRepository(value) {
|
|
107
145
|
const parts = value.split('/');
|
|
108
146
|
if (parts.length !== 2
|
|
@@ -242,6 +280,7 @@ export function buildHostedJson(operation, state, extras = {}) {
|
|
|
242
280
|
...(extras.nextCommand ? { next_command: extras.nextCommand } : {}),
|
|
243
281
|
...(extras.timedOut !== undefined ? { timed_out: extras.timedOut } : {}),
|
|
244
282
|
...(state.output ? { output: state.output } : {}),
|
|
283
|
+
...(state.result ? { result: state.result } : {}),
|
|
245
284
|
...(state.error ? { error: state.error } : {}),
|
|
246
285
|
});
|
|
247
286
|
}
|
|
@@ -273,6 +312,15 @@ function printHostedState(operation, state, options) {
|
|
|
273
312
|
if (state.baseCommit && state.headCommit) {
|
|
274
313
|
console.log(` ${chalk.dim('Commits:')} ${state.baseCommit.slice(0, 12)} → ${state.headCommit.slice(0, 12)}`);
|
|
275
314
|
}
|
|
315
|
+
if (state.result) {
|
|
316
|
+
const passed = state.result.casesTotal - state.result.casesFailed;
|
|
317
|
+
console.log(` ${chalk.dim('Verdict:')} ${state.result.verdict} `
|
|
318
|
+
+ `(${passed}/${state.result.casesTotal} cases clean, ${Math.round(state.result.wallSeconds)}s)`);
|
|
319
|
+
for (const regression of state.result.regressions) {
|
|
320
|
+
console.log(` ${chalk.red('✗')} ${regression.caseId}: ${regression.title}`);
|
|
321
|
+
}
|
|
322
|
+
console.log(` ${chalk.dim('Artifacts:')} ${state.result.outputLocation}`);
|
|
323
|
+
}
|
|
276
324
|
if (state.error)
|
|
277
325
|
console.log(` ${chalk.dim('Error:')} ${state.error}`);
|
|
278
326
|
if (options.timedOut) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type PrecomputeOutcome = 'enqueued' | 'cached' | 'existing' | 'skipped_no_auth' | 'error';
|
|
2
|
+
export interface VerifyPrecomputeOptions {
|
|
3
|
+
hook?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface FrontHalfEnqueueRequest {
|
|
6
|
+
owner: string;
|
|
7
|
+
repository: string;
|
|
8
|
+
baseCommit: string;
|
|
9
|
+
workCommit: string;
|
|
10
|
+
cacheKey: string;
|
|
11
|
+
patchGzBase64: string;
|
|
12
|
+
patchSha256: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PrecomputeDerivation {
|
|
15
|
+
treeSha: string;
|
|
16
|
+
request: FrontHalfEnqueueRequest;
|
|
17
|
+
}
|
|
18
|
+
export declare function derivePrecomputeRequest(gitRoot: string, owner: string, repository: string, deadline?: number): Promise<PrecomputeDerivation>;
|
|
19
|
+
export declare function verifyPrecomputeCommand(options: VerifyPrecomputeOptions): Promise<void>;
|