@haystackeditor/cli 0.15.21 → 0.15.22

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 CHANGED
@@ -33,8 +33,8 @@ Most consumers of this CLI are coding agents. These invariants hold everywhere:
33
33
  - **Every JSON payload is versioned and schema'd.** Payloads carry
34
34
  `schema_version`; print the contract with `haystack schema <name>`
35
35
  (`haystack schema` lists all: `triage`, `pr`, `pr-status`, `inbox`, `ask`,
36
- `traces`, `submit`, `action`, `error`, `setup`). Schemas are JSON-Schema
37
- 2020-12 and CI-guarded against drift.
36
+ `traces`, `submit`, `action`, `cloud-verifier`, `error`, `setup`). Schemas
37
+ are JSON-Schema 2020-12 and CI-guarded against drift.
38
38
  - **One state vocabulary.** All state tokens are snake_case across every
39
39
  command: verdicts are `good_to_merge` / `needs_review` / `needs_input`;
40
40
  feed buckets are `analyzing`, `good_to_merge`, `issues`, `needs_assignment`,
@@ -79,6 +79,32 @@ haystack verify refork latest 3 attempt:fix-2
79
79
  haystack verify reopen archil:<disk-id>:branch:<returned-child>
80
80
  ```
81
81
 
82
+ **`haystack verify hosted`** is the authenticated production entry point for
83
+ exact GitHub commit pairs. It uses the repository-default saved account,
84
+ submits an idempotent run, and waits up to 35 minutes by default:
85
+
86
+ ```bash
87
+ haystack verify hosted start owner/repo \
88
+ --base <40-character-commit-sha> \
89
+ --head <40-character-commit-sha> \
90
+ --json
91
+ ```
92
+
93
+ Use `--no-wait` to return as soon as the workflow is queued. The result includes
94
+ an account-bound `next_command`; use it verbatim on machines with multiple
95
+ saved GitHub accounts. Repeating the same repository/base/head tuple reuses the
96
+ same production run by default. Pass a new explicit `--idempotency-key` only
97
+ when a fresh execution is intentional. A bounded wait that expires still emits
98
+ the `cloud-verifier` schema with `timed_out: true`, the last observed status,
99
+ and `next_command`, then exits 2. Read or resume a known run with:
100
+
101
+ ```bash
102
+ haystack verify hosted status cv_<48-lowercase-hex-characters> \
103
+ --account <login> \
104
+ --wait \
105
+ --json
106
+ ```
107
+
82
108
  **`haystack setup --json`** speaks NDJSON: events (`question`, `permission`,
83
109
  `progress`, `result`) on stdout, replies on stdin keyed by
84
110
  `requestID`/`permissionID`. `haystack schema setup` documents the full
@@ -41,15 +41,31 @@ export interface UniverseRef {
41
41
  }
42
42
  export interface RiskCell {
43
43
  riskCellId: string;
44
+ riskSourceId?: string;
44
45
  ordinal: number;
45
46
  title: string;
46
47
  rationale?: string;
47
48
  observableConsequence?: string;
48
49
  activationConditions?: string[];
49
50
  causalPath?: string[];
51
+ capabilityId?: string;
50
52
  status: string;
51
53
  severity?: string;
52
54
  confidence?: number;
55
+ sourceEvidence?: Array<{
56
+ evidenceId: string;
57
+ file: string;
58
+ line?: number;
59
+ excerpt?: string;
60
+ relationship: string;
61
+ }>;
62
+ oracles?: Array<{
63
+ oracleId: string;
64
+ kind: string;
65
+ description?: string;
66
+ measurement: string;
67
+ threshold?: string;
68
+ }>;
53
69
  durationMs?: number;
54
70
  base: UniverseRef;
55
71
  head: UniverseRef;
@@ -114,6 +130,7 @@ export interface VerificationRun {
114
130
  command: string;
115
131
  createdAt: string;
116
132
  updatedAt: string;
133
+ planSourceRunId?: string;
117
134
  chapters: VerificationChapter[];
118
135
  events: VerificationRunEvent[];
119
136
  summary: {
@@ -139,9 +156,15 @@ export declare class VerifyUnauthorizedError extends Error {
139
156
  export declare function discoverVerifyRoot(explicit?: string): string | null;
140
157
  export declare function runsRoot(repoRoot: string): string;
141
158
  export declare function runDir(repoRoot: string, runId: string): string;
159
+ /**
160
+ * Read when a run began from the record itself. updatedAt and mtime both move
161
+ * when cleanup or a migration rewrites old runs and therefore cannot define
162
+ * "latest".
163
+ */
164
+ export declare function runStartedAtMs(runJsonPath: string): number | null;
142
165
  export declare function listRunDirs(repoRoot: string): Array<{
143
166
  runId: string;
144
- changedAt: number;
167
+ startedAt: number;
145
168
  }>;
146
169
  /** Same alias rules as the control plane: run id verbatim, `latest`, or a chapter id. */
147
170
  export declare function resolveRunIdOnDisk(repoRoot: string, requested: string): string;
@@ -179,6 +202,7 @@ export interface RunListRow {
179
202
  chapters: string[];
180
203
  status: string;
181
204
  age: string | null;
205
+ created_at?: string;
182
206
  updated_at: string;
183
207
  tests: number;
184
208
  passed: number;
@@ -400,6 +424,7 @@ export interface WatchCallbacks {
400
424
  export interface WatchResult {
401
425
  loaded: LoadedRun;
402
426
  timedOut: boolean;
427
+ endReason: 'terminal' | 'stale' | 'timeout';
403
428
  }
404
429
  /**
405
430
  * Poll until the run reaches a terminal state. The terminal predicate is the
@@ -72,6 +72,36 @@ export function runsRoot(repoRoot) {
72
72
  export function runDir(repoRoot, runId) {
73
73
  return path.join(runsRoot(repoRoot), runId);
74
74
  }
75
+ function isMissingFileError(error) {
76
+ return typeof error === 'object'
77
+ && error !== null
78
+ && 'code' in error
79
+ && error.code === 'ENOENT';
80
+ }
81
+ function runRecordReadError(runJsonPath, error) {
82
+ return new Error(`Could not read verifier run record ${runJsonPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
83
+ }
84
+ /**
85
+ * Read when a run began from the record itself. updatedAt and mtime both move
86
+ * when cleanup or a migration rewrites old runs and therefore cannot define
87
+ * "latest".
88
+ */
89
+ export function runStartedAtMs(runJsonPath) {
90
+ try {
91
+ const run = JSON.parse(fs.readFileSync(runJsonPath, 'utf8'));
92
+ if (typeof run.createdAt === 'string') {
93
+ const recorded = Date.parse(run.createdAt);
94
+ if (Number.isFinite(recorded))
95
+ return recorded;
96
+ }
97
+ }
98
+ catch (error) {
99
+ if (isMissingFileError(error))
100
+ return null;
101
+ throw runRecordReadError(runJsonPath, error);
102
+ }
103
+ return null;
104
+ }
75
105
  export function listRunDirs(repoRoot) {
76
106
  let entries;
77
107
  try {
@@ -82,16 +112,12 @@ export function listRunDirs(repoRoot) {
82
112
  }
83
113
  return entries
84
114
  .filter(name => name.startsWith('run-'))
85
- .map(name => {
86
- try {
87
- return { runId: name, changedAt: fs.statSync(path.join(runsRoot(repoRoot), name, 'run.json')).mtimeMs };
88
- }
89
- catch {
90
- return { runId: name, changedAt: 0 };
91
- }
92
- })
93
- .filter(entry => entry.changedAt > 0)
94
- .sort((a, b) => b.changedAt - a.changedAt);
115
+ .map(name => ({
116
+ runId: name,
117
+ startedAt: runStartedAtMs(path.join(runsRoot(repoRoot), name, 'run.json')),
118
+ }))
119
+ .filter((entry) => (entry.startedAt !== null))
120
+ .sort((a, b) => b.startedAt - a.startedAt);
95
121
  }
96
122
  /** Same alias rules as the control plane: run id verbatim, `latest`, or a chapter id. */
97
123
  export function resolveRunIdOnDisk(repoRoot, requested) {
@@ -102,11 +128,14 @@ export function resolveRunIdOnDisk(repoRoot, requested) {
102
128
  return match?.runId ?? requested;
103
129
  }
104
130
  export function loadRunFromDisk(repoRoot, runId) {
131
+ const runJsonPath = path.join(runDir(repoRoot, runId), 'run.json');
105
132
  try {
106
- return JSON.parse(fs.readFileSync(path.join(runDir(repoRoot, runId), 'run.json'), 'utf8'));
133
+ return JSON.parse(fs.readFileSync(runJsonPath, 'utf8'));
107
134
  }
108
- catch {
109
- return null;
135
+ catch (error) {
136
+ if (isMissingFileError(error))
137
+ return null;
138
+ throw runRecordReadError(runJsonPath, error);
110
139
  }
111
140
  }
112
141
  export async function fetchRunFromServer(server, idOrAlias) {
@@ -158,12 +187,13 @@ function rowOfRun(run) {
158
187
  run_id: run.runId,
159
188
  chapters: run.chapters.map(chapter => chapter.chapterId),
160
189
  status: run.status,
161
- age: formatAge(run.updatedAt),
190
+ age: formatAge(run.createdAt),
191
+ created_at: run.createdAt,
162
192
  updated_at: run.updatedAt,
163
193
  tests: run.summary.total,
164
194
  passed: run.summary.passed,
165
195
  failed: run.summary.failed,
166
- could_not_run: run.summary.infraErrors,
196
+ could_not_run: run.summary.infraErrors + (run.summary.inconclusive ?? 0),
167
197
  culprit_sha: bisect?.culpritSha ?? null,
168
198
  };
169
199
  }
@@ -184,7 +214,10 @@ export async function listRunRows(options) {
184
214
  const body = (await response.json());
185
215
  if (Array.isArray(body.runs)) {
186
216
  // Recompute ages locally so they do not drift with server response caching.
187
- const rows = body.runs.map(row => ({ ...row, age: formatAge(row.updated_at) ?? row.age }));
217
+ const rows = body.runs.map(row => ({
218
+ ...row,
219
+ age: formatAge(row.created_at || row.updated_at) ?? row.age,
220
+ }));
188
221
  return { rows, source: 'server', total: body.total ?? rows.length };
189
222
  }
190
223
  }
@@ -504,7 +537,7 @@ export function summarizeRun(loaded, server) {
504
537
  source: loaded.source,
505
538
  created_at: run.createdAt,
506
539
  updated_at: run.updatedAt,
507
- age: formatAge(run.updatedAt),
540
+ age: formatAge(run.createdAt),
508
541
  counts: {
509
542
  total: run.summary.total,
510
543
  completed: run.summary.completed,
@@ -729,8 +762,9 @@ export async function waitForTerminal(options) {
729
762
  }
730
763
  first = false;
731
764
  const staleMarked = loaded.stale || loaded.run.events.some(event => event.eventId.startsWith('evt-stale-'));
732
- if (isTerminal(loaded.run) && !staleMarked)
733
- return { loaded, timedOut: false };
765
+ if (isTerminal(loaded.run) && !staleMarked) {
766
+ return { loaded, timedOut: false, endReason: 'terminal' };
767
+ }
734
768
  const progressed = loaded.run.updatedAt !== lastUpdatedAt;
735
769
  lastUpdatedAt = loaded.run.updatedAt;
736
770
  if (staleMarked) {
@@ -739,15 +773,17 @@ export async function waitForTerminal(options) {
739
773
  }
740
774
  else {
741
775
  noProgressSince = noProgressSince ?? Date.now();
742
- if (Date.now() - noProgressSince > staleGraceMs)
743
- return { loaded, timedOut: false };
776
+ if (Date.now() - noProgressSince > staleGraceMs) {
777
+ return { loaded, timedOut: false, endReason: 'stale' };
778
+ }
744
779
  }
745
780
  }
746
781
  else {
747
782
  noProgressSince = null;
748
783
  }
749
- if (deadline && Date.now() > deadline)
750
- return { loaded, timedOut: true };
784
+ if (deadline && Date.now() > deadline) {
785
+ return { loaded, timedOut: true, endReason: 'timeout' };
786
+ }
751
787
  await new Promise(resolve => setTimeout(resolve, intervalMs));
752
788
  }
753
789
  }
@@ -0,0 +1,60 @@
1
+ export interface HostedStartOptions {
2
+ base: string;
3
+ head: string;
4
+ idempotencyKey?: string;
5
+ account?: string;
6
+ wait?: boolean;
7
+ interval?: string;
8
+ timeout?: string;
9
+ json?: boolean;
10
+ }
11
+ export interface HostedStatusOptions {
12
+ account?: string;
13
+ wait?: boolean;
14
+ interval?: string;
15
+ timeout?: string;
16
+ json?: boolean;
17
+ }
18
+ interface HostedRunState {
19
+ runId: string;
20
+ status: string;
21
+ repository?: string;
22
+ baseCommit?: string;
23
+ headCommit?: string;
24
+ createdAt?: string;
25
+ completedAt?: string;
26
+ output?: Record<string, unknown>;
27
+ error?: string;
28
+ }
29
+ interface HostedPollOptions {
30
+ intervalMs: number;
31
+ timeoutMs: number;
32
+ onStatus?: (state: HostedRunState) => void;
33
+ }
34
+ interface HostedWaitResult {
35
+ state: HostedRunState;
36
+ timedOut: boolean;
37
+ }
38
+ export declare function parseHostedRunState(value: unknown, expectedRunId?: string): HostedRunState;
39
+ export declare function parseHostedRepository(value: string): {
40
+ owner: string;
41
+ repository: string;
42
+ fullName: string;
43
+ };
44
+ export declare function defaultHostedIdempotencyKey(repository: string, baseCommit: string, headCommit: string): string;
45
+ export declare function startHostedVerification(repositoryArg: string, options: HostedStartOptions, token: string): Promise<{
46
+ state: HostedRunState;
47
+ idempotencyKey: string;
48
+ }>;
49
+ export declare function getHostedVerification(runId: string, token: string): Promise<HostedRunState>;
50
+ export declare function waitForHostedVerification(runId: string, token: string, options: HostedPollOptions): Promise<HostedWaitResult>;
51
+ export declare function buildHostedJson(operation: 'start' | 'status', state: HostedRunState, extras?: {
52
+ account?: string;
53
+ idempotencyKey?: string;
54
+ nextCommand?: string;
55
+ timedOut?: boolean;
56
+ }): Record<string, unknown>;
57
+ export declare function hostedResumeCommand(runId: string, account?: string): string;
58
+ export declare function verifyHostedStartCommand(repositoryArg: string, options: HostedStartOptions): Promise<void>;
59
+ export declare function verifyHostedStatusCommand(runId: string, options: HostedStatusOptions): Promise<void>;
60
+ export {};
@@ -0,0 +1,342 @@
1
+ import { createHash } from 'node:crypto';
2
+ import chalk from 'chalk';
3
+ import { withSchema } from '../schema.js';
4
+ import { resolveAuthContext } from '../utils/auth.js';
5
+ import { HaystackApiError, haystackJson } from '../utils/haystack-api.js';
6
+ const REPOSITORY_COMPONENT = /^[A-Za-z0-9_.-]{1,100}$/;
7
+ const COMMIT_SHA = /^[0-9a-f]{40}$/;
8
+ const IDEMPOTENCY_KEY = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
9
+ const RUN_ID = /^cv_[0-9a-f]{48}$/;
10
+ const STATUS = /^[a-z][a-z0-9_-]{1,31}$/;
11
+ const TERMINAL_STATUSES = new Set(['complete', 'errored', 'expired', 'terminated']);
12
+ const DEFAULT_INTERVAL_SECONDS = 5;
13
+ const DEFAULT_TIMEOUT_MINUTES = 35;
14
+ function requireRecord(value, context) {
15
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
16
+ throw new Error(`${context} returned an invalid response object.`);
17
+ }
18
+ return value;
19
+ }
20
+ function requireString(value, field, pattern) {
21
+ const candidate = value[field];
22
+ if (typeof candidate !== 'string'
23
+ || candidate.length === 0
24
+ || (pattern && !pattern.test(candidate))) {
25
+ throw new Error(`Hosted verifier response is missing a valid ${field}.`);
26
+ }
27
+ return candidate;
28
+ }
29
+ function optionalString(value, field) {
30
+ const candidate = value[field];
31
+ if (candidate === undefined)
32
+ return undefined;
33
+ if (typeof candidate !== 'string' || candidate.length === 0) {
34
+ throw new Error(`Hosted verifier response has an invalid ${field}.`);
35
+ }
36
+ return candidate;
37
+ }
38
+ function optionalTimestamp(value, field) {
39
+ const candidate = optionalString(value, field);
40
+ if (candidate === undefined)
41
+ return undefined;
42
+ const parsed = new Date(candidate);
43
+ if (!Number.isFinite(parsed.getTime())
44
+ || parsed.toISOString() !== candidate) {
45
+ throw new Error(`Hosted verifier response has an invalid ${field}.`);
46
+ }
47
+ return candidate;
48
+ }
49
+ export function parseHostedRunState(value, expectedRunId) {
50
+ const response = requireRecord(value, 'Hosted verifier');
51
+ const runId = requireString(response, 'runId', RUN_ID);
52
+ if (expectedRunId && runId !== expectedRunId) {
53
+ throw new Error(`Hosted verifier returned run ${runId} while status for ${expectedRunId} was requested.`);
54
+ }
55
+ const status = requireString(response, 'status', STATUS);
56
+ const repository = optionalString(response, 'repository');
57
+ if (repository) {
58
+ const parts = repository.split('/');
59
+ if (parts.length !== 2
60
+ || !parts.every(part => REPOSITORY_COMPONENT.test(part))) {
61
+ throw new Error('Hosted verifier response has an invalid repository.');
62
+ }
63
+ }
64
+ const baseCommit = optionalString(response, 'baseCommit');
65
+ const headCommit = optionalString(response, 'headCommit');
66
+ if (baseCommit && !COMMIT_SHA.test(baseCommit)) {
67
+ throw new Error('Hosted verifier response has an invalid baseCommit.');
68
+ }
69
+ if (headCommit && !COMMIT_SHA.test(headCommit)) {
70
+ throw new Error('Hosted verifier response has an invalid headCommit.');
71
+ }
72
+ const outputValue = response.output;
73
+ const output = outputValue === undefined
74
+ ? undefined
75
+ : requireRecord(outputValue, 'Hosted verifier output');
76
+ const createdAt = optionalTimestamp(response, 'createdAt');
77
+ const completedAt = optionalTimestamp(response, 'completedAt');
78
+ if (status === 'complete') {
79
+ if (!repository
80
+ || !baseCommit
81
+ || !headCommit
82
+ || !createdAt
83
+ || !completedAt
84
+ || !output
85
+ || output.runId !== runId
86
+ || output.repository !== repository
87
+ || output.baseCommit !== baseCommit
88
+ || output.headCommit !== headCommit
89
+ || output.sourceOfflineReady !== true
90
+ || output.sandboxDeleted !== true) {
91
+ throw new Error('Hosted verifier returned a complete run without a matching terminal receipt.');
92
+ }
93
+ }
94
+ return {
95
+ runId,
96
+ status,
97
+ repository,
98
+ baseCommit,
99
+ headCommit,
100
+ createdAt,
101
+ completedAt,
102
+ output,
103
+ error: optionalString(response, 'error'),
104
+ };
105
+ }
106
+ export function parseHostedRepository(value) {
107
+ const parts = value.split('/');
108
+ if (parts.length !== 2
109
+ || !parts.every(part => REPOSITORY_COMPONENT.test(part))) {
110
+ throw new Error(`Invalid repository "${value}". Use an exact GitHub owner/repository name.`);
111
+ }
112
+ return {
113
+ owner: parts[0],
114
+ repository: parts[1],
115
+ fullName: value,
116
+ };
117
+ }
118
+ function requireCommit(value, flag) {
119
+ if (typeof value !== 'string' || !COMMIT_SHA.test(value)) {
120
+ throw new Error(`${flag} must be an exact 40-character lowercase commit SHA.`);
121
+ }
122
+ return value;
123
+ }
124
+ export function defaultHostedIdempotencyKey(repository, baseCommit, headCommit) {
125
+ const digest = createHash('sha256')
126
+ .update(JSON.stringify([repository, baseCommit, headCommit]))
127
+ .digest('hex');
128
+ return `cli:${digest}`;
129
+ }
130
+ function requireIdempotencyKey(value) {
131
+ if (!IDEMPOTENCY_KEY.test(value)) {
132
+ throw new Error('--idempotency-key must be 8-128 characters and use only letters, numbers, ".", "_", ":", or "-".');
133
+ }
134
+ return value;
135
+ }
136
+ function parsePositiveNumber(value, fallback, flag, maximum) {
137
+ if (value === undefined)
138
+ return fallback;
139
+ const parsed = Number(value);
140
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > maximum) {
141
+ throw new Error(`${flag} must be a positive number no greater than ${maximum}.`);
142
+ }
143
+ return parsed;
144
+ }
145
+ function pollingOptions(interval, timeout, onStatus) {
146
+ return {
147
+ intervalMs: parsePositiveNumber(interval, DEFAULT_INTERVAL_SECONDS, '--interval', 300) * 1000,
148
+ timeoutMs: parsePositiveNumber(timeout, DEFAULT_TIMEOUT_MINUTES, '--timeout', 1_440) * 60_000,
149
+ onStatus,
150
+ };
151
+ }
152
+ export async function startHostedVerification(repositoryArg, options, token) {
153
+ const repository = parseHostedRepository(repositoryArg);
154
+ const baseCommit = requireCommit(options.base, '--base');
155
+ const headCommit = requireCommit(options.head, '--head');
156
+ const idempotencyKey = requireIdempotencyKey(options.idempotencyKey
157
+ ?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit));
158
+ const response = await haystackJson('/api/agent/cloud-verifier/runs', token, {
159
+ method: 'POST',
160
+ headers: { 'Content-Type': 'application/json' },
161
+ body: JSON.stringify({
162
+ owner: repository.owner,
163
+ repository: repository.repository,
164
+ baseCommit,
165
+ headCommit,
166
+ idempotencyKey,
167
+ }),
168
+ });
169
+ const state = parseHostedRunState(response);
170
+ return {
171
+ state: {
172
+ ...state,
173
+ repository: state.repository ?? repository.fullName,
174
+ baseCommit: state.baseCommit ?? baseCommit,
175
+ headCommit: state.headCommit ?? headCommit,
176
+ },
177
+ idempotencyKey,
178
+ };
179
+ }
180
+ export async function getHostedVerification(runId, token) {
181
+ if (!RUN_ID.test(runId)) {
182
+ throw new Error('Run id must match cv_ followed by 48 lowercase hex characters.');
183
+ }
184
+ let response;
185
+ try {
186
+ response = await haystackJson(`/api/agent/cloud-verifier/runs/${encodeURIComponent(runId)}`, token, {}, [410]);
187
+ }
188
+ catch (error) {
189
+ if (error instanceof HaystackApiError && error.status === 404) {
190
+ throw new Error(`Hosted verifier run ${runId} was not found for the selected account. `
191
+ + 'If the run was started with another saved account, retry with `--account <login>`.');
192
+ }
193
+ throw error;
194
+ }
195
+ return parseHostedRunState(response, runId);
196
+ }
197
+ function delay(milliseconds) {
198
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
199
+ }
200
+ export async function waitForHostedVerification(runId, token, options) {
201
+ const startedAt = Date.now();
202
+ let lastStatus;
203
+ while (true) {
204
+ const state = await getHostedVerification(runId, token);
205
+ if (state.status !== lastStatus) {
206
+ options.onStatus?.(state);
207
+ lastStatus = state.status;
208
+ }
209
+ if (TERMINAL_STATUSES.has(state.status)) {
210
+ return { state, timedOut: false };
211
+ }
212
+ const elapsed = Date.now() - startedAt;
213
+ if (elapsed >= options.timeoutMs) {
214
+ return { state, timedOut: true };
215
+ }
216
+ await delay(Math.min(options.intervalMs, options.timeoutMs - elapsed));
217
+ }
218
+ }
219
+ export function buildHostedJson(operation, state, extras = {}) {
220
+ return withSchema('cloud-verifier', {
221
+ operation,
222
+ run_id: state.runId,
223
+ status: state.status,
224
+ ...(state.repository ? { repository: state.repository } : {}),
225
+ ...(state.baseCommit ? { base_commit: state.baseCommit } : {}),
226
+ ...(state.headCommit ? { head_commit: state.headCommit } : {}),
227
+ ...(state.createdAt ? { created_at: state.createdAt } : {}),
228
+ ...(state.completedAt ? { completed_at: state.completedAt } : {}),
229
+ ...(extras.account ? { account: extras.account } : {}),
230
+ ...(extras.idempotencyKey
231
+ ? { idempotency_key: extras.idempotencyKey }
232
+ : {}),
233
+ ...(extras.nextCommand ? { next_command: extras.nextCommand } : {}),
234
+ ...(extras.timedOut !== undefined ? { timed_out: extras.timedOut } : {}),
235
+ ...(state.output ? { output: state.output } : {}),
236
+ ...(state.error ? { error: state.error } : {}),
237
+ });
238
+ }
239
+ export function hostedResumeCommand(runId, account) {
240
+ return `haystack verify hosted status ${runId}${account ? ` --account ${account}` : ''} --wait`;
241
+ }
242
+ function printHostedState(operation, state, options) {
243
+ const nextCommand = TERMINAL_STATUSES.has(state.status)
244
+ ? undefined
245
+ : hostedResumeCommand(state.runId, options.account);
246
+ if (options.json) {
247
+ process.stdout.write(`${JSON.stringify(buildHostedJson(operation, state, {
248
+ account: options.account,
249
+ idempotencyKey: options.idempotencyKey,
250
+ nextCommand,
251
+ timedOut: options.timedOut,
252
+ }), null, 2)}\n`);
253
+ return;
254
+ }
255
+ const icon = state.status === 'complete'
256
+ ? chalk.green('✓')
257
+ : TERMINAL_STATUSES.has(state.status)
258
+ ? chalk.red('✗')
259
+ : chalk.cyan('•');
260
+ console.log(`${icon} Hosted verification ${chalk.bold(state.runId)}: ${state.status}`);
261
+ if (state.repository) {
262
+ console.log(` ${chalk.dim('Repository:')} ${state.repository}`);
263
+ }
264
+ if (state.baseCommit && state.headCommit) {
265
+ console.log(` ${chalk.dim('Commits:')} ${state.baseCommit.slice(0, 12)} → ${state.headCommit.slice(0, 12)}`);
266
+ }
267
+ if (state.error)
268
+ console.log(` ${chalk.dim('Error:')} ${state.error}`);
269
+ if (options.timedOut) {
270
+ console.log(chalk.yellow(' Wait timed out; the production run is still active.'));
271
+ }
272
+ if (nextCommand)
273
+ console.log(` ${chalk.dim('Resume:')} ${nextCommand}`);
274
+ }
275
+ function markNonSuccess(state, timedOut = false) {
276
+ if (timedOut
277
+ || (state.status !== 'complete' && TERMINAL_STATUSES.has(state.status))) {
278
+ process.exitCode = 2;
279
+ }
280
+ }
281
+ export async function verifyHostedStartCommand(repositoryArg, options) {
282
+ const repository = parseHostedRepository(repositoryArg);
283
+ const auth = await resolveAuthContext({
284
+ preferredLogin: options.account,
285
+ owner: repository.owner,
286
+ repo: repository.repository,
287
+ });
288
+ if (!options.json) {
289
+ console.error(chalk.dim('Submitting the exact commit pair to the hosted verifier…'));
290
+ }
291
+ const started = await startHostedVerification(repositoryArg, options, auth.token);
292
+ const selectedAccount = auth.token.startsWith('hsk_live_')
293
+ ? undefined
294
+ : auth.login;
295
+ let state = started.state;
296
+ let timedOut;
297
+ if (options.wait !== false && !TERMINAL_STATUSES.has(state.status)) {
298
+ const waited = await waitForHostedVerification(state.runId, auth.token, pollingOptions(options.interval, options.timeout, current => {
299
+ if (!options.json) {
300
+ console.error(chalk.dim(`Hosted verifier: ${current.status}…`));
301
+ }
302
+ }));
303
+ state = waited.state;
304
+ timedOut = waited.timedOut;
305
+ }
306
+ else if (options.wait !== false) {
307
+ timedOut = false;
308
+ }
309
+ printHostedState('start', state, {
310
+ json: options.json,
311
+ account: selectedAccount,
312
+ idempotencyKey: started.idempotencyKey,
313
+ timedOut,
314
+ });
315
+ markNonSuccess(state, timedOut);
316
+ }
317
+ export async function verifyHostedStatusCommand(runId, options) {
318
+ const auth = await resolveAuthContext({ preferredLogin: options.account });
319
+ const selectedAccount = auth.token.startsWith('hsk_live_')
320
+ ? undefined
321
+ : auth.login;
322
+ let state = await getHostedVerification(runId, auth.token);
323
+ let timedOut;
324
+ if (options.wait && !TERMINAL_STATUSES.has(state.status)) {
325
+ const waited = await waitForHostedVerification(runId, auth.token, pollingOptions(options.interval, options.timeout, current => {
326
+ if (!options.json) {
327
+ console.error(chalk.dim(`Hosted verifier: ${current.status}…`));
328
+ }
329
+ }));
330
+ state = waited.state;
331
+ timedOut = waited.timedOut;
332
+ }
333
+ else if (options.wait) {
334
+ timedOut = false;
335
+ }
336
+ printHostedState('status', state, {
337
+ json: options.json,
338
+ account: selectedAccount,
339
+ timedOut,
340
+ });
341
+ markNonSuccess(state, timedOut);
342
+ }