@haystackeditor/cli 0.15.27 → 0.15.29

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 (55) 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 +668 -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-mcp.js +12 -2
  22. package/dist/commands/verify-hosted-reproducibility.d.ts +1 -0
  23. package/dist/commands/verify-hosted-reproducibility.js +4 -1
  24. package/dist/commands/verify-hosted.d.ts +20 -0
  25. package/dist/commands/verify-hosted.js +119 -11
  26. package/dist/commands/verify-precompute.d.ts +19 -0
  27. package/dist/commands/verify-precompute.js +328 -0
  28. package/dist/index.js +45 -44
  29. package/dist/schema.d.ts +1 -2
  30. package/dist/schema.js +1 -2
  31. package/dist/triage/prompts.d.ts +0 -7
  32. package/dist/triage/prompts.js +0 -145
  33. package/dist/triage/runner.js +3 -22
  34. package/dist/triage/types.d.ts +1 -8
  35. package/dist/types.d.ts +8 -8
  36. package/dist/types.js +1 -1
  37. package/dist/utils/design-verifier-api.d.ts +71 -0
  38. package/dist/utils/design-verifier-api.js +96 -0
  39. package/dist/utils/design-verifier-result.d.ts +7 -2
  40. package/dist/utils/design-verifier-result.js +67 -21
  41. package/dist/utils/git.d.ts +5 -1
  42. package/dist/utils/git.js +19 -9
  43. package/dist/utils/github-api.js +11 -14
  44. package/dist/utils/hooks.d.ts +1 -15
  45. package/dist/utils/hooks.js +1 -127
  46. package/dist/utils/telemetry.d.ts +2 -0
  47. package/dist/utils/telemetry.js +81 -8
  48. package/package.json +2 -1
  49. package/schemas/cloud-verifier.v1.json +69 -3
  50. package/schemas/{pr.v2.json → pr.v3.json} +3 -12
  51. package/dist/commands/traces.d.ts +0 -48
  52. package/dist/commands/traces.js +0 -92
  53. package/dist/triage/traces.d.ts +0 -20
  54. package/dist/triage/traces.js +0 -311
  55. package/schemas/traces.v1.json +0 -53
@@ -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. Install session tracking (Entire CLI + .entire/settings.json + local hooks)
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 { mkdirSync } from 'fs';
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, trackSetupEvent } from '../utils/telemetry.js';
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, session tracking,
45
- // hooks). Use --skip-entire / --no-auto-merge to opt out. Only fill ids not
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 (`configFiles` + `.entire/settings.json`) via the Git Data API, CREATE
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` enables the merge queue, `.entire/settings.json` enables the
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, entireSettingsBundled };
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. Step 7 ensures .entire/settings.json rode along in
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: Install Entire CLI (session tracking). bootstrapPRs tells it which
1176
- // repos got a Configure Haystack PR (all of them) so it can report that
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 Entire CLI for session tracking
1111
+ // Step 7: Install Haystack git hooks
1221
1112
  // =============================================================================
1222
- const ENTIRE_VERSION = 'v0.5.3-haystack.2';
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 git hooks locally for ${repoName}? (agent detection, truncation checking, LLM rules)`,
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({ skipEntire: true, force: true });
1131
+ await hooksInstall({ force: true });
1132
+ await installSessionHooks({ cli: 'claude' });
1387
1133
  }
1388
1134
  }
1389
1135
  catch (err) {
@@ -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', 'intent-drift']) {
163
+ for (const name of ['code-review', 'rules-validator']) {
165
164
  maxTurns[name] = options.maxTurns;
166
165
  }
167
166
  }
@@ -7,7 +7,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
7
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
9
9
  import { resolveAuthContext } from '../utils/auth.js';
10
- import { buildHostedJson, getHostedVerification, hostedResumeCommand, parseHostedRepository, startHostedVerification, waitForHostedVerification, } from './verify-hosted.js';
10
+ import { buildHostedJson, getHostedVerification, hostedResumeCommand, parseHostedRepository, readHostedIntentFile, startHostedVerification, waitForHostedVerification, } from './verify-hosted.js';
11
11
  const TERMINAL_STATUSES = new Set([
12
12
  'complete',
13
13
  'errored',
@@ -48,6 +48,11 @@ export const HOSTED_VERIFY_TOOLS = [
48
48
  pattern: '^[0-9a-f]{40}$',
49
49
  description: 'Exact 40-character lowercase head commit SHA.',
50
50
  },
51
+ intent_file: {
52
+ type: 'string',
53
+ minLength: 1,
54
+ description: 'Path to agent-supplied JSON containing exactly problem, goal, and intended_outcomes (a non-empty array of non-empty strings).',
55
+ },
51
56
  idempotency_key: {
52
57
  type: 'string',
53
58
  pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$',
@@ -59,7 +64,7 @@ export const HOSTED_VERIFY_TOOLS = [
59
64
  },
60
65
  account: accountInput,
61
66
  },
62
- required: ['repository', 'base_commit', 'head_commit'],
67
+ required: ['repository', 'base_commit', 'head_commit', 'intent_file'],
63
68
  },
64
69
  },
65
70
  {
@@ -124,6 +129,7 @@ function assertToolInputFields(name, input) {
124
129
  'base_commit',
125
130
  'head_commit',
126
131
  'idempotency_key',
132
+ 'intent_file',
127
133
  'replay_plan_from_run_id',
128
134
  'repository',
129
135
  ]);
@@ -151,6 +157,7 @@ export async function executeHostedVerifyTool(name, input, context) {
151
157
  const started = await startHostedVerification(repository, {
152
158
  base: requireInputString(input, 'base_commit'),
153
159
  head: requireInputString(input, 'head_commit'),
160
+ intentFile: requireInputString(input, 'intent_file'),
154
161
  idempotencyKey: optionalInputString(input, 'idempotency_key'),
155
162
  replayPlanFrom: optionalInputString(input, 'replay_plan_from_run_id'),
156
163
  wait: false,
@@ -217,6 +224,9 @@ export async function runHostedVerifyMcpServer(options) {
217
224
  // Reject undeclared fields before account resolution so callers cannot
218
225
  // use an invalid request to trigger credential or repository lookup.
219
226
  assertToolInputFields(name, input);
227
+ if (name === 'verify_start') {
228
+ readHostedIntentFile(requireInputString(input, 'intent_file'));
229
+ }
220
230
  const context = await resolveToolContext(name, input, options);
221
231
  const result = await executeHostedVerifyTool(name, input, context);
222
232
  return {
@@ -2,6 +2,7 @@ import { type HostedRunState } from './verify-hosted.js';
2
2
  export interface HostedReproducibilityOptions {
3
3
  base: string;
4
4
  head: string;
5
+ intentFile: string;
5
6
  runs?: string;
6
7
  seriesKey?: string;
7
8
  freshPlans?: boolean;
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from 'node:crypto';
2
2
  import chalk from 'chalk';
3
3
  import { withSchema } from '../schema.js';
4
4
  import { resolveAuthContext } from '../utils/auth.js';
5
- import { hostedPollingOptions, parseHostedRepository, startHostedVerification, waitForHostedVerification, } from './verify-hosted.js';
5
+ import { hostedPollingOptions, parseHostedRepository, readHostedIntentFile, startHostedVerification, waitForHostedVerification, } from './verify-hosted.js';
6
6
  const CELL_ID = /^cell_[0-9a-f]{32}$/;
7
7
  const CAPABILITY_ID = /^repository_driver_[0-9a-f]{64}$/;
8
8
  const COMPARISON_ID = /^driver_comparison_[0-9a-f]{64}$/;
@@ -502,6 +502,8 @@ function renderHostedReproducibility(report) {
502
502
  }
503
503
  export async function verifyHostedReproducibilityCommand(repositoryArg, options) {
504
504
  const repository = parseHostedRepository(repositoryArg);
505
+ // A reproducibility series reuses one explicit agent intent for every run.
506
+ readHostedIntentFile(options.intentFile);
505
507
  const requestedRuns = boundedInteger(options.runs, DEFAULT_RUNS, '--runs', 2, MAX_RUNS);
506
508
  const seriesKey = requireSeriesKey(options.seriesKey);
507
509
  const mode = options.freshPlans
@@ -521,6 +523,7 @@ export async function verifyHostedReproducibilityCommand(repositoryArg, options)
521
523
  const started = await startHostedVerification(repositoryArg, {
522
524
  base: options.base,
523
525
  head: options.head,
526
+ intentFile: options.intentFile,
524
527
  idempotencyKey: hostedReproducibilityRunKey(seriesKey, repository.fullName, options.base, options.head, index + 1, mode),
525
528
  replayPlanFrom: options.freshPlans || index === 0
526
529
  ? undefined
@@ -1,6 +1,12 @@
1
+ export interface HostedIntent {
2
+ problem: string;
3
+ goal: string;
4
+ intended_outcomes: string[];
5
+ }
1
6
  export interface HostedStartOptions {
2
7
  base: string;
3
8
  head: string;
9
+ intentFile: string;
4
10
  idempotencyKey?: string;
5
11
  replayPlanFrom?: string;
6
12
  account?: string;
@@ -16,6 +22,18 @@ export interface HostedStatusOptions {
16
22
  timeout?: string;
17
23
  json?: boolean;
18
24
  }
25
+ export interface HostedRunnerResult {
26
+ verdict: string;
27
+ summary: string;
28
+ casesTotal: number;
29
+ casesFailed: number;
30
+ regressions: Array<{
31
+ caseId: string;
32
+ title: string;
33
+ }>;
34
+ outputLocation: string;
35
+ wallSeconds: number;
36
+ }
19
37
  export interface HostedRunState {
20
38
  runId: string;
21
39
  status: string;
@@ -25,6 +43,7 @@ export interface HostedRunState {
25
43
  createdAt?: string;
26
44
  completedAt?: string;
27
45
  output?: Record<string, unknown>;
46
+ result?: HostedRunnerResult;
28
47
  error?: string;
29
48
  }
30
49
  export interface HostedPollOptions {
@@ -42,6 +61,7 @@ export declare function parseHostedRepository(value: string): {
42
61
  repository: string;
43
62
  fullName: string;
44
63
  };
64
+ export declare function readHostedIntentFile(intentFile: string | undefined): HostedIntent;
45
65
  export declare function defaultHostedIdempotencyKey(repository: string, baseCommit: string, headCommit: string, replayPlanFrom?: string): string;
46
66
  export declare function hostedPollingOptions(interval: string | undefined, timeout: string | undefined, onStatus?: (state: HostedRunState) => void): HostedPollOptions;
47
67
  export declare function startHostedVerification(repositoryArg: string, options: HostedStartOptions, token: string): Promise<{