@haystackeditor/cli 0.15.29 → 0.15.31

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 (32) hide show
  1. package/dist/assets/telemetry/browser-runtime.js +148 -2
  2. package/dist/commands/design-verify.d.ts +2 -0
  3. package/dist/commands/design-verify.js +27 -1
  4. package/dist/commands/precompute-delivery-contract.d.ts +68 -0
  5. package/dist/commands/precompute-delivery-contract.js +1 -0
  6. package/dist/commands/precompute-delivery-worker.d.ts +1 -0
  7. package/dist/commands/precompute-delivery-worker.js +68 -0
  8. package/dist/commands/precompute-delivery.d.ts +20 -0
  9. package/dist/commands/precompute-delivery.js +319 -0
  10. package/dist/commands/verify-explore.d.ts +14 -0
  11. package/dist/commands/verify-explore.js +180 -0
  12. package/dist/commands/verify-history.d.ts +14 -0
  13. package/dist/commands/verify-history.js +152 -0
  14. package/dist/commands/verify-hosted.d.ts +12 -2
  15. package/dist/commands/verify-hosted.js +305 -50
  16. package/dist/commands/verify-precompute.d.ts +12 -1
  17. package/dist/commands/verify-precompute.js +135 -27
  18. package/dist/commands/verify.d.ts +6 -0
  19. package/dist/commands/verify.js +22 -0
  20. package/dist/index.js +71 -5
  21. package/dist/types/verify-history.d.ts +38 -0
  22. package/dist/types/verify-history.js +1 -0
  23. package/dist/utils/design-verifier-api.d.ts +8 -0
  24. package/dist/utils/design-verifier-api.js +23 -0
  25. package/dist/utils/design-verifier-history.d.ts +46 -0
  26. package/dist/utils/design-verifier-history.js +79 -0
  27. package/dist/utils/design-verifier-result.d.ts +17 -0
  28. package/dist/utils/design-verifier-result.js +80 -1
  29. package/dist/utils/git.js +5 -1
  30. package/dist/utils/telemetry.js +8 -2
  31. package/package.json +1 -1
  32. package/schemas/cloud-verifier.v1.json +108 -45
@@ -0,0 +1,79 @@
1
+ const outcomes = ['healthy', 'degraded', 'invalid', 'failed', 'unknown'];
2
+ export function isDesignVerifierHistoryReport(value, expected) {
3
+ if (!value || typeof value !== 'object' || Array.isArray(value))
4
+ return false;
5
+ const report = value;
6
+ if (report.version !== 'dv-cli-history-v1' || report.owner !== expected.owner ||
7
+ report.repo !== expected.repo || report.limit !== expected.limit ||
8
+ !Number.isSafeInteger(report.retentionDays) || report.retentionDays <= 0 ||
9
+ !Array.isArray(report.runs) || report.runs.length > report.limit || !report.summary)
10
+ return false;
11
+ const seen = new Set();
12
+ const counts = { healthy: 0, degraded: 0, invalid: 0, failed: 0, unknown: 0 };
13
+ const blockers = new Map();
14
+ for (const run of report.runs) {
15
+ if (!run || !Number.isSafeInteger(run.pr) || run.pr < 1 || typeof run.runId !== 'string')
16
+ return false;
17
+ const prefix = `${expected.owner}--${expected.repo}--${run.pr}--`;
18
+ if (!run.runId.startsWith(prefix) ||
19
+ !/^[0-9a-f]{12}(?:--attempt-[1-9]\d*)?$/.test(run.runId.slice(prefix.length)) ||
20
+ seen.has(run.runId) || !outcomes.includes(run.outcome) ||
21
+ !['succeeded', 'failed', 'failed-stale'].includes(run.status) ||
22
+ (run.outcome === 'failed') !== (run.status !== 'succeeded') ||
23
+ typeof run.createdAt !== 'string' || !Number.isFinite(Date.parse(run.createdAt)) ||
24
+ (run.finishedAt !== null && (typeof run.finishedAt !== 'string' || !Number.isFinite(Date.parse(run.finishedAt)))) ||
25
+ (run.baseSha !== null && typeof run.baseSha !== 'string') ||
26
+ (run.headSha !== null && typeof run.headSha !== 'string') ||
27
+ (run.admitted !== null && (!Number.isSafeInteger(run.admitted) || run.admitted < 0)) ||
28
+ !Array.isArray(run.losses) || run.losses.some(loss => !loss || typeof loss.code !== 'string' || !loss.code || typeof loss.owner !== 'string' ||
29
+ !loss.owner || !['degraded', 'invalid'].includes(loss.impact) ||
30
+ !Number.isSafeInteger(loss.count) || loss.count < 1))
31
+ return false;
32
+ if (['healthy', 'degraded'].includes(run.outcome) && (run.admitted === null || run.admitted === 0))
33
+ return false;
34
+ seen.add(run.runId);
35
+ counts[run.outcome]++;
36
+ for (const code of new Set(run.losses.map(loss => loss.code))) {
37
+ blockers.set(code, (blockers.get(code) || 0) + 1);
38
+ }
39
+ }
40
+ const summary = report.summary;
41
+ const usableRate = report.runs.length ? (counts.healthy + counts.degraded) / report.runs.length : null;
42
+ if (summary.total !== report.runs.length || summary.usableRate !== usableRate ||
43
+ outcomes.some(outcome => summary[outcome] !== counts[outcome]) ||
44
+ !Array.isArray(summary.topBlockers) || summary.topBlockers.length !== blockers.size)
45
+ return false;
46
+ const seenBlockers = new Set();
47
+ return summary.topBlockers.every(blocker => {
48
+ if (!blocker || seenBlockers.has(blocker.code) || blockers.get(blocker.code) !== blocker.affectedRuns)
49
+ return false;
50
+ seenBlockers.add(blocker.code);
51
+ return true;
52
+ });
53
+ }
54
+ function terminalSafe(value) {
55
+ return Array.from(value).filter(character => {
56
+ const point = character.codePointAt(0);
57
+ return point >= 32 && !(point >= 127 && point <= 159);
58
+ }).join('');
59
+ }
60
+ export function formatDesignVerifierHistory(report) {
61
+ const { summary } = report;
62
+ const lines = [`Execution history for ${terminalSafe(report.owner)}/${terminalSafe(report.repo)}`,
63
+ `Latest ${summary.total} recorded terminal attempts within ${report.retentionDays} days (limit ${report.limit}).`];
64
+ if (summary.total === 0) {
65
+ lines.push('No recorded attempts in this window. History starts when recording is deployed.');
66
+ return lines.join('\n');
67
+ }
68
+ lines.push(`Usable evidence: ${Math.round(summary.usableRate * 100)}% of attempts (${summary.healthy + summary.degraded}/${summary.total}).`, `Fully healthy: ${summary.healthy}; partially degraded: ${summary.degraded}; invalid: ${summary.invalid}; failed: ${summary.failed}; unknown: ${summary.unknown}.`, 'Usable includes partial evidence from degraded attempts. This measures execution reliability, not bug-detection accuracy.', '');
69
+ for (const run of report.runs) {
70
+ lines.push(`${run.finishedAt || run.createdAt} PR #${run.pr} ${run.outcome} ${run.admitted ?? '?'} admitted cases`, ` ${run.runId}`);
71
+ }
72
+ if (summary.topBlockers.length) {
73
+ lines.push('', 'Most frequent blockers (attempts affected):');
74
+ for (const blocker of summary.topBlockers.slice(0, 5)) {
75
+ lines.push(` ${terminalSafe(blocker.code)}: ${blocker.affectedRuns}`);
76
+ }
77
+ }
78
+ return lines.join('\n');
79
+ }
@@ -30,6 +30,22 @@ export interface DesignVerifierAgentCase {
30
30
  errors: string[];
31
31
  } | null;
32
32
  }
33
+ export interface DesignVerifierRunHealthLoss {
34
+ code: string;
35
+ owner: 'harness' | 'case';
36
+ impact: 'degraded' | 'invalid';
37
+ count: number;
38
+ caseIds: string[];
39
+ obligationIds: string[];
40
+ slotIds: string[];
41
+ sides: string[];
42
+ }
43
+ export interface DesignVerifierRunHealth {
44
+ version: 'dv-run-health-v1';
45
+ status: 'healthy' | 'degraded' | 'invalid';
46
+ admitted: number;
47
+ losses: DesignVerifierRunHealthLoss[];
48
+ }
33
49
  export interface DesignVerifierAgentReport {
34
50
  ok: boolean;
35
51
  status: 'complete' | 'failed';
@@ -47,6 +63,7 @@ export interface DesignVerifierAgentReport {
47
63
  timings: DesignVerifierTimingSummary | null;
48
64
  changedFiles: string[];
49
65
  cases: DesignVerifierAgentCase[];
66
+ runHealth: DesignVerifierRunHealth | null;
50
67
  inspectCommand: string;
51
68
  replay: Record<string, unknown> | null;
52
69
  error: string | null;
@@ -30,6 +30,54 @@ function universeSummaries(value, caseId) {
30
30
  ['control-before', 'before', 'after'].includes(universe.role) &&
31
31
  universe.lease && typeof universe.lease.available === 'boolean');
32
32
  }
33
+ function runHealthOf(value) {
34
+ if (!value || typeof value !== 'object' || Array.isArray(value))
35
+ return null;
36
+ const health = value;
37
+ if (health.version !== 'dv-run-health-v1' ||
38
+ !['healthy', 'degraded', 'invalid'].includes(health.status) ||
39
+ !Number.isInteger(health.admitted) || health.admitted < 0 ||
40
+ !Array.isArray(health.losses))
41
+ return null;
42
+ const losses = health.losses.filter((loss) => loss && typeof loss === 'object' &&
43
+ typeof loss.code === 'string' && loss.code.length > 0 &&
44
+ ['harness', 'case'].includes(loss.owner) &&
45
+ ['degraded', 'invalid'].includes(loss.impact) &&
46
+ Number.isInteger(loss.count) && loss.count > 0 &&
47
+ [loss.caseIds, loss.obligationIds, loss.slotIds, loss.sides]
48
+ .every(items => Array.isArray(items) && items.every(item => typeof item === 'string')));
49
+ if (losses.length !== health.losses.length)
50
+ return null;
51
+ const invalidHarnessLoss = losses.some(loss => loss.owner === 'harness' && loss.impact === 'invalid');
52
+ const expectedStatus = health.admitted === 0 || invalidHarnessLoss
53
+ ? 'invalid'
54
+ : losses.length > 0 ? 'degraded' : 'healthy';
55
+ if (health.status !== expectedStatus)
56
+ return null;
57
+ return {
58
+ version: health.version,
59
+ status: health.status,
60
+ admitted: health.admitted,
61
+ losses,
62
+ };
63
+ }
64
+ function missingRunHealth() {
65
+ return {
66
+ version: 'dv-run-health-v1',
67
+ status: 'invalid',
68
+ admitted: 0,
69
+ losses: [{
70
+ code: 'run-health-missing',
71
+ owner: 'harness',
72
+ impact: 'invalid',
73
+ count: 1,
74
+ caseIds: [],
75
+ obligationIds: [],
76
+ slotIds: [],
77
+ sides: [],
78
+ }],
79
+ };
80
+ }
33
81
  export function buildDesignVerifierAgentReport(run) {
34
82
  const repository = `${run.owner}/${run.repo}`;
35
83
  if (run.status !== 'succeeded' || !run.result) {
@@ -47,6 +95,7 @@ export function buildDesignVerifierAgentReport(run) {
47
95
  timings: null,
48
96
  changedFiles: [],
49
97
  cases: [],
98
+ runHealth: null,
50
99
  inspectCommand: `haystack design-verify ${repository}#${run.pr} --run ${run.runId}`,
51
100
  replay: null,
52
101
  error: run.error,
@@ -55,6 +104,7 @@ export function buildDesignVerifierAgentReport(run) {
55
104
  }
56
105
  const verdict = run.result.verdict || {};
57
106
  const summary = verdict.summary || {};
107
+ const runHealth = runHealthOf(verdict.runHealth) || missingRunHealth();
58
108
  const findings = Array.isArray(verdict.findings) ? verdict.findings : [];
59
109
  const confirmations = Array.isArray(verdict.confirmations) ? verdict.confirmations : [];
60
110
  const findingsByCase = new Map();
@@ -133,7 +183,19 @@ export function buildDesignVerifierAgentReport(run) {
133
183
  let headline;
134
184
  let explanation;
135
185
  let exitCode;
136
- if (findings.length > 0) {
186
+ if (runHealth.status === 'invalid') {
187
+ agentVerdict = 'inconclusive';
188
+ headline = 'VERIFICATION INCONCLUSIVE';
189
+ explanation = 'No admissible evidence — this is not a pass.';
190
+ exitCode = 2;
191
+ }
192
+ else if (runHealth.status === 'degraded') {
193
+ agentVerdict = 'inconclusive';
194
+ headline = 'VERIFICATION INCONCLUSIVE';
195
+ explanation = 'Evidence losses degraded this run; inspect the loss table before drawing a conclusion.';
196
+ exitCode = 2;
197
+ }
198
+ else if (findings.length > 0) {
137
199
  agentVerdict = 'anomaly_candidate';
138
200
  headline = 'ANOMALY CANDIDATE';
139
201
  explanation = findingExplanation(String(findings[0]?.kind || ''));
@@ -171,6 +233,7 @@ export function buildDesignVerifierAgentReport(run) {
171
233
  timings: run.result.timings || null,
172
234
  changedFiles: run.result.changedFiles,
173
235
  cases,
236
+ runHealth,
174
237
  inspectCommand: `haystack design-verify ${repository}#${run.pr} --run ${run.runId}`,
175
238
  replay: run.result.replay,
176
239
  error: null,
@@ -243,6 +306,22 @@ export function formatDesignVerifierAgentReport(report) {
243
306
  lines.push(` orchestration/finalization not recorded in a phase: ${preciseDurationLabel(report.timings.unattributedMs)}`);
244
307
  }
245
308
  }
309
+ if (report.runHealth && report.runHealth.status !== 'healthy') {
310
+ lines.push('', `Run health: ${report.runHealth.status}; admitted evidence: ${report.runHealth.admitted}`);
311
+ if (report.runHealth.losses.length > 0) {
312
+ lines.push('Evidence loss table:', ' owner impact count loss / stable identifiers');
313
+ for (const loss of report.runHealth.losses) {
314
+ const identifiers = [
315
+ ...loss.caseIds,
316
+ ...loss.obligationIds,
317
+ ...loss.slotIds,
318
+ ...loss.sides,
319
+ ];
320
+ lines.push(` ${loss.owner.padEnd(7)} ${loss.impact.padEnd(9)} ${String(loss.count).padStart(5)} ` +
321
+ `${loss.code}${identifiers.length ? `: ${identifiers.join(', ')}` : ''}`);
322
+ }
323
+ }
324
+ }
246
325
  for (const testCase of report.cases) {
247
326
  lines.push('', `Test: ${testCase.title}`);
248
327
  if (testCase.story)
package/dist/utils/git.js CHANGED
@@ -386,7 +386,11 @@ function gitCommandOptions(options) {
386
386
  /** True if `ref` resolves to a commit in the current repo. */
387
387
  function gitRefResolves(ref, options = {}) {
388
388
  try {
389
- execFileSync('git', ['rev-parse', '--verify', '--quiet', '--', `${ref}^{commit}`], {
389
+ // `--end-of-options`, not `--`: rev-parse treats everything after `--` as
390
+ // a pathspec, so `-- refs/remotes/origin/main^{commit}` exits 1 for every
391
+ // ref and the caller fell back to a bare local `main` (which a fresh clone
392
+ // does not have). Verified on git 2.39: `--` rc=1, `--end-of-options` rc=0.
393
+ execFileSync('git', ['rev-parse', '--verify', '--quiet', '--end-of-options', `${ref}^{commit}`], {
390
394
  ...gitCommandOptions(options),
391
395
  });
392
396
  return true;
@@ -103,7 +103,10 @@ export function trackError(errorType, metadata) {
103
103
  method: 'POST',
104
104
  headers: { 'Content-Type': 'application/json' },
105
105
  body: JSON.stringify(event),
106
- }).catch(() => { });
106
+ }).catch(() => {
107
+ // Documented fire-and-forget product telemetry: delivery cannot affect
108
+ // the CLI command being measured.
109
+ });
107
110
  }
108
111
  catch {
109
112
  // Telemetry must never crash the CLI
@@ -135,7 +138,10 @@ export function trackSetupEvent(step, metadata) {
135
138
  method: 'POST',
136
139
  headers: { 'Content-Type': 'application/json' },
137
140
  body: JSON.stringify(event),
138
- }).catch(() => { });
141
+ }).catch(() => {
142
+ // Documented fire-and-forget product telemetry: delivery cannot affect
143
+ // the CLI command being measured.
144
+ });
139
145
  }
140
146
  catch {
141
147
  // Telemetry must never crash the CLI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.15.29",
3
+ "version": "0.15.31",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {
@@ -69,57 +69,99 @@
69
69
  "type": "string"
70
70
  },
71
71
  "result": {
72
+ "oneOf": [
73
+ {
74
+ "type": "object",
75
+ "additionalProperties": false,
76
+ "required": [
77
+ "verdict",
78
+ "summary",
79
+ "casesTotal",
80
+ "casesFailed",
81
+ "regressions",
82
+ "outputLocation",
83
+ "wallSeconds"
84
+ ],
85
+ "properties": {
86
+ "verdict": {
87
+ "type": "string"
88
+ },
89
+ "summary": {
90
+ "type": "string"
91
+ },
92
+ "casesTotal": {
93
+ "type": "integer",
94
+ "minimum": 0
95
+ },
96
+ "casesFailed": {
97
+ "type": "integer",
98
+ "minimum": 0
99
+ },
100
+ "regressions": {
101
+ "type": "array",
102
+ "items": {
103
+ "type": "object",
104
+ "additionalProperties": false,
105
+ "required": [
106
+ "caseId",
107
+ "title"
108
+ ],
109
+ "properties": {
110
+ "caseId": {
111
+ "type": "string"
112
+ },
113
+ "title": {
114
+ "type": "string"
115
+ }
116
+ }
117
+ }
118
+ },
119
+ "outputLocation": {
120
+ "type": "string"
121
+ },
122
+ "wallSeconds": {
123
+ "type": "number",
124
+ "minimum": 0
125
+ }
126
+ }
127
+ },
128
+ {
129
+ "type": "object",
130
+ "description": "Opaque VerifyResultV1 document, validated by the hosted service against the canonical P0 contract.",
131
+ "required": [
132
+ "version",
133
+ "provider"
134
+ ],
135
+ "properties": {
136
+ "version": {
137
+ "const": "dv-verify-result-v1"
138
+ },
139
+ "provider": {
140
+ "const": "haystack"
141
+ }
142
+ }
143
+ }
144
+ ]
145
+ },
146
+ "provider": {
147
+ "const": "haystack"
148
+ },
149
+ "version": {
150
+ "const": "dv-cloud-verifier-fleet-runner-projection-v1"
151
+ },
152
+ "progress": {
72
153
  "type": "object",
73
- "additionalProperties": false,
154
+ "description": "Opaque VerifyProgressV1 document, validated by the hosted service against the canonical P0 contract.",
74
155
  "required": [
75
- "verdict",
76
- "summary",
77
- "casesTotal",
78
- "casesFailed",
79
- "regressions",
80
- "outputLocation",
81
- "wallSeconds"
156
+ "version",
157
+ "publicRunId"
82
158
  ],
83
159
  "properties": {
84
- "verdict": {
85
- "type": "string"
160
+ "version": {
161
+ "const": "dv-verify-progress-v1"
86
162
  },
87
- "summary": {
163
+ "publicRunId": {
88
164
  "type": "string"
89
- },
90
- "casesTotal": {
91
- "type": "integer",
92
- "minimum": 0
93
- },
94
- "casesFailed": {
95
- "type": "integer",
96
- "minimum": 0
97
- },
98
- "regressions": {
99
- "type": "array",
100
- "items": {
101
- "type": "object",
102
- "additionalProperties": false,
103
- "required": [
104
- "caseId",
105
- "title"
106
- ],
107
- "properties": {
108
- "caseId": {
109
- "type": "string"
110
- },
111
- "title": {
112
- "type": "string"
113
- }
114
- }
115
- }
116
- },
117
- "outputLocation": {
118
- "type": "string"
119
- },
120
- "wallSeconds": {
121
- "type": "number",
122
- "minimum": 0
123
165
  }
124
166
  }
125
167
  }
@@ -157,6 +199,27 @@
157
199
  }
158
200
  ]
159
201
  }
202
+ },
203
+ {
204
+ "if": {
205
+ "properties": {
206
+ "provider": {
207
+ "const": "haystack"
208
+ }
209
+ },
210
+ "required": [
211
+ "provider"
212
+ ]
213
+ },
214
+ "then": {
215
+ "required": [
216
+ "version",
217
+ "repository",
218
+ "base_commit",
219
+ "head_commit",
220
+ "created_at"
221
+ ]
222
+ }
160
223
  }
161
224
  ]
162
225
  }