@haystackeditor/cli 0.15.31 → 0.17.0
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 +141 -61
- package/dist/assets/hooks/scripts/commit-msg.sh +3 -0
- package/dist/assets/hooks/scripts/post-commit.sh +3 -0
- package/dist/assets/hooks/scripts/pre-commit.sh +11 -6
- package/dist/assets/hooks/scripts/pre-push.sh +3 -0
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
- package/dist/commands/case-batch-contract.js +694 -0
- package/dist/commands/case-batch.js +1011 -0
- package/dist/commands/cloud-verifier-identity-census.js +5 -2
- package/dist/commands/combination-search-hook.js +139 -0
- package/dist/commands/dismiss.js +2 -1
- package/dist/commands/hooks.js +66 -7
- package/dist/commands/install-session-hooks.js +131 -44
- package/dist/commands/policy.js +32 -45
- package/dist/commands/precompute-delivery.js +6 -1
- package/dist/commands/scaffold-provisional-universe.js +8 -10
- package/dist/commands/setup.js +32 -10
- package/dist/commands/submit.js +39 -71
- package/dist/commands/telemetry.js +17 -2
- package/dist/commands/triage.js +2 -1
- package/dist/commands/verify-explore.js +1 -0
- package/dist/commands/verify-hosted-mcp.js +3 -12
- package/dist/commands/verify-hosted-reproducibility.js +49 -547
- package/dist/commands/verify-hosted.js +66 -168
- package/dist/commands/verify-precompute.js +64 -11
- package/dist/commands/verify.js +51 -139
- package/dist/index.js +290 -366
- package/dist/lazy.js +8 -0
- package/dist/schema.js +2 -1
- package/dist/tools/detect.js +3 -24
- package/dist/triage/astra.js +199 -0
- package/dist/triage/prompts.js +145 -179
- package/dist/triage/runner.js +104 -300
- package/dist/triage/types.js +2 -2
- package/dist/types.js +2 -6
- package/dist/utils/auth.js +14 -2
- package/dist/utils/git.js +60 -29
- package/dist/utils/github-api.js +14 -1
- package/dist/utils/haystack-api.js +38 -7
- package/dist/utils/hooks.js +43 -6
- package/dist/utils/prompter.js +25 -10
- package/dist/utils/safe-write.js +31 -0
- package/dist/utils/secret-paths.js +115 -0
- package/dist/utils/secrets.js +0 -1
- package/dist/utils/telemetry.js +34 -14
- package/dist/utils/update-check.js +151 -0
- package/package.json +19 -14
- package/schemas/case-batch.v1.json +235 -0
- package/schemas/cloud-verifier.v1.json +55 -100
- package/schemas/submit.v1.json +4 -2
- package/dist/commands/ask.d.ts +0 -14
- package/dist/commands/cloud-verifier-behaviors.d.ts +0 -27
- package/dist/commands/cloud-verifier-data-store-census.d.ts +0 -47
- package/dist/commands/cloud-verifier-data-store-drift.d.ts +0 -42
- package/dist/commands/cloud-verifier-identity-census.d.ts +0 -88
- package/dist/commands/cloud-verifier-materialization.d.ts +0 -16
- package/dist/commands/cloud-verifier-pascal-selector-census.d.ts +0 -29
- package/dist/commands/cloud-verifier-python-manifest-selector-census.d.ts +0 -27
- package/dist/commands/cloud-verifier-specialized-operational-census.d.ts +0 -51
- package/dist/commands/cloud-verifier-universe.d.ts +0 -31
- package/dist/commands/config.d.ts +0 -46
- package/dist/commands/design-verify.d.ts +0 -33
- package/dist/commands/dismiss.d.ts +0 -29
- package/dist/commands/hooks.d.ts +0 -13
- package/dist/commands/inbox.d.ts +0 -65
- package/dist/commands/init.d.ts +0 -10
- package/dist/commands/install-session-hooks.d.ts +0 -17
- package/dist/commands/login.d.ts +0 -8
- package/dist/commands/mcp.d.ts +0 -1
- package/dist/commands/policy.d.ts +0 -31
- package/dist/commands/pr-status.d.ts +0 -144
- package/dist/commands/pr.d.ts +0 -40
- package/dist/commands/precompute-delivery-contract.d.ts +0 -68
- package/dist/commands/precompute-delivery.d.ts +0 -20
- package/dist/commands/prepare-universe-review.d.ts +0 -115
- package/dist/commands/production-source-deny-policy.d.ts +0 -15
- package/dist/commands/request-review.d.ts +0 -26
- package/dist/commands/review.d.ts +0 -25
- package/dist/commands/rules.d.ts +0 -4
- package/dist/commands/scaffold-provisional-universe.d.ts +0 -468
- package/dist/commands/schema-cmd.d.ts +0 -2
- package/dist/commands/setup.d.ts +0 -28
- package/dist/commands/skills.d.ts +0 -8
- package/dist/commands/status.d.ts +0 -4
- package/dist/commands/submit.d.ts +0 -30
- package/dist/commands/system-map.d.ts +0 -42
- package/dist/commands/telemetry.d.ts +0 -53
- package/dist/commands/tokens.d.ts +0 -14
- package/dist/commands/triage.d.ts +0 -35
- package/dist/commands/verify-core.d.ts +0 -449
- package/dist/commands/verify-core.js +0 -789
- package/dist/commands/verify-explore.d.ts +0 -14
- package/dist/commands/verify-history.d.ts +0 -14
- package/dist/commands/verify-hosted-mcp.d.ts +0 -12
- package/dist/commands/verify-hosted-reproducibility.d.ts +0 -90
- package/dist/commands/verify-hosted.d.ts +0 -92
- package/dist/commands/verify-mcp.d.ts +0 -8
- package/dist/commands/verify-mcp.js +0 -517
- package/dist/commands/verify-ops.d.ts +0 -158
- package/dist/commands/verify-ops.js +0 -1148
- package/dist/commands/verify-precompute.d.ts +0 -30
- package/dist/commands/verify-reproducibility.d.ts +0 -85
- package/dist/commands/verify-reproducibility.js +0 -494
- package/dist/commands/verify-reseal.d.ts +0 -9
- package/dist/commands/verify-reseal.js +0 -148
- package/dist/commands/verify-sandboxes.d.ts +0 -95
- package/dist/commands/verify-sandboxes.js +0 -352
- package/dist/commands/verify.d.ts +0 -28
- package/dist/commands/webhooks.d.ts +0 -30
- package/dist/index.d.ts +0 -22
- package/dist/schema.d.ts +0 -28
- package/dist/states.d.ts +0 -29
- package/dist/tools/detect.d.ts +0 -50
- package/dist/triage/prompts.d.ts +0 -24
- package/dist/triage/runner.d.ts +0 -34
- package/dist/triage/types.d.ts +0 -42
- package/dist/types/verify-history.d.ts +0 -38
- package/dist/types.d.ts +0 -1684
- package/dist/utils/action-output.d.ts +0 -24
- package/dist/utils/analysis-api.d.ts +0 -187
- package/dist/utils/auth.d.ts +0 -79
- package/dist/utils/config.d.ts +0 -24
- package/dist/utils/design-verifier-api.d.ts +0 -208
- package/dist/utils/design-verifier-history.d.ts +0 -46
- package/dist/utils/design-verifier-result.d.ts +0 -74
- package/dist/utils/detect.d.ts +0 -43
- package/dist/utils/git.d.ts +0 -135
- package/dist/utils/github-api.d.ts +0 -104
- package/dist/utils/haystack-api.d.ts +0 -37
- package/dist/utils/hooks.d.ts +0 -12
- package/dist/utils/pending-state.d.ts +0 -40
- package/dist/utils/pr-ref.d.ts +0 -27
- package/dist/utils/prompter.d.ts +0 -85
- package/dist/utils/secrets.d.ts +0 -47
- package/dist/utils/telemetry.d.ts +0 -19
- /package/dist/commands/{precompute-delivery-worker.d.ts → combination-search-hook-contract.js} +0 -0
package/dist/triage/runner.js
CHANGED
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Triage runner —
|
|
2
|
+
* Triage runner — runs the pre-PR checkers in parallel and aggregates results.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Each checker (code-review, rules-validator) is one structured Responses API
|
|
5
|
+
* call to gpt-6-astra at the highest reasoning effort (see astra.ts). A checker
|
|
6
|
+
* that fails is reported as "<checker> failed: <error>" and never blocks
|
|
7
|
+
* submit; there is no second reviewer to fall back to.
|
|
6
8
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { existsSync, readFileSync, mkdirSync, rmSync } from 'fs';
|
|
9
|
+
import { execFileSync } from 'child_process';
|
|
10
|
+
import { existsSync, readFileSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
|
9
11
|
import { join } from 'path';
|
|
10
12
|
import chalk from 'chalk';
|
|
11
|
-
import { buildCodeReviewPrompt, buildRulesValidatorPrompt } from './prompts.js';
|
|
13
|
+
import { buildCodeReviewPrompt, buildRulesValidatorPrompt, CODE_REVIEW_SCHEMA, RULES_VALIDATOR_SCHEMA, } from './prompts.js';
|
|
14
|
+
import { callAstra, TRIAGE_MODEL, TRIAGE_REASONING_EFFORT } from './astra.js';
|
|
12
15
|
import { resolveDiffBaseRef } from '../utils/git.js';
|
|
13
16
|
import { trackError } from '../utils/telemetry.js';
|
|
14
17
|
// ============================================================================
|
|
15
18
|
// Constants
|
|
16
19
|
// ============================================================================
|
|
17
20
|
const TRIAGE_DIR = '.haystack/triage';
|
|
18
|
-
/** Wall-clock timeout per checker. Overridable via .haystack.json triage.timeoutMs or --triage-timeout. */
|
|
19
|
-
const DEFAULT_TIMEOUT_MS = 180_000; // 3 minutes
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
-
* triage.
|
|
23
|
-
*
|
|
22
|
+
* Abort a checker when its response stream has been silent this long.
|
|
23
|
+
* Overridable via .haystack.json triage.timeoutMs or --triage-timeout.
|
|
24
|
+
* Measured 2026-09-24: a max-effort review of a 20 KB diff streamed its
|
|
25
|
+
* longest silent gap at 14.6s.
|
|
24
26
|
*/
|
|
25
|
-
const
|
|
26
|
-
'code-review': 8,
|
|
27
|
-
'rules-validator': 10,
|
|
28
|
-
};
|
|
27
|
+
const DEFAULT_STALL_MS = 300_000;
|
|
29
28
|
// ============================================================================
|
|
30
29
|
// Agent policy file discovery
|
|
31
30
|
// ============================================================================
|
|
@@ -54,218 +53,53 @@ function discoverAgentPolicyFiles(gitRoot) {
|
|
|
54
53
|
}
|
|
55
54
|
return results;
|
|
56
55
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* Detect which agentic CLI is installed.
|
|
62
|
-
* Returns the first one found, or null if none available.
|
|
63
|
-
*/
|
|
64
|
-
export function detectAgenticCLI() {
|
|
65
|
-
const isWindows = process.platform === 'win32';
|
|
66
|
-
const inCodexSession = Boolean(process.env.CODEX_SHELL || process.env.CODEX_THREAD_ID || process.env.CODEX_CI);
|
|
67
|
-
const inClaudeSession = Boolean(process.env.CLAUDE_CODE || process.env.CLAUDECODE);
|
|
68
|
-
const clis = inCodexSession
|
|
69
|
-
? ['codex', 'claude', 'gemini']
|
|
70
|
-
: inClaudeSession
|
|
71
|
-
? ['claude', 'codex', 'gemini']
|
|
72
|
-
: ['claude', 'codex', 'gemini'];
|
|
73
|
-
for (const name of clis) {
|
|
74
|
-
try {
|
|
75
|
-
execSync(isWindows ? `where ${name}` : `which ${name}`, { stdio: 'ignore' });
|
|
76
|
-
return name;
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
// Not installed
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Build the CLI command + args for the detected agentic tool.
|
|
86
|
-
*/
|
|
87
|
-
export function buildCommand(cli, prompt, maxTurns) {
|
|
88
|
-
switch (cli) {
|
|
89
|
-
case 'claude':
|
|
90
|
-
return {
|
|
91
|
-
command: 'claude',
|
|
92
|
-
args: [
|
|
93
|
-
'-p', prompt,
|
|
94
|
-
'--dangerously-skip-permissions',
|
|
95
|
-
'--max-turns', String(maxTurns),
|
|
96
|
-
'--allowedTools', 'Bash', 'Read', 'Glob', 'Grep', 'Write',
|
|
97
|
-
],
|
|
98
|
-
};
|
|
99
|
-
case 'codex':
|
|
100
|
-
return {
|
|
101
|
-
command: 'codex',
|
|
102
|
-
args: [
|
|
103
|
-
'--sandbox', 'workspace-write',
|
|
104
|
-
'--ask-for-approval', 'never',
|
|
105
|
-
'exec',
|
|
106
|
-
'--ephemeral',
|
|
107
|
-
prompt,
|
|
108
|
-
],
|
|
109
|
-
};
|
|
110
|
-
case 'gemini':
|
|
111
|
-
return {
|
|
112
|
-
command: 'gemini',
|
|
113
|
-
args: ['-p', prompt, '-y'],
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
56
|
+
const CHECKER_LABEL = {
|
|
57
|
+
'code-review': 'code review',
|
|
58
|
+
'rules-validator': 'rules validator',
|
|
59
|
+
};
|
|
117
60
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
61
|
+
* Map a schema-conformant reply onto the CheckerResult shape submit consumes.
|
|
62
|
+
* Strict mode enforces types, enums and required keys but not non-empty
|
|
63
|
+
* strings; a finding with a blank file or summary is left out and counted in
|
|
64
|
+
* `malformed` so the runner reports it, without discarding the other findings.
|
|
120
65
|
*/
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
proc.stderr?.on('data', (data) => {
|
|
138
|
-
stderr += data.toString();
|
|
139
|
-
});
|
|
140
|
-
// Timeout
|
|
141
|
-
const timer = setTimeout(() => {
|
|
142
|
-
proc.kill('SIGTERM');
|
|
143
|
-
stderr += `\nProcess killed after ${Math.round(timeoutMs / 1000)}s timeout`;
|
|
144
|
-
}, timeoutMs);
|
|
145
|
-
proc.on('close', (code) => {
|
|
146
|
-
clearTimeout(timer);
|
|
147
|
-
resolve({ exitCode: code ?? 1, output: `${stdout}\n${stderr}`.trim() });
|
|
148
|
-
});
|
|
149
|
-
proc.on('error', (err) => {
|
|
150
|
-
clearTimeout(timer);
|
|
151
|
-
resolve({ exitCode: 1, output: err.message });
|
|
152
|
-
});
|
|
66
|
+
function toCheckerResult(name, reply) {
|
|
67
|
+
const usable = reply.findings.filter(finding => finding.file.trim() && finding.summary.trim());
|
|
68
|
+
const malformed = reply.findings.length - usable.length;
|
|
69
|
+
const issues = usable.map((finding) => {
|
|
70
|
+
const issue = {
|
|
71
|
+
file: finding.file,
|
|
72
|
+
severity: finding.severity,
|
|
73
|
+
message: finding.summary,
|
|
74
|
+
category: finding.category,
|
|
75
|
+
failureScenario: finding.failureScenario,
|
|
76
|
+
};
|
|
77
|
+
if (typeof finding.line === 'number' && finding.line > 0)
|
|
78
|
+
issue.line = finding.line;
|
|
79
|
+
if (finding.rule)
|
|
80
|
+
issue.rule = finding.rule;
|
|
81
|
+
return issue;
|
|
153
82
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
* the diff is too large for the budget; should not block submit.
|
|
160
|
-
* - `timeout`: we killed the process at the configured wall-clock cap. Also non-blocking.
|
|
161
|
-
* - `error`: anything else (auth failure, crash, malformed output). Blocks.
|
|
162
|
-
*
|
|
163
|
-
* The max-turns markers come from the CLIs themselves (Claude CLI prints
|
|
164
|
-
* "Reached max turns", Codex prints "turn limit"). If a new CLI is added, add
|
|
165
|
-
* its marker here — unknown failures fall through to `error` (safe default).
|
|
166
|
-
*/
|
|
167
|
-
function classifyCheckerFailure(output) {
|
|
168
|
-
const summary = summarizeCheckerFailureOutput(output) ?? '(no output)';
|
|
169
|
-
// Claude: "Reached max turns (N)". Codex/Gemini variants: "turn limit", "max iterations".
|
|
170
|
-
if (/reached max turns|max turns reached|turn limit|max iterations/i.test(output)) {
|
|
171
|
-
return { kind: 'max_turns', summary };
|
|
172
|
-
}
|
|
173
|
-
// Our own SIGTERM message from spawnChecker.
|
|
174
|
-
if (/process killed after \d+s timeout/i.test(output)) {
|
|
175
|
-
return { kind: 'timeout', summary };
|
|
176
|
-
}
|
|
177
|
-
return { kind: 'error', summary };
|
|
178
|
-
}
|
|
179
|
-
function summarizeCheckerFailureOutput(output) {
|
|
180
|
-
const lines = output
|
|
181
|
-
.split('\n')
|
|
182
|
-
.map(line => line.trim())
|
|
183
|
-
.filter(Boolean);
|
|
184
|
-
if (lines.length === 0)
|
|
185
|
-
return null;
|
|
186
|
-
const priorityPatterns = [
|
|
187
|
-
/not logged in/i,
|
|
188
|
-
/stream disconnected|error sending request|failed to refresh available models/i,
|
|
189
|
-
/operation not permitted|readonly database|permission denied/i,
|
|
190
|
-
/timeout|timed out/i,
|
|
191
|
-
/unauthorized|forbidden/i,
|
|
192
|
-
/^error:/i,
|
|
193
|
-
/failed/i,
|
|
194
|
-
];
|
|
195
|
-
let summary = lines[lines.length - 1];
|
|
196
|
-
for (const pattern of priorityPatterns) {
|
|
197
|
-
const match = lines.find(line => pattern.test(line));
|
|
198
|
-
if (match) {
|
|
199
|
-
summary = match;
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return summary.length > 220
|
|
204
|
-
? `${summary.slice(0, 217)}...`
|
|
205
|
-
: summary;
|
|
206
|
-
}
|
|
207
|
-
// ============================================================================
|
|
208
|
-
// Result reading
|
|
209
|
-
// ============================================================================
|
|
210
|
-
/**
|
|
211
|
-
* Read and validate a checker result JSON file.
|
|
212
|
-
*/
|
|
213
|
-
function readCheckerResult(outputPath, checkerName) {
|
|
214
|
-
if (!existsSync(outputPath)) {
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
try {
|
|
218
|
-
const content = readFileSync(outputPath, 'utf-8');
|
|
219
|
-
const result = JSON.parse(content);
|
|
220
|
-
// Basic validation
|
|
221
|
-
if (!result.checker || !Array.isArray(result.issues) || typeof result.passed !== 'boolean') {
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
// Ensure issues have required fields
|
|
225
|
-
result.issues = result.issues.filter((issue) => issue.file && issue.message && issue.severity);
|
|
226
|
-
// Recalculate passed based on issues
|
|
227
|
-
result.passed = !result.issues.some((i) => i.severity === 'error');
|
|
228
|
-
return result;
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
83
|
+
const passed = !issues.some(issue => issue.severity === 'error');
|
|
84
|
+
const result = name === 'rules-validator'
|
|
85
|
+
? { checker: name, issues, summary: reply.summary, passed, rulesChecked: reply.rulesChecked ?? 0 }
|
|
86
|
+
: { checker: name, issues, summary: reply.summary, passed };
|
|
87
|
+
return { result, malformed };
|
|
233
88
|
}
|
|
234
89
|
// ============================================================================
|
|
235
90
|
// Main runner
|
|
236
91
|
// ============================================================================
|
|
237
92
|
/**
|
|
238
|
-
* Run pre-PR triage checks
|
|
93
|
+
* Run pre-PR triage checks in parallel.
|
|
239
94
|
*
|
|
240
95
|
* @param gitRoot - Absolute path to the git repository root
|
|
241
96
|
* @param baseBranch - The base branch to diff against (e.g., "main")
|
|
242
97
|
* @returns Aggregated triage results
|
|
243
|
-
* @throws If no agentic CLI is detected
|
|
244
98
|
*/
|
|
245
99
|
export async function runTriage(gitRoot, baseBranch, options = {}) {
|
|
246
100
|
const startTime = Date.now();
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// calling us — we just see a flat override map.
|
|
250
|
-
const maxTurns = { ...DEFAULT_MAX_TURNS };
|
|
251
|
-
for (const [name, value] of Object.entries(options.maxTurns ?? {})) {
|
|
252
|
-
if (typeof value === 'number' && value > 0) {
|
|
253
|
-
maxTurns[name] = value;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
257
|
-
// Detect CLI
|
|
258
|
-
const cli = detectAgenticCLI();
|
|
259
|
-
if (!cli) {
|
|
260
|
-
throw new Error('No agentic CLI found. Install Claude Code (claude), Codex CLI (codex), or Gemini CLI (gemini).');
|
|
261
|
-
}
|
|
262
|
-
// Fast-fail in Codex app shells where outbound network is disabled.
|
|
263
|
-
// Spawning nested `codex exec` will fail after retries; skip triage immediately instead.
|
|
264
|
-
const codexNetworkDisabled = process.env.CODEX_SANDBOX_NETWORK_DISABLED === '1';
|
|
265
|
-
if (cli === 'codex' && codexNetworkDisabled) {
|
|
266
|
-
throw new Error('Codex shell has network disabled (CODEX_SANDBOX_NETWORK_DISABLED=1), so triage sub-agents cannot run. Re-run outside this sandbox or use --force.');
|
|
267
|
-
}
|
|
268
|
-
console.log(chalk.dim(` Using: ${cli}`));
|
|
101
|
+
const stallMs = options.timeoutMs ?? DEFAULT_STALL_MS;
|
|
102
|
+
console.log(chalk.dim(` Reviewer: ${TRIAGE_MODEL} (reasoning effort ${TRIAGE_REASONING_EFFORT})`));
|
|
269
103
|
// Prepare output directory
|
|
270
104
|
const triageDir = join(gitRoot, TRIAGE_DIR);
|
|
271
105
|
if (existsSync(triageDir)) {
|
|
@@ -274,123 +108,93 @@ export async function runTriage(gitRoot, baseBranch, options = {}) {
|
|
|
274
108
|
mkdirSync(triageDir, { recursive: true });
|
|
275
109
|
// Resolve the ref to diff against once. Prefers origin/<base> over the bare
|
|
276
110
|
// local <base> ref (which is often stale and balloons the diff with already-
|
|
277
|
-
// merged code).
|
|
278
|
-
// commands in the prompts use this single resolved ref so they all see the
|
|
279
|
-
// same fork point.
|
|
111
|
+
// merged code).
|
|
280
112
|
const diffBaseRef = resolveDiffBaseRef(baseBranch);
|
|
281
113
|
if (diffBaseRef !== baseBranch) {
|
|
282
114
|
console.log(chalk.dim(` Diff base: ${diffBaseRef}`));
|
|
283
115
|
}
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
let
|
|
116
|
+
// 10 lines of context per hunk: the reviewer sees only the diff. argv, not a
|
|
117
|
+
// shell string: the ref comes from a branch name.
|
|
118
|
+
let diff;
|
|
287
119
|
try {
|
|
288
|
-
|
|
289
|
-
// Only inline if the diff is under 100K chars — beyond that, let the agent
|
|
290
|
-
// run git diff itself (it can paginate or review file-by-file).
|
|
291
|
-
if (diffOutput.length > 0 && diffOutput.length <= 100_000) {
|
|
292
|
-
precomputedDiff = diffOutput;
|
|
293
|
-
}
|
|
120
|
+
diff = execFileSync('git', ['diff', '-U10', '--end-of-options', `${diffBaseRef}...HEAD`], { cwd: gitRoot, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
294
121
|
}
|
|
295
|
-
catch {
|
|
296
|
-
|
|
122
|
+
catch (err) {
|
|
123
|
+
const stderr = err?.stderr?.toString().trim();
|
|
124
|
+
const cause = stderr || (err instanceof Error ? err.message : String(err));
|
|
125
|
+
throw new Error(`Could not compute the triage diff against ${diffBaseRef}: ${cause}`);
|
|
297
126
|
}
|
|
127
|
+
writeFileSync(join(triageDir, 'diff.patch'), diff);
|
|
298
128
|
// Build checker configs
|
|
299
129
|
const checkers = [];
|
|
300
130
|
// 1. Code review (always runs)
|
|
301
|
-
const codeReviewOutput = join(triageDir, 'code-review.json');
|
|
302
131
|
checkers.push({
|
|
303
132
|
name: 'code-review',
|
|
304
|
-
prompt: buildCodeReviewPrompt(diffBaseRef,
|
|
305
|
-
|
|
306
|
-
|
|
133
|
+
prompt: buildCodeReviewPrompt(diffBaseRef, diff),
|
|
134
|
+
schema: CODE_REVIEW_SCHEMA,
|
|
135
|
+
outputFile: join(triageDir, 'code-review.json'),
|
|
307
136
|
});
|
|
308
137
|
// 2. Rules validator (if pr-rules.yml or agent instruction files exist)
|
|
309
138
|
const rulesPath = join(gitRoot, '.haystack', 'pr-rules.yml');
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
prompt: rulesPrompt,
|
|
320
|
-
outputFile: rulesValidatorOutput,
|
|
321
|
-
maxTurns: maxTurns['rules-validator'],
|
|
322
|
-
});
|
|
323
|
-
}
|
|
139
|
+
const rulesYaml = existsSync(rulesPath) ? readFileSync(rulesPath, 'utf-8') : '';
|
|
140
|
+
const rulesPrompt = buildRulesValidatorPrompt(diffBaseRef, rulesYaml, diff, discoverAgentPolicyFiles(gitRoot));
|
|
141
|
+
if (rulesPrompt) {
|
|
142
|
+
checkers.push({
|
|
143
|
+
name: 'rules-validator',
|
|
144
|
+
prompt: rulesPrompt,
|
|
145
|
+
schema: RULES_VALIDATOR_SCHEMA,
|
|
146
|
+
outputFile: join(triageDir, 'rules-validator.json'),
|
|
147
|
+
});
|
|
324
148
|
}
|
|
325
|
-
|
|
326
|
-
const checkerNames = checkers.map(c => c.name);
|
|
327
|
-
console.log(chalk.dim(` Checkers: ${checkerNames.join(', ')}`));
|
|
149
|
+
console.log(chalk.dim(` Checkers: ${checkers.map(c => c.name).join(', ')}`));
|
|
328
150
|
console.log(chalk.dim(` Running ${checkers.length} checker(s) in parallel...\n`));
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
151
|
+
const settled = await Promise.allSettled(checkers.map(async (checker) => {
|
|
152
|
+
const label = chalk.dim(` [${checker.name}]`);
|
|
153
|
+
console.log(`${label} Starting...`);
|
|
154
|
+
try {
|
|
155
|
+
const reply = await callAstra({
|
|
156
|
+
instructions: checker.prompt.instructions,
|
|
157
|
+
input: checker.prompt.input,
|
|
158
|
+
schemaName: checker.name.replace('-', '_'),
|
|
159
|
+
schema: checker.schema,
|
|
160
|
+
stallMs,
|
|
161
|
+
});
|
|
162
|
+
const mapped = toCheckerResult(checker.name, reply);
|
|
163
|
+
writeFileSync(checker.outputFile, JSON.stringify(mapped.result, null, 2));
|
|
164
|
+
console.log(`${label} ${chalk.green('Done')}`);
|
|
165
|
+
return mapped;
|
|
336
166
|
}
|
|
337
|
-
|
|
338
|
-
console.log(`${
|
|
167
|
+
catch (err) {
|
|
168
|
+
console.log(`${label} ${chalk.yellow('Failed')}`);
|
|
169
|
+
throw err;
|
|
339
170
|
}
|
|
340
|
-
return { checker, result };
|
|
341
171
|
}));
|
|
342
|
-
// Collect results
|
|
343
172
|
const results = [];
|
|
344
173
|
const errors = [];
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
checker
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
reason: String(spawnResult.reason),
|
|
354
|
-
});
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
const { checker, result } = spawnResult.value;
|
|
358
|
-
const checkerResult = readCheckerResult(checker.outputFile, checker.name);
|
|
359
|
-
if (checkerResult) {
|
|
360
|
-
results.push(checkerResult);
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
// Sub-agent didn't produce valid output — classify as warning vs hard error.
|
|
364
|
-
const classification = classifyCheckerFailure(result.output);
|
|
365
|
-
if (classification.kind === 'max_turns') {
|
|
366
|
-
warnings.push(`${checker.name}: hit max turns (${checker.maxTurns}) — raise via --max-turns or .haystack.json triage.maxTurns, or split the PR into smaller changes`);
|
|
367
|
-
}
|
|
368
|
-
else if (classification.kind === 'timeout') {
|
|
369
|
-
warnings.push(`${checker.name}: wall-clock timeout after ${Math.round(timeoutMs / 1000)}s — consider splitting the PR`);
|
|
370
|
-
}
|
|
371
|
-
else {
|
|
372
|
-
const exitPart = result.exitCode !== 0 ? ` (exit ${result.exitCode})` : '';
|
|
373
|
-
errors.push(`${checker.name}: ${classification.summary}${exitPart}`);
|
|
174
|
+
settled.forEach((outcome, index) => {
|
|
175
|
+
const checker = checkers[index];
|
|
176
|
+
if (outcome.status === 'fulfilled') {
|
|
177
|
+
results.push(outcome.value.result);
|
|
178
|
+
if (outcome.value.malformed > 0) {
|
|
179
|
+
errors.push(`${CHECKER_LABEL[checker.name]} returned ${outcome.value.malformed} finding(s) with an empty file or summary; they were left out`);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
374
182
|
}
|
|
375
|
-
|
|
183
|
+
const message = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
184
|
+
errors.push(`${CHECKER_LABEL[checker.name]} failed: ${message}`);
|
|
376
185
|
trackError('triage_checker_failed', {
|
|
377
186
|
checker: checker.name,
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
timeout_ms: timeoutMs,
|
|
382
|
-
exit_code: result.exitCode,
|
|
383
|
-
summary: classification.summary,
|
|
187
|
+
model: TRIAGE_MODEL,
|
|
188
|
+
stall_ms: stallMs,
|
|
189
|
+
summary: message.slice(0, 220),
|
|
384
190
|
});
|
|
385
|
-
}
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
const passed = results.every(r => r.passed) && errors.length === 0;
|
|
191
|
+
});
|
|
192
|
+
// A checker that failed to run gives no signal either way; only findings
|
|
193
|
+
// with severity "error" fail triage.
|
|
389
194
|
return {
|
|
390
|
-
passed,
|
|
195
|
+
passed: results.every(r => r.passed),
|
|
391
196
|
results,
|
|
392
197
|
errors,
|
|
393
|
-
warnings,
|
|
394
198
|
duration: Date.now() - startTime,
|
|
395
199
|
};
|
|
396
200
|
}
|
package/dist/triage/types.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for the pre-PR triage system.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Each checker is one structured gpt-6-astra call (see astra.ts); the runner
|
|
5
|
+
* maps its findings onto these shapes and aggregates them.
|
|
6
6
|
*/
|
|
7
7
|
export {};
|
package/dist/types.js
CHANGED
|
@@ -117,7 +117,6 @@ export const AI_REVIEWER_BOT_USERNAMES = {
|
|
|
117
117
|
codereviewer: 'codereviewbot[bot]',
|
|
118
118
|
codium: 'codiumai[bot]',
|
|
119
119
|
};
|
|
120
|
-
const AIReviewerSourceSchema = z.enum(AI_REVIEWER_SOURCES);
|
|
121
120
|
const MergeQueueConfigSchema = z.object({
|
|
122
121
|
/** Enable the Haystack auto-merge queue */
|
|
123
122
|
merge_queue: z.boolean().optional(),
|
|
@@ -272,11 +271,8 @@ const SecretDeclarationSchema = z.object({
|
|
|
272
271
|
// =============================================================================
|
|
273
272
|
// TRIAGE CONFIGURATION
|
|
274
273
|
// =============================================================================
|
|
275
|
-
const TriageCheckerNameSchema = z.enum(['code-review', 'rules-validator']);
|
|
276
274
|
const TriageConfigSchema = z.object({
|
|
277
|
-
/**
|
|
278
|
-
maxTurns: z.record(TriageCheckerNameSchema, z.number().int().positive()).optional(),
|
|
279
|
-
/** Wall-clock timeout per checker, in milliseconds. */
|
|
275
|
+
/** Abort a checker when its model stream has been silent this long, in milliseconds. */
|
|
280
276
|
timeoutMs: z.number().int().positive().optional(),
|
|
281
277
|
});
|
|
282
278
|
// =============================================================================
|
|
@@ -322,6 +318,6 @@ export const HaystackConfigSchema = z.object({
|
|
|
322
318
|
secrets: z.record(z.string(), SecretDeclarationSchema).optional(),
|
|
323
319
|
/** Merge queue configuration for autonomous PR maintenance */
|
|
324
320
|
merge_queue: MergeQueueConfigSchema.optional(),
|
|
325
|
-
/** Pre-PR triage configuration (
|
|
321
|
+
/** Pre-PR triage configuration (stall timeout) */
|
|
326
322
|
triage: TriageConfigSchema.optional(),
|
|
327
323
|
});
|
package/dist/utils/auth.js
CHANGED
|
@@ -18,6 +18,15 @@ const TOKEN_FILE_DISPLAY = '~/.haystack/credentials.json';
|
|
|
18
18
|
*/
|
|
19
19
|
export const HEADLESS_TOKEN_FILE = path.join(CONFIG_DIR, 'cli-token');
|
|
20
20
|
const HEADLESS_TOKEN_FILE_DISPLAY = '~/.haystack/cli-token';
|
|
21
|
+
const HEADLESS_TOKEN_PREFIX = 'hsk_live_';
|
|
22
|
+
/**
|
|
23
|
+
* True for a Haystack headless token (`hsk_live_*`). These authenticate to
|
|
24
|
+
* Haystack only: they must never be sent to api.github.com or used as a git
|
|
25
|
+
* push password, where they would leak to GitHub and fail anyway.
|
|
26
|
+
*/
|
|
27
|
+
export function isHeadlessToken(token) {
|
|
28
|
+
return token.startsWith(HEADLESS_TOKEN_PREFIX);
|
|
29
|
+
}
|
|
21
30
|
const GITHUB_LOGIN_PATTERN = /^[A-Za-z\d](?:[A-Za-z\d]|-(?=[A-Za-z\d])){0,38}$/;
|
|
22
31
|
function emptyStore() {
|
|
23
32
|
return {
|
|
@@ -362,13 +371,13 @@ export class NotLoggedInError extends Error {
|
|
|
362
371
|
* on-disk file so CI can override without touching the home directory. */
|
|
363
372
|
export async function readHeadlessToken() {
|
|
364
373
|
const envToken = process.env.HAYSTACK_CLI_TOKEN;
|
|
365
|
-
if (envToken && envToken
|
|
374
|
+
if (envToken && isHeadlessToken(envToken)) {
|
|
366
375
|
return { token: envToken, label: 'env:HAYSTACK_CLI_TOKEN' };
|
|
367
376
|
}
|
|
368
377
|
try {
|
|
369
378
|
const raw = await fs.readFile(HEADLESS_TOKEN_FILE, 'utf8');
|
|
370
379
|
const parsed = JSON.parse(raw);
|
|
371
|
-
if (typeof parsed.token === 'string' && parsed.token
|
|
380
|
+
if (typeof parsed.token === 'string' && isHeadlessToken(parsed.token)) {
|
|
372
381
|
return parsed;
|
|
373
382
|
}
|
|
374
383
|
return null;
|
|
@@ -385,6 +394,9 @@ export async function writeHeadlessToken(record) {
|
|
|
385
394
|
await fs.writeFile(HEADLESS_TOKEN_FILE, JSON.stringify(record, null, 2) + '\n', {
|
|
386
395
|
mode: 0o600,
|
|
387
396
|
});
|
|
397
|
+
// writeFile's mode only applies when it creates the file; tighten an
|
|
398
|
+
// existing file too, as saveStore does.
|
|
399
|
+
await fs.chmod(HEADLESS_TOKEN_FILE, 0o600);
|
|
388
400
|
return HEADLESS_TOKEN_FILE_DISPLAY;
|
|
389
401
|
}
|
|
390
402
|
export async function clearHeadlessToken() {
|