@haystackeditor/cli 0.15.19 → 0.15.21
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 +100 -1
- package/dist/assets/skills/map-your-system.md +9 -4
- package/dist/commands/ask.js +2 -2
- package/dist/commands/config.d.ts +9 -0
- package/dist/commands/config.js +46 -14
- package/dist/commands/dismiss.d.ts +15 -3
- package/dist/commands/dismiss.js +66 -116
- package/dist/commands/mcp.js +96 -1
- package/dist/commands/policy.js +31 -12
- package/dist/commands/pr-status.d.ts +125 -2
- package/dist/commands/pr-status.js +88 -73
- package/dist/commands/pr.d.ts +1 -7
- package/dist/commands/pr.js +10 -21
- package/dist/commands/request-review.d.ts +12 -1
- package/dist/commands/request-review.js +42 -65
- package/dist/commands/review.d.ts +1 -0
- package/dist/commands/review.js +58 -60
- package/dist/commands/setup.js +4 -1
- package/dist/commands/submit.d.ts +3 -0
- package/dist/commands/submit.js +178 -38
- package/dist/commands/system-map.d.ts +42 -0
- package/dist/commands/system-map.js +228 -0
- package/dist/commands/tokens.js +3 -2
- package/dist/commands/traces.js +2 -2
- package/dist/commands/triage.d.ts +1 -6
- package/dist/commands/triage.js +51 -55
- package/dist/commands/verify-core.d.ts +424 -0
- package/dist/commands/verify-core.js +753 -0
- package/dist/commands/verify-mcp.d.ts +8 -0
- package/dist/commands/verify-mcp.js +458 -0
- package/dist/commands/verify-ops.d.ts +158 -0
- package/dist/commands/verify-ops.js +1148 -0
- package/dist/commands/verify-reseal.d.ts +9 -0
- package/dist/commands/verify-reseal.js +148 -0
- package/dist/commands/verify-sandboxes.d.ts +95 -0
- package/dist/commands/verify-sandboxes.js +352 -0
- package/dist/commands/verify.d.ts +11 -0
- package/dist/commands/verify.js +97 -0
- package/dist/index.js +343 -20
- package/dist/schema.d.ts +4 -0
- package/dist/schema.js +4 -0
- package/dist/states.d.ts +29 -0
- package/dist/states.js +46 -0
- package/dist/triage/runner.d.ts +7 -0
- package/dist/triage/runner.js +2 -2
- package/dist/types.d.ts +54 -0
- package/dist/types.js +15 -0
- package/dist/utils/action-output.d.ts +24 -0
- package/dist/utils/action-output.js +26 -0
- package/dist/utils/analysis-api.d.ts +11 -0
- package/dist/utils/analysis-api.js +29 -3
- package/dist/utils/auth.js +7 -0
- package/dist/utils/git.d.ts +4 -1
- package/dist/utils/git.js +17 -7
- package/dist/utils/haystack-api.d.ts +15 -0
- package/dist/utils/haystack-api.js +58 -7
- package/dist/utils/pr-ref.d.ts +27 -0
- package/dist/utils/pr-ref.js +56 -0
- package/package.json +2 -2
- package/schemas/action.v1.json +22 -0
- package/schemas/error.v1.json +14 -0
- package/schemas/pr-status.v1.json +61 -0
- package/schemas/pr.v1.json +80 -17
- package/schemas/setup.v1.json +66 -12
- package/schemas/submit.v1.json +88 -0
- package/schemas/triage.v1.json +71 -18
package/dist/commands/submit.js
CHANGED
|
@@ -19,6 +19,8 @@ import { checkAnalysisReady, fetchAnalysisResults } from '../utils/analysis-api.
|
|
|
19
19
|
import { savePendingSubmit, updatePendingSubmit } from '../utils/pending-state.js';
|
|
20
20
|
import { ensureHaystackLabels, addLabelsToIssue, } from '../utils/github-api.js';
|
|
21
21
|
import { listStoredAccounts, resolveAuthContext, setRepoDefaultAccount, } from '../utils/auth.js';
|
|
22
|
+
import { HaystackApiError } from '../utils/haystack-api.js';
|
|
23
|
+
import { withSchema } from '../schema.js';
|
|
22
24
|
// ============================================================================
|
|
23
25
|
// Helpers
|
|
24
26
|
// ============================================================================
|
|
@@ -248,6 +250,26 @@ function printTriageSummary(result) {
|
|
|
248
250
|
// Main command
|
|
249
251
|
// ============================================================================
|
|
250
252
|
export async function submitCommand(options) {
|
|
253
|
+
// --json contract: stdout carries exactly one JSON document (the submit
|
|
254
|
+
// payload or an error envelope); every progress/status line goes to stderr.
|
|
255
|
+
// Redirecting console.log wholesale keeps the existing progress output
|
|
256
|
+
// intact for humans watching stderr. (console.error/warn already hit stderr.)
|
|
257
|
+
if (options.json) {
|
|
258
|
+
console.log = (...args) => {
|
|
259
|
+
process.stderr.write(args.map((a) => (typeof a === 'string' ? a : String(a))).join(' ') + '\n');
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/** Print a classified failure (JSON envelope on stdout when --json) and exit 1. */
|
|
263
|
+
const fail = (message, extra) => {
|
|
264
|
+
if (options.json) {
|
|
265
|
+
process.stdout.write(JSON.stringify(withSchema('submit', { status: 'error', error: message, ...extra }), null, 2) + '\n');
|
|
266
|
+
console.error(chalk.red(`\n${message}\n`));
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
console.error(chalk.red(`\n${message}\n`));
|
|
270
|
+
}
|
|
271
|
+
process.exit(1);
|
|
272
|
+
};
|
|
251
273
|
console.log(chalk.bold.cyan('\nHaystack Submit\n'));
|
|
252
274
|
// -------------------------------------------------------------------------
|
|
253
275
|
// Step 1: Validate environment
|
|
@@ -255,14 +277,13 @@ export async function submitCommand(options) {
|
|
|
255
277
|
console.log(chalk.dim('Checking environment...'));
|
|
256
278
|
// Validate mutually exclusive options
|
|
257
279
|
if (options.body && options.bodyFile) {
|
|
258
|
-
|
|
259
|
-
process.exit(1);
|
|
280
|
+
fail('Cannot use both --body and --body-file. Pick one.');
|
|
260
281
|
}
|
|
261
282
|
// Check git repo
|
|
262
283
|
const gitRoot = findGitRoot();
|
|
263
284
|
if (!gitRoot) {
|
|
264
|
-
|
|
265
|
-
|
|
285
|
+
fail('Not a git repository. Run `haystack submit` from inside a repository checkout.');
|
|
286
|
+
return; // unreachable — satisfies TS narrowing
|
|
266
287
|
}
|
|
267
288
|
// Get remote info
|
|
268
289
|
let remoteInfo;
|
|
@@ -271,8 +292,8 @@ export async function submitCommand(options) {
|
|
|
271
292
|
console.log(chalk.green(`✓ Repository: ${remoteInfo.owner}/${remoteInfo.repo}`));
|
|
272
293
|
}
|
|
273
294
|
catch (err) {
|
|
274
|
-
|
|
275
|
-
|
|
295
|
+
fail(err.message);
|
|
296
|
+
return; // unreachable — satisfies TS narrowing
|
|
276
297
|
}
|
|
277
298
|
let githubToken;
|
|
278
299
|
let activeGitHubLogin;
|
|
@@ -294,8 +315,7 @@ export async function submitCommand(options) {
|
|
|
294
315
|
}
|
|
295
316
|
}
|
|
296
317
|
catch (err) {
|
|
297
|
-
|
|
298
|
-
process.exit(1);
|
|
318
|
+
fail(err.message);
|
|
299
319
|
}
|
|
300
320
|
// Check for uncommitted changes
|
|
301
321
|
if (hasUncommittedChanges()) {
|
|
@@ -305,10 +325,14 @@ export async function submitCommand(options) {
|
|
|
305
325
|
const currentBranch = getCurrentBranch();
|
|
306
326
|
const baseBranch = options.base || getDefaultBranch();
|
|
307
327
|
console.log(chalk.dim(` Current branch: ${currentBranch}`));
|
|
308
|
-
console.log(chalk.dim(` Base branch: ${baseBranch}`));
|
|
328
|
+
console.log(chalk.dim(` Base branch: ${baseBranch}${options.base ? '' : ' (detected from origin)'}`));
|
|
309
329
|
// -------------------------------------------------------------------------
|
|
310
330
|
// Step 2: Pre-PR Triage
|
|
311
331
|
// -------------------------------------------------------------------------
|
|
332
|
+
// Recorded for the --json payload: what triage concluded before the PR
|
|
333
|
+
// was created. 'unavailable' = infrastructure gave zero signal (never
|
|
334
|
+
// reported as 'passed').
|
|
335
|
+
let triageStatus = 'skipped';
|
|
312
336
|
if (!options.force) {
|
|
313
337
|
console.log(chalk.dim('\nRunning pre-PR triage checks...'));
|
|
314
338
|
try {
|
|
@@ -330,10 +354,22 @@ export async function submitCommand(options) {
|
|
|
330
354
|
// We already warned above and continue.
|
|
331
355
|
}
|
|
332
356
|
else {
|
|
333
|
-
|
|
334
|
-
|
|
357
|
+
fail('Triage failed. Fix the issues above or re-run with --force.', {
|
|
358
|
+
triage: {
|
|
359
|
+
status: 'failed',
|
|
360
|
+
issues: triageResult.results.flatMap((r) => r.issues.map((issue) => ({
|
|
361
|
+
checker: r.checker,
|
|
362
|
+
severity: issue.severity,
|
|
363
|
+
file: issue.file,
|
|
364
|
+
line: issue.line ?? null,
|
|
365
|
+
message: issue.message,
|
|
366
|
+
rule: issue.rule ?? null,
|
|
367
|
+
}))),
|
|
368
|
+
},
|
|
369
|
+
});
|
|
335
370
|
}
|
|
336
371
|
}
|
|
372
|
+
triageStatus = triageInfraFailure ? 'unavailable' : 'passed';
|
|
337
373
|
}
|
|
338
374
|
catch (err) {
|
|
339
375
|
// Triage failure should not block submission
|
|
@@ -346,6 +382,7 @@ export async function submitCommand(options) {
|
|
|
346
382
|
else {
|
|
347
383
|
console.log(chalk.yellow(`\n⚠ Triage skipped: ${err.message}`));
|
|
348
384
|
}
|
|
385
|
+
triageStatus = 'unavailable';
|
|
349
386
|
}
|
|
350
387
|
}
|
|
351
388
|
else {
|
|
@@ -355,24 +392,22 @@ export async function submitCommand(options) {
|
|
|
355
392
|
// Step 3: Check if on main/master
|
|
356
393
|
// -------------------------------------------------------------------------
|
|
357
394
|
if (currentBranch === 'main' || currentBranch === 'master') {
|
|
358
|
-
|
|
359
|
-
process.exit(1);
|
|
395
|
+
fail(`Refusing to submit from ${currentBranch}. Create a feature branch first (e.g. \`git checkout -b my-change\`), then re-run \`haystack submit\`.`);
|
|
360
396
|
}
|
|
361
397
|
// -------------------------------------------------------------------------
|
|
362
398
|
// Step 4: Push to remote
|
|
363
399
|
// -------------------------------------------------------------------------
|
|
364
400
|
console.log(chalk.dim('\nPushing to origin...'));
|
|
365
401
|
if (!githubToken) {
|
|
366
|
-
|
|
367
|
-
|
|
402
|
+
fail('No Haystack account token was resolved before push (internal error). Run `haystack login`, then retry.');
|
|
403
|
+
return; // unreachable — satisfies TS narrowing
|
|
368
404
|
}
|
|
369
405
|
try {
|
|
370
406
|
pushBranch('origin', currentBranch, { token: githubToken });
|
|
371
407
|
console.log(chalk.green('✓ Branch pushed'));
|
|
372
408
|
}
|
|
373
409
|
catch (err) {
|
|
374
|
-
|
|
375
|
-
process.exit(1);
|
|
410
|
+
fail(err.message);
|
|
376
411
|
}
|
|
377
412
|
// -------------------------------------------------------------------------
|
|
378
413
|
// Step 5: Create or find PR
|
|
@@ -382,30 +417,53 @@ export async function submitCommand(options) {
|
|
|
382
417
|
const existingPR = await findExistingPR(remoteInfo.owner, remoteInfo.repo, currentBranch, githubToken);
|
|
383
418
|
let prNumber;
|
|
384
419
|
let prUrl;
|
|
420
|
+
let prTitle;
|
|
421
|
+
// Where the posted title/body came from — echoed to the user and recorded
|
|
422
|
+
// in the --json payload so the derived values are never silent.
|
|
423
|
+
let titleSource;
|
|
424
|
+
let bodySource;
|
|
425
|
+
let prBody = null;
|
|
385
426
|
if (existingPR) {
|
|
386
427
|
console.log(chalk.yellow(`PR already exists: #${existingPR.number}`));
|
|
387
428
|
prNumber = existingPR.number;
|
|
388
429
|
prUrl = existingPR.html_url;
|
|
430
|
+
prTitle = existingPR.title;
|
|
431
|
+
titleSource = 'existing-pr';
|
|
432
|
+
bodySource = 'existing-pr';
|
|
389
433
|
}
|
|
390
434
|
else {
|
|
391
435
|
// Generate PR title and body
|
|
392
|
-
|
|
393
|
-
|
|
436
|
+
try {
|
|
437
|
+
prTitle = options.title || getLastCommitMessage();
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
fail(err.message);
|
|
441
|
+
return; // unreachable — satisfies TS narrowing
|
|
442
|
+
}
|
|
443
|
+
titleSource = options.title ? 'flag' : 'last-commit';
|
|
444
|
+
let bodyContent = '';
|
|
394
445
|
if (options.bodyFile) {
|
|
446
|
+
bodySource = options.bodyFile === '-' ? 'stdin' : 'body-file';
|
|
395
447
|
try {
|
|
396
448
|
bodyContent = options.bodyFile === '-'
|
|
397
449
|
? fs.readFileSync(0, 'utf-8')
|
|
398
450
|
: fs.readFileSync(options.bodyFile, 'utf-8');
|
|
399
451
|
}
|
|
400
452
|
catch (err) {
|
|
401
|
-
|
|
402
|
-
process.exit(1);
|
|
453
|
+
fail(`Failed to read body file: ${err.message}`);
|
|
403
454
|
}
|
|
404
455
|
}
|
|
456
|
+
else if (options.body) {
|
|
457
|
+
bodySource = 'flag';
|
|
458
|
+
bodyContent = options.body;
|
|
459
|
+
}
|
|
405
460
|
else {
|
|
406
|
-
|
|
461
|
+
bodySource = 'commit-messages';
|
|
462
|
+
bodyContent = getCommitMessagesSinceBase(baseBranch);
|
|
407
463
|
}
|
|
408
|
-
|
|
464
|
+
prBody = buildPRBody(bodyContent);
|
|
465
|
+
console.log(chalk.dim(` Title: ${prTitle} (from ${titleSource === 'flag' ? '--title' : 'last commit message'})`));
|
|
466
|
+
console.log(chalk.dim(` Body: from ${bodySource === 'commit-messages' ? 'commit messages since base' : bodySource}`));
|
|
409
467
|
try {
|
|
410
468
|
const pr = await createPullRequest({
|
|
411
469
|
owner: remoteInfo.owner,
|
|
@@ -422,32 +480,46 @@ export async function submitCommand(options) {
|
|
|
422
480
|
console.log(chalk.green(`✓ Pull request created: #${prNumber}`));
|
|
423
481
|
}
|
|
424
482
|
catch (err) {
|
|
425
|
-
|
|
426
|
-
|
|
483
|
+
fail(formatSubmitPermissionError(err, activeGitHubLogin, remoteInfo.owner, remoteInfo.repo));
|
|
484
|
+
return; // unreachable — satisfies TS narrowing
|
|
427
485
|
}
|
|
428
486
|
}
|
|
429
487
|
// -------------------------------------------------------------------------
|
|
430
488
|
// Step 6: Apply auto-merge label if enabled
|
|
431
489
|
// -------------------------------------------------------------------------
|
|
490
|
+
// Applied-state tracking: the --json payload must report what actually
|
|
491
|
+
// happened on GitHub, not what was requested — label application can fail
|
|
492
|
+
// (403/422) and only warn.
|
|
493
|
+
let autoMergeApplied = false;
|
|
494
|
+
let autoFixApplied = false;
|
|
495
|
+
let reviewLabelApplied = false;
|
|
496
|
+
let reviewerRequestedOk = null;
|
|
497
|
+
// Announce the resolved value AND its source in both directions — a silent
|
|
498
|
+
// config-driven default is invisible to the caller otherwise.
|
|
499
|
+
const autoMergeSourceLabel = options.autoMergeSource === 'flag' ? '--auto-merge flag' : '.haystack.json';
|
|
432
500
|
if (options.autoMerge) {
|
|
433
501
|
try {
|
|
434
502
|
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-merge'], githubToken);
|
|
435
503
|
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-merge'], githubToken);
|
|
504
|
+
autoMergeApplied = true;
|
|
436
505
|
// With --review, the PR still enters the auto-merge queue but the
|
|
437
506
|
// merge queue's needs-review gate holds it until a human approves
|
|
438
507
|
// (removes haystack:needs-review or marks it reviewed). Don't print a
|
|
439
508
|
// bare "Auto-merge enabled" that reads as if it'll merge unreviewed.
|
|
440
509
|
if (options.review) {
|
|
441
|
-
console.log(chalk.green(
|
|
510
|
+
console.log(chalk.green(`✓ Auto-merge enabled via ${autoMergeSourceLabel} (held until a human approves this PR)`));
|
|
442
511
|
}
|
|
443
512
|
else {
|
|
444
|
-
console.log(chalk.green(
|
|
513
|
+
console.log(chalk.green(`✓ Auto-merge enabled for this PR (via ${autoMergeSourceLabel})`));
|
|
445
514
|
}
|
|
446
515
|
}
|
|
447
516
|
catch (err) {
|
|
448
517
|
console.log(chalk.yellow(`⚠ Could not set auto-merge label: ${err.message}`));
|
|
449
518
|
}
|
|
450
519
|
}
|
|
520
|
+
else {
|
|
521
|
+
console.log(chalk.dim(` Auto-merge: disabled (via ${autoMergeSourceLabel})`));
|
|
522
|
+
}
|
|
451
523
|
// -------------------------------------------------------------------------
|
|
452
524
|
// Step 6b: Apply auto-fix label if --auto-fix
|
|
453
525
|
// -------------------------------------------------------------------------
|
|
@@ -455,7 +527,8 @@ export async function submitCommand(options) {
|
|
|
455
527
|
try {
|
|
456
528
|
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-fix'], githubToken);
|
|
457
529
|
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-fix'], githubToken);
|
|
458
|
-
|
|
530
|
+
autoFixApplied = true;
|
|
531
|
+
console.log(chalk.green(`✓ Auto-fix (alpha) enabled for this PR (via ${options.autoFixSource === 'flag' ? '--auto-fix flag' : '.haystack.json'})`));
|
|
459
532
|
}
|
|
460
533
|
catch (err) {
|
|
461
534
|
console.log(chalk.yellow(`⚠ Could not set auto-fix label: ${err.message}`));
|
|
@@ -468,9 +541,11 @@ export async function submitCommand(options) {
|
|
|
468
541
|
try {
|
|
469
542
|
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:needs-review'], githubToken);
|
|
470
543
|
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:needs-review'], githubToken);
|
|
544
|
+
reviewLabelApplied = true;
|
|
471
545
|
if (typeof options.review === 'string') {
|
|
472
546
|
// Specific reviewer requested
|
|
473
547
|
await requestReviewers(remoteInfo.owner, remoteInfo.repo, prNumber, [options.review], githubToken);
|
|
548
|
+
reviewerRequestedOk = options.review;
|
|
474
549
|
console.log(chalk.green(`✓ Review requested from ${options.review}`));
|
|
475
550
|
}
|
|
476
551
|
else {
|
|
@@ -486,9 +561,9 @@ export async function submitCommand(options) {
|
|
|
486
561
|
// Step 8: Success!
|
|
487
562
|
// -------------------------------------------------------------------------
|
|
488
563
|
console.log(chalk.green('\n' + '━'.repeat(50)));
|
|
489
|
-
console.log(chalk.bold.green('\nPull Request Created!\n'));
|
|
564
|
+
console.log(chalk.bold.green(existingPR ? '\nPull Request Updated!\n' : '\nPull Request Created!\n'));
|
|
490
565
|
console.log(` ${chalk.dim('URL:')} ${chalk.cyan(prUrl)}`);
|
|
491
|
-
console.log(` ${chalk.dim('Title:')} ${
|
|
566
|
+
console.log(` ${chalk.dim('Title:')} ${prTitle}`);
|
|
492
567
|
console.log(` ${chalk.dim('Branch:')} ${currentBranch} → ${baseBranch}`);
|
|
493
568
|
if (options.autoMerge) {
|
|
494
569
|
// --review and auto-merge are not contradictory: the PR is queued but the
|
|
@@ -526,10 +601,42 @@ export async function submitCommand(options) {
|
|
|
526
601
|
status: 'polling',
|
|
527
602
|
};
|
|
528
603
|
savePendingSubmit(pendingState, gitRoot);
|
|
604
|
+
const ref = `${remoteInfo.owner}/${remoteInfo.repo}#${prNumber}`;
|
|
605
|
+
/** Emit the final --json payload (stdout) — the single machine-readable
|
|
606
|
+
* document of the run. `next` always tells the agent the follow-up command. */
|
|
607
|
+
const emitPayload = (analysis) => {
|
|
608
|
+
if (!options.json)
|
|
609
|
+
return;
|
|
610
|
+
process.stdout.write(JSON.stringify(withSchema('submit', {
|
|
611
|
+
status: existingPR ? 'already_exists' : 'created',
|
|
612
|
+
ref,
|
|
613
|
+
pr_number: prNumber,
|
|
614
|
+
url: prUrl,
|
|
615
|
+
title: prTitle,
|
|
616
|
+
title_source: titleSource,
|
|
617
|
+
body: prBody,
|
|
618
|
+
body_source: bodySource,
|
|
619
|
+
branch: currentBranch,
|
|
620
|
+
base: baseBranch,
|
|
621
|
+
draft: options.draft ?? false,
|
|
622
|
+
account: activeGitHubLogin ?? null,
|
|
623
|
+
// Applied state, not requested state: if a label failed to apply, the
|
|
624
|
+
// payload says so (enabled: false) even though it was asked for.
|
|
625
|
+
auto_merge: { enabled: autoMergeApplied, source: options.autoMergeSource ?? 'config' },
|
|
626
|
+
auto_fix: { enabled: autoFixApplied, source: options.autoFixSource ?? 'config' },
|
|
627
|
+
review_requested: reviewLabelApplied
|
|
628
|
+
? { reviewer: reviewerRequestedOk }
|
|
629
|
+
: null,
|
|
630
|
+
triage: { status: triageStatus },
|
|
631
|
+
analysis,
|
|
632
|
+
next: analysis.status === 'pending' ? `haystack triage ${ref} --json` : null,
|
|
633
|
+
}), null, 2) + '\n');
|
|
634
|
+
};
|
|
529
635
|
// If --no-wait or non-interactive, exit here
|
|
530
636
|
if (options.wait === false || !process.stdin.isTTY) {
|
|
531
|
-
console.log(chalk.dim(
|
|
532
|
-
console.log(chalk.dim('Check results later with: ') + chalk.cyan(
|
|
637
|
+
console.log(chalk.dim(`\nAnalysis running in background${options.wait === false ? '' : ' (no TTY — not waiting inline)'}.`));
|
|
638
|
+
console.log(chalk.dim('Check results later with: ') + chalk.cyan(`haystack triage ${ref}\n`));
|
|
639
|
+
emitPayload({ status: 'pending', verdict: null, merged: false, summary: null });
|
|
533
640
|
return;
|
|
534
641
|
}
|
|
535
642
|
// Wait for analysis inline by polling the result endpoint
|
|
@@ -539,7 +646,8 @@ export async function submitCommand(options) {
|
|
|
539
646
|
const sigintHandler = () => {
|
|
540
647
|
interrupted = true;
|
|
541
648
|
console.log(chalk.dim('\n\nAnalysis still running. Check later with:'));
|
|
542
|
-
console.log(chalk.cyan(
|
|
649
|
+
console.log(chalk.cyan(` haystack triage ${ref}\n`));
|
|
650
|
+
emitPayload({ status: 'pending', verdict: null, merged: false, summary: null });
|
|
543
651
|
process.exit(0);
|
|
544
652
|
};
|
|
545
653
|
process.on('SIGINT', sigintHandler);
|
|
@@ -548,6 +656,7 @@ export async function submitCommand(options) {
|
|
|
548
656
|
const deadline = Date.now() + POLL_TIMEOUT;
|
|
549
657
|
let consecutiveErrors = 0;
|
|
550
658
|
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
659
|
+
let emitted = false;
|
|
551
660
|
while (Date.now() < deadline && !interrupted) {
|
|
552
661
|
const result = await checkAnalysisReady(remoteInfo.owner, remoteInfo.repo, prNumber, githubToken);
|
|
553
662
|
if (result.status === 'ready') {
|
|
@@ -608,10 +717,33 @@ export async function submitCommand(options) {
|
|
|
608
717
|
if (!options.autoFix || verdict.state === 'good-to-merge') {
|
|
609
718
|
console.log(`\n ${chalk.dim('Review:')} ${chalk.cyan(verdict.reviewUrl)}\n`);
|
|
610
719
|
}
|
|
720
|
+
emitPayload({
|
|
721
|
+
status: 'complete',
|
|
722
|
+
// Same public vocabulary as `triage --json` / `pr get --json`.
|
|
723
|
+
verdict: verdict.state === 'good-to-merge'
|
|
724
|
+
? 'good_to_merge'
|
|
725
|
+
: verdict.needsHumanReview ? 'needs_review' : 'needs_input',
|
|
726
|
+
merged: verdict.autoMerged ?? false,
|
|
727
|
+
summary: verdict.summary,
|
|
728
|
+
});
|
|
729
|
+
emitted = true;
|
|
611
730
|
}
|
|
612
|
-
catch {
|
|
613
|
-
|
|
614
|
-
|
|
731
|
+
catch (err) {
|
|
732
|
+
const message = err.message;
|
|
733
|
+
if (err instanceof HaystackApiError && err.isAuthError) {
|
|
734
|
+
// Auth failure is not transient — retrying `haystack triage` with the
|
|
735
|
+
// same credential would fail identically. Report failed + exit 1.
|
|
736
|
+
console.error(chalk.red(`\n ${message}`));
|
|
737
|
+
emitPayload({ status: 'failed', verdict: null, merged: false, summary: message });
|
|
738
|
+
process.exitCode = 1;
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
// The PR exists and analysis completed — only the result fetch failed.
|
|
742
|
+
console.log(chalk.yellow(`⚠ Analysis complete but could not fetch results: ${message}`));
|
|
743
|
+
console.log(chalk.dim(' Check results with: ') + chalk.cyan(`haystack triage ${ref}\n`));
|
|
744
|
+
emitPayload({ status: 'pending', verdict: null, merged: false, summary: message });
|
|
745
|
+
}
|
|
746
|
+
emitted = true;
|
|
615
747
|
}
|
|
616
748
|
break;
|
|
617
749
|
}
|
|
@@ -620,19 +752,27 @@ export async function submitCommand(options) {
|
|
|
620
752
|
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
621
753
|
console.log(chalk.red(`\n Analysis check failed: ${result.message}`));
|
|
622
754
|
updatePendingSubmit({ status: 'failed', resolvedAt: new Date().toISOString() }, gitRoot);
|
|
755
|
+
emitPayload({ status: 'failed', verdict: null, merged: false, summary: result.message ?? null });
|
|
756
|
+
emitted = true;
|
|
757
|
+
// The PR was created, but the caller asked us to wait for a verdict
|
|
758
|
+
// and we could not deliver one — that is a failure, not a success.
|
|
759
|
+
process.exitCode = 1;
|
|
623
760
|
break;
|
|
624
761
|
}
|
|
625
762
|
}
|
|
626
763
|
else {
|
|
627
764
|
consecutiveErrors = 0;
|
|
628
765
|
}
|
|
629
|
-
process.
|
|
766
|
+
process.stderr.write(`\r ${chalk.dim('Status:')} Waiting for analysis...${' '.repeat(10)}`);
|
|
630
767
|
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
631
768
|
}
|
|
632
769
|
// Clean up SIGINT handler
|
|
633
770
|
process.removeListener('SIGINT', sigintHandler);
|
|
634
771
|
if (Date.now() >= deadline && !interrupted) {
|
|
635
772
|
console.log(chalk.yellow('\n\n Timed out waiting for analysis.'));
|
|
636
|
-
console.log(chalk.dim(' Check results later with: ') + chalk.cyan(
|
|
773
|
+
console.log(chalk.dim(' Check results later with: ') + chalk.cyan(`haystack triage ${ref}\n`));
|
|
774
|
+
if (!emitted) {
|
|
775
|
+
emitPayload({ status: 'pending', verdict: null, merged: false, summary: 'Timed out waiting for analysis; it continues server-side.' });
|
|
776
|
+
}
|
|
637
777
|
}
|
|
638
778
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `haystack system-map validate` — instant feedback on `.haystack/system.yml`.
|
|
3
|
+
*
|
|
4
|
+
* The system map holds customer-declared facts about how their system WORKS
|
|
5
|
+
* (build, boot, seed, log in, observe) so Haystack QA can run it. It never
|
|
6
|
+
* selects tests — Haystack decides what to test from each PR's diff. The
|
|
7
|
+
* `/map-your-system` skill authors the file; this command lets a user (or
|
|
8
|
+
* their coding agent) check it before committing, instead of waiting for the
|
|
9
|
+
* next QA run to reject it.
|
|
10
|
+
*
|
|
11
|
+
* Source-of-truth note: the QA pipeline's own loader for this file
|
|
12
|
+
* (`analysis/src/agents/systemMap.ts`) lives on the incoming QA-replay
|
|
13
|
+
* subsystem branch and is NOT on main yet — until it lands, this command is
|
|
14
|
+
* the only validator of the format, and its rules were copied verbatim from
|
|
15
|
+
* that loader. Once the subsystem merges, the analysis-side loader becomes
|
|
16
|
+
* the source of truth (it is a separate deployable the CLI cannot import),
|
|
17
|
+
* and rule changes must update both in the same PR.
|
|
18
|
+
*/
|
|
19
|
+
export interface SystemMapIssue {
|
|
20
|
+
severity: 'error' | 'warning';
|
|
21
|
+
path: string;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SystemMapSummary {
|
|
25
|
+
setup_steps: number;
|
|
26
|
+
toolchain_entries: number;
|
|
27
|
+
services: string[];
|
|
28
|
+
personas: string[];
|
|
29
|
+
data_commands: number;
|
|
30
|
+
observability_entries: number;
|
|
31
|
+
external_services: string[];
|
|
32
|
+
testing_entries: number;
|
|
33
|
+
constraints: number;
|
|
34
|
+
artifacts: string[];
|
|
35
|
+
}
|
|
36
|
+
export declare function validateSystemMapText(rawYaml: string): {
|
|
37
|
+
issues: SystemMapIssue[];
|
|
38
|
+
summary: SystemMapSummary | null;
|
|
39
|
+
};
|
|
40
|
+
export declare function systemMapValidateCommand(options: {
|
|
41
|
+
json?: boolean;
|
|
42
|
+
}): Promise<void>;
|