@haystackeditor/cli 0.15.31 → 0.16.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.
Files changed (133) hide show
  1. package/README.md +111 -60
  2. package/dist/assets/hooks/scripts/commit-msg.sh +3 -0
  3. package/dist/assets/hooks/scripts/post-commit.sh +3 -0
  4. package/dist/assets/hooks/scripts/pre-commit.sh +11 -6
  5. package/dist/assets/hooks/scripts/pre-push.sh +3 -0
  6. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
  7. package/dist/commands/case-batch-contract.js +608 -0
  8. package/dist/commands/case-batch.js +713 -0
  9. package/dist/commands/cloud-verifier-identity-census.js +5 -2
  10. package/dist/commands/combination-search-hook.js +139 -0
  11. package/dist/commands/dismiss.js +2 -1
  12. package/dist/commands/hooks.js +66 -7
  13. package/dist/commands/install-session-hooks.js +131 -44
  14. package/dist/commands/policy.js +32 -45
  15. package/dist/commands/precompute-delivery.js +6 -1
  16. package/dist/commands/scaffold-provisional-universe.js +8 -10
  17. package/dist/commands/setup.js +32 -10
  18. package/dist/commands/submit.js +7 -2
  19. package/dist/commands/telemetry.js +17 -2
  20. package/dist/commands/triage.js +2 -1
  21. package/dist/commands/verify-explore.js +1 -0
  22. package/dist/commands/verify-hosted-mcp.js +3 -12
  23. package/dist/commands/verify-hosted-reproducibility.js +49 -547
  24. package/dist/commands/verify-hosted.js +66 -168
  25. package/dist/commands/verify-precompute.js +57 -10
  26. package/dist/commands/verify.js +51 -139
  27. package/dist/index.js +238 -350
  28. package/dist/lazy.js +8 -0
  29. package/dist/schema.js +1 -0
  30. package/dist/tools/detect.js +3 -24
  31. package/dist/triage/prompts.js +46 -45
  32. package/dist/triage/runner.js +119 -33
  33. package/dist/types.js +3 -3
  34. package/dist/utils/auth.js +14 -2
  35. package/dist/utils/git.js +60 -29
  36. package/dist/utils/github-api.js +14 -1
  37. package/dist/utils/haystack-api.js +26 -7
  38. package/dist/utils/hooks.js +43 -6
  39. package/dist/utils/prompter.js +25 -10
  40. package/dist/utils/safe-write.js +31 -0
  41. package/dist/utils/secret-paths.js +115 -0
  42. package/dist/utils/secrets.js +0 -1
  43. package/dist/utils/telemetry.js +34 -14
  44. package/dist/utils/update-check.js +151 -0
  45. package/package.json +18 -14
  46. package/schemas/case-batch.v1.json +184 -0
  47. package/schemas/cloud-verifier.v1.json +55 -100
  48. package/dist/commands/ask.d.ts +0 -14
  49. package/dist/commands/cloud-verifier-behaviors.d.ts +0 -27
  50. package/dist/commands/cloud-verifier-data-store-census.d.ts +0 -47
  51. package/dist/commands/cloud-verifier-data-store-drift.d.ts +0 -42
  52. package/dist/commands/cloud-verifier-identity-census.d.ts +0 -88
  53. package/dist/commands/cloud-verifier-materialization.d.ts +0 -16
  54. package/dist/commands/cloud-verifier-pascal-selector-census.d.ts +0 -29
  55. package/dist/commands/cloud-verifier-python-manifest-selector-census.d.ts +0 -27
  56. package/dist/commands/cloud-verifier-specialized-operational-census.d.ts +0 -51
  57. package/dist/commands/cloud-verifier-universe.d.ts +0 -31
  58. package/dist/commands/config.d.ts +0 -46
  59. package/dist/commands/design-verify.d.ts +0 -33
  60. package/dist/commands/dismiss.d.ts +0 -29
  61. package/dist/commands/hooks.d.ts +0 -13
  62. package/dist/commands/inbox.d.ts +0 -65
  63. package/dist/commands/init.d.ts +0 -10
  64. package/dist/commands/install-session-hooks.d.ts +0 -17
  65. package/dist/commands/login.d.ts +0 -8
  66. package/dist/commands/mcp.d.ts +0 -1
  67. package/dist/commands/policy.d.ts +0 -31
  68. package/dist/commands/pr-status.d.ts +0 -144
  69. package/dist/commands/pr.d.ts +0 -40
  70. package/dist/commands/precompute-delivery-contract.d.ts +0 -68
  71. package/dist/commands/precompute-delivery.d.ts +0 -20
  72. package/dist/commands/prepare-universe-review.d.ts +0 -115
  73. package/dist/commands/production-source-deny-policy.d.ts +0 -15
  74. package/dist/commands/request-review.d.ts +0 -26
  75. package/dist/commands/review.d.ts +0 -25
  76. package/dist/commands/rules.d.ts +0 -4
  77. package/dist/commands/scaffold-provisional-universe.d.ts +0 -468
  78. package/dist/commands/schema-cmd.d.ts +0 -2
  79. package/dist/commands/setup.d.ts +0 -28
  80. package/dist/commands/skills.d.ts +0 -8
  81. package/dist/commands/status.d.ts +0 -4
  82. package/dist/commands/submit.d.ts +0 -30
  83. package/dist/commands/system-map.d.ts +0 -42
  84. package/dist/commands/telemetry.d.ts +0 -53
  85. package/dist/commands/tokens.d.ts +0 -14
  86. package/dist/commands/triage.d.ts +0 -35
  87. package/dist/commands/verify-core.d.ts +0 -449
  88. package/dist/commands/verify-core.js +0 -789
  89. package/dist/commands/verify-explore.d.ts +0 -14
  90. package/dist/commands/verify-history.d.ts +0 -14
  91. package/dist/commands/verify-hosted-mcp.d.ts +0 -12
  92. package/dist/commands/verify-hosted-reproducibility.d.ts +0 -90
  93. package/dist/commands/verify-hosted.d.ts +0 -92
  94. package/dist/commands/verify-mcp.d.ts +0 -8
  95. package/dist/commands/verify-mcp.js +0 -517
  96. package/dist/commands/verify-ops.d.ts +0 -158
  97. package/dist/commands/verify-ops.js +0 -1148
  98. package/dist/commands/verify-precompute.d.ts +0 -30
  99. package/dist/commands/verify-reproducibility.d.ts +0 -85
  100. package/dist/commands/verify-reproducibility.js +0 -494
  101. package/dist/commands/verify-reseal.d.ts +0 -9
  102. package/dist/commands/verify-reseal.js +0 -148
  103. package/dist/commands/verify-sandboxes.d.ts +0 -95
  104. package/dist/commands/verify-sandboxes.js +0 -352
  105. package/dist/commands/verify.d.ts +0 -28
  106. package/dist/commands/webhooks.d.ts +0 -30
  107. package/dist/index.d.ts +0 -22
  108. package/dist/schema.d.ts +0 -28
  109. package/dist/states.d.ts +0 -29
  110. package/dist/tools/detect.d.ts +0 -50
  111. package/dist/triage/prompts.d.ts +0 -24
  112. package/dist/triage/runner.d.ts +0 -34
  113. package/dist/triage/types.d.ts +0 -42
  114. package/dist/types/verify-history.d.ts +0 -38
  115. package/dist/types.d.ts +0 -1684
  116. package/dist/utils/action-output.d.ts +0 -24
  117. package/dist/utils/analysis-api.d.ts +0 -187
  118. package/dist/utils/auth.d.ts +0 -79
  119. package/dist/utils/config.d.ts +0 -24
  120. package/dist/utils/design-verifier-api.d.ts +0 -208
  121. package/dist/utils/design-verifier-history.d.ts +0 -46
  122. package/dist/utils/design-verifier-result.d.ts +0 -74
  123. package/dist/utils/detect.d.ts +0 -43
  124. package/dist/utils/git.d.ts +0 -135
  125. package/dist/utils/github-api.d.ts +0 -104
  126. package/dist/utils/haystack-api.d.ts +0 -37
  127. package/dist/utils/hooks.d.ts +0 -12
  128. package/dist/utils/pending-state.d.ts +0 -40
  129. package/dist/utils/pr-ref.d.ts +0 -27
  130. package/dist/utils/prompter.d.ts +0 -85
  131. package/dist/utils/secrets.d.ts +0 -47
  132. package/dist/utils/telemetry.d.ts +0 -19
  133. /package/dist/commands/{precompute-delivery-worker.d.ts → combination-search-hook-contract.js} +0 -0
@@ -160,7 +160,6 @@ function isSafePattern(match) {
160
160
  */
161
161
  export function scanForSecrets(content, filename) {
162
162
  const findings = [];
163
- const lines = content.split('\n');
164
163
  for (const secretPattern of SECRET_PATTERNS) {
165
164
  // Reset regex state
166
165
  secretPattern.pattern.lastIndex = 0;
@@ -7,11 +7,11 @@
7
7
  * All calls are non-blocking and never throw — telemetry failures must not
8
8
  * break CLI commands.
9
9
  */
10
- import { readFileSync } from 'node:fs';
11
- import { dirname, resolve } from 'node:path';
10
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
+ import { dirname, join, resolve } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
- import * as os from 'os';
14
- import { createHash } from 'node:crypto';
13
+ import * as os from 'node:os';
14
+ import { randomBytes } from 'node:crypto';
15
15
  const POSTHOG_HOST = 'https://us.i.posthog.com';
16
16
  const POSTHOG_API_KEY = process.env.HAYSTACK_POSTHOG_KEY || 'phc_7wHrCoMlcwuftLe6N38hPb9wKHb4OK0wNw9ayeRySad';
17
17
  const CURRENT_FILE_DIR = dirname(fileURLToPath(import.meta.url));
@@ -55,25 +55,45 @@ function minimizeMetadata(metadata) {
55
55
  }
56
56
  return minimized;
57
57
  }
58
+ const TELEMETRY_ID_PATTERN = /^[0-9a-f]{24}$/;
59
+ let cachedTelemetryId;
58
60
  /**
59
- * Pseudonymous stable machine identifier for PostHog distinct_id.
60
- * The local username and hostname are never transmitted.
61
- * Never throws — falls back to hostname-only or 'anonymous'.
61
+ * Pseudonymous stable install identifier for PostHog distinct_id: 24 random
62
+ * hex chars generated once and kept in ~/.haystack/telemetry-id (0600). It is
63
+ * not derived from the username or hostname, so it cannot be reversed to
64
+ * them. A missing, unreadable or malformed file gets a fresh id. Never throws.
62
65
  */
63
66
  function getDistinctId() {
67
+ if (cachedTelemetryId)
68
+ return `cli:${cachedTelemetryId}`;
69
+ let idPath;
64
70
  try {
65
- const hostname = os.hostname();
71
+ idPath = join(os.homedir(), '.haystack', 'telemetry-id');
72
+ const stored = readFileSync(idPath, 'utf8').trim();
73
+ if (TELEMETRY_ID_PATTERN.test(stored)) {
74
+ cachedTelemetryId = stored;
75
+ return `cli:${stored}`;
76
+ }
77
+ }
78
+ catch {
79
+ // Missing or unreadable: a new id is generated below.
80
+ cachedTelemetryId = undefined;
81
+ }
82
+ const fresh = randomBytes(12).toString('hex');
83
+ cachedTelemetryId = fresh;
84
+ if (idPath) {
66
85
  try {
67
- const localMachine = `${os.userInfo().username}\0${hostname}`;
68
- return `cli:${createHash('sha256').update(localMachine).digest('hex').slice(0, 24)}`;
86
+ mkdirSync(dirname(idPath), { recursive: true });
87
+ writeFileSync(idPath, `${fresh}\n`, { mode: 0o600 });
88
+ chmodSync(idPath, 0o600);
69
89
  }
70
90
  catch {
71
- return `cli:${createHash('sha256').update(hostname).digest('hex').slice(0, 24)}`;
91
+ // Could not persist (read-only home, etc.): this process keeps the
92
+ // random id and the next run tries again. Telemetry must not break the CLI.
93
+ cachedTelemetryId = fresh;
72
94
  }
73
95
  }
74
- catch {
75
- return 'cli:anonymous';
76
- }
96
+ return `cli:${fresh}`;
77
97
  }
78
98
  /**
79
99
  * Track an error event. Fire-and-forget — never throws.
@@ -0,0 +1,151 @@
1
+ /**
2
+ * "New version available" notice.
3
+ *
4
+ * Runs after a command finishes, never before it starts. At most once per 24
5
+ * hours (timestamp in ~/.haystack/update-check.json, mode 0600) it asks the
6
+ * npm registry for the latest @haystackeditor/cli with a 1.5 second timeout,
7
+ * and prints one dim stderr line when that version is newer than this one.
8
+ *
9
+ * It stays off when stdout is not a TTY, when --json is passed, and when
10
+ * HAYSTACK_NO_UPDATE_CHECK or CI is set, so scripts and agents never see it.
11
+ * Failures print nothing unless DEBUG is set.
12
+ */
13
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { dirname, join } from 'node:path';
16
+ import chalk from 'chalk';
17
+ export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
18
+ export const UPDATE_CHECK_TIMEOUT_MS = 1500;
19
+ export const LATEST_URL = 'https://registry.npmjs.org/@haystackeditor/cli/latest';
20
+ /** Parse `X.Y.Z` or `X.Y.Z-pre`; anything else is not comparable. */
21
+ function parseVersion(version) {
22
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/.exec(version.trim());
23
+ if (!match)
24
+ return null;
25
+ return {
26
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
27
+ prerelease: match[4] !== undefined,
28
+ };
29
+ }
30
+ /**
31
+ * True when `latest` is a strictly newer release than `current`. A prerelease
32
+ * `latest` is never offered. A release with the same core as a prerelease
33
+ * `current` is newer (0.16.0 is newer than 0.16.0-rc.1).
34
+ */
35
+ export function isNewerVersion(latest, current) {
36
+ const l = parseVersion(latest);
37
+ const c = parseVersion(current);
38
+ if (!l || !c || l.prerelease)
39
+ return false;
40
+ for (let i = 0; i < 3; i++) {
41
+ if (l.core[i] !== c.core[i])
42
+ return l.core[i] > c.core[i];
43
+ }
44
+ return c.prerelease;
45
+ }
46
+ /** True when no check has run within the interval (or the clock went backwards). */
47
+ export function isCheckDue(state, now, intervalMs = UPDATE_CHECK_INTERVAL_MS) {
48
+ if (!state)
49
+ return true;
50
+ const elapsed = now - state.lastCheckedAt;
51
+ return elapsed < 0 || elapsed >= intervalMs;
52
+ }
53
+ export function isUpdateCheckEnabled({ argv, env, stdoutIsTTY }) {
54
+ if (!stdoutIsTTY)
55
+ return false;
56
+ if (env.HAYSTACK_NO_UPDATE_CHECK || env.CI)
57
+ return false;
58
+ return !argv.includes('--json');
59
+ }
60
+ export function defaultStatePath() {
61
+ return join(homedir(), '.haystack', 'update-check.json');
62
+ }
63
+ function readState(statePath, debug) {
64
+ let raw;
65
+ try {
66
+ raw = readFileSync(statePath, 'utf8');
67
+ }
68
+ catch (err) {
69
+ if (err.code !== 'ENOENT') {
70
+ debug(`could not read ${statePath}: ${err instanceof Error ? err.message : String(err)}`);
71
+ }
72
+ return null;
73
+ }
74
+ try {
75
+ const parsed = JSON.parse(raw);
76
+ return typeof parsed.lastCheckedAt === 'number' ? { lastCheckedAt: parsed.lastCheckedAt } : null;
77
+ }
78
+ catch (err) {
79
+ debug(`ignoring unreadable ${statePath}: ${err instanceof Error ? err.message : String(err)}`);
80
+ return null;
81
+ }
82
+ }
83
+ function writeState(statePath, state) {
84
+ mkdirSync(dirname(statePath), { recursive: true, mode: 0o700 });
85
+ writeFileSync(statePath, `${JSON.stringify(state)}\n`, { mode: 0o600 });
86
+ // writeFileSync only applies `mode` when it creates the file.
87
+ chmodSync(statePath, 0o600);
88
+ }
89
+ /**
90
+ * Returns the newer version when one should be announced, otherwise null.
91
+ * The timestamp is recorded before the request so an offline machine pays the
92
+ * timeout at most once per interval.
93
+ */
94
+ export async function checkForUpdate(deps) {
95
+ const { currentVersion, now, statePath, fetchLatest, debug } = deps;
96
+ if (!isCheckDue(readState(statePath, debug), now))
97
+ return null;
98
+ try {
99
+ writeState(statePath, { lastCheckedAt: now });
100
+ }
101
+ catch (err) {
102
+ debug(`could not write ${statePath}: ${err instanceof Error ? err.message : String(err)}`);
103
+ return null;
104
+ }
105
+ const controller = new AbortController();
106
+ const timer = setTimeout(() => controller.abort(), UPDATE_CHECK_TIMEOUT_MS);
107
+ try {
108
+ const latest = await fetchLatest(controller.signal);
109
+ return isNewerVersion(latest, currentVersion) ? latest : null;
110
+ }
111
+ catch (err) {
112
+ debug(`registry lookup failed: ${err instanceof Error ? err.message : String(err)}`);
113
+ return null;
114
+ }
115
+ finally {
116
+ clearTimeout(timer);
117
+ }
118
+ }
119
+ async function fetchLatestFromRegistry(signal) {
120
+ const response = await fetch(LATEST_URL, { signal, headers: { accept: 'application/json' } });
121
+ if (!response.ok)
122
+ throw new Error(`registry returned HTTP ${response.status}`);
123
+ const body = (await response.json());
124
+ if (typeof body.version !== 'string')
125
+ throw new Error('registry response has no version');
126
+ return body.version;
127
+ }
128
+ /** Entry point for src/index.ts, called once the command has finished. */
129
+ export async function notifyIfUpdateAvailable(currentVersion) {
130
+ const environment = {
131
+ argv: process.argv.slice(2),
132
+ env: process.env,
133
+ stdoutIsTTY: process.stdout.isTTY === true,
134
+ };
135
+ if (!isUpdateCheckEnabled(environment))
136
+ return;
137
+ const debug = (message) => {
138
+ if (process.env.DEBUG)
139
+ console.error(`haystack: update check: ${message}`);
140
+ };
141
+ const newer = await checkForUpdate({
142
+ currentVersion,
143
+ now: Date.now(),
144
+ statePath: defaultStatePath(),
145
+ fetchLatest: fetchLatestFromRegistry,
146
+ debug,
147
+ });
148
+ if (!newer)
149
+ return;
150
+ console.error(chalk.dim(`haystack ${newer} is available (you have ${currentVersion}). Update: npm i -g @haystackeditor/cli@latest`));
151
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.15.31",
3
+ "version": "0.16.0",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "scripts": {
10
10
  "build": "rm -rf dist && tsc && cp -r src/assets dist/assets",
11
11
  "dev": "tsc --watch",
12
+ "lint": "eslint src",
12
13
  "start": "node dist/index.js",
13
14
  "benchmark:telemetry": "pnpm build && node scripts/benchmark-telemetry-overhead.mjs",
14
15
  "benchmark:telemetry:corpus": "pnpm build && node scripts/benchmark-open-source-telemetry.mjs",
@@ -39,23 +40,26 @@
39
40
  "url": "https://github.com/haystackeditor/haystack-review/issues"
40
41
  },
41
42
  "dependencies": {
42
- "@babel/core": "7.29.0",
43
+ "@babel/core": "8.0.6",
44
+ "@inquirer/prompts": "8.7.2",
43
45
  "@modelcontextprotocol/sdk": "1.26.0",
44
- "chalk": "5.6.2",
45
- "commander": "12.1.0",
46
+ "chalk": "6.0.0",
47
+ "commander": "15.0.0",
46
48
  "fast-glob": "3.3.3",
47
- "glob": "10.5.0",
48
- "inquirer": "9.3.8",
49
- "ws": "8.21.0",
50
- "yaml": "2.8.2",
51
- "zod": "3.25.76"
49
+ "ws": "8.21.3",
50
+ "yaml": "2.9.1",
51
+ "zod": "4.6.5"
52
52
  },
53
53
  "devDependencies": {
54
- "@types/inquirer": "9.0.9",
55
- "@types/node": "20.19.30",
54
+ "@eslint/js": "8.57.1",
55
+ "@types/node": "22.19.8",
56
56
  "@types/ws": "8.18.1",
57
- "typescript": "5.9.3",
58
- "vitest": "4.1.10"
57
+ "@typescript-eslint/eslint-plugin": "7.18.0",
58
+ "@typescript-eslint/parser": "7.18.0",
59
+ "eslint": "8.57.1",
60
+ "globals": "13.24.0",
61
+ "typescript": "6.0.3",
62
+ "vitest": "5.0.1"
59
63
  },
60
64
  "files": [
61
65
  "dist",
@@ -63,6 +67,6 @@
63
67
  ],
64
68
  "bundledDependencies": [],
65
69
  "engines": {
66
- "node": ">=18"
70
+ "node": ">=22.18"
67
71
  }
68
72
  }
@@ -0,0 +1,184 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://docs.haystackeditor.com/cli/schemas/case-batch.v1.json",
4
+ "title": "Haystack hosted case batch (CASE-BATCH-V1)",
5
+ "description": "Machine-readable output of `haystack case-batch submit|status|cancel|bundle --json`. The run-side shapes mirror infra/lambda/haystack-design-verifier-shared/case_batch_contracts.ts.",
6
+ "type": "object",
7
+ "required": ["schema_version", "operation", "run_id"],
8
+ "properties": {
9
+ "schema_version": { "const": "1.0.0" },
10
+ "operation": { "enum": ["submit", "status", "cancel", "bundle"] },
11
+ "run_id": { "type": "string", "pattern": "^cv_[0-9a-f]{48}$" },
12
+ "repository": { "type": "string", "pattern": "^[A-Za-z0-9_.-]{1,100}/[A-Za-z0-9_.-]{1,100}$" },
13
+ "admitted": { "enum": ["created", "existing"] },
14
+ "base_commit": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
15
+ "head_commit": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
16
+ "base_world": { "type": "string", "pattern": "^frz_[0-9a-f]{16}$" },
17
+ "head_world": { "type": "string", "pattern": "^frz_[0-9a-f]{16}$" },
18
+ "idempotency_key": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$" },
19
+ "cases": {
20
+ "description": "On submit, the number of cases sent. On status, the per-case rows.",
21
+ "oneOf": [
22
+ { "type": "integer", "minimum": 1, "maximum": 5000 },
23
+ {
24
+ "type": "array",
25
+ "items": {
26
+ "type": "object",
27
+ "required": ["id", "position", "status", "outcome_status", "unknown_reason", "wall_ms", "cleanup_proven"],
28
+ "properties": {
29
+ "id": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,160}$" },
30
+ "position": { "type": "integer", "minimum": 0, "maximum": 4999 },
31
+ "label": { "type": "string" },
32
+ "condition_id": { "type": "string" },
33
+ "status": { "enum": ["running", "completed", "incomplete"] },
34
+ "launched_at_ms": { "type": "integer", "minimum": 0 },
35
+ "finished_at_ms": { "type": ["integer", "null"], "minimum": 0 },
36
+ "outcome_status": { "enum": ["AGREE", "DIFFER", "UNKNOWN", null] },
37
+ "unknown_reason": {
38
+ "type": ["string", "null"],
39
+ "description": "Open by ruling 1: an unrecognised reason still reads as UNKNOWN."
40
+ },
41
+ "wall_ms": { "type": ["integer", "null"], "minimum": 0 },
42
+ "cleanup_proven": { "type": ["boolean", "null"] },
43
+ "comparison_path": { "type": ["string", "null"] },
44
+ "case_bundle_path": { "type": ["string", "null"] },
45
+ "differing_outcomes": { "type": "array", "items": { "type": "string" } },
46
+ "detail": { "type": ["string", "null"] },
47
+ "phases": { "type": ["object", "null"] },
48
+ "vm_ids": { "type": "array", "items": { "type": "string" } },
49
+ "lease_ids": { "type": "array", "items": { "type": "string" } }
50
+ }
51
+ }
52
+ }
53
+ ]
54
+ },
55
+ "limits": {
56
+ "type": "object",
57
+ "required": ["max_concurrent_cases", "per_case_wall_ms", "total_budget_ms"],
58
+ "properties": {
59
+ "max_concurrent_cases": { "type": "integer", "minimum": 1, "maximum": 500 },
60
+ "per_case_wall_ms": { "type": "integer", "minimum": 1000, "maximum": 600000 },
61
+ "total_budget_ms": { "type": "integer", "minimum": 10000, "maximum": 1800000 }
62
+ }
63
+ },
64
+ "source_patch_attached": { "type": "boolean" },
65
+ "status": { "enum": ["queued", "preparing", "searching", "draining", "completed", "incomplete", "cancelled"] },
66
+ "terminal": { "type": "boolean" },
67
+ "timed_out": { "type": "boolean" },
68
+ "generation": { "type": "integer", "minimum": 0 },
69
+ "observed_at": { "type": "string" },
70
+ "cleanup_complete": { "type": "boolean" },
71
+ "vm_ids": { "type": "integer", "minimum": 0 },
72
+ "worlds": {
73
+ "type": "object",
74
+ "properties": {
75
+ "base": { "type": ["string", "null"] },
76
+ "head": { "type": ["string", "null"] }
77
+ }
78
+ },
79
+ "clocks": {
80
+ "type": "object",
81
+ "properties": {
82
+ "preparation_started_at": { "type": ["string", "null"] },
83
+ "search_started_at": { "type": ["string", "null"] },
84
+ "launches_closed_at": { "type": ["string", "null"] },
85
+ "finished_at": { "type": ["string", "null"] },
86
+ "search_budget_ms": { "type": "integer", "minimum": 0 },
87
+ "search_wall_ms": { "type": "integer", "minimum": 0 },
88
+ "drain_wall_ms": { "type": "integer", "minimum": 0 }
89
+ }
90
+ },
91
+ "totals": {
92
+ "type": "object",
93
+ "required": ["cases", "launched", "settled", "agree", "differ", "unknown", "cleanup_proven", "cleanup_unproven", "leases_released"],
94
+ "properties": {
95
+ "cases": { "type": "integer", "minimum": 0 },
96
+ "launched": { "type": "integer", "minimum": 0 },
97
+ "settled": { "type": "integer", "minimum": 0 },
98
+ "agree": { "type": "integer", "minimum": 0 },
99
+ "differ": { "type": "integer", "minimum": 0 },
100
+ "unknown": { "type": "integer", "minimum": 0 },
101
+ "cleanup_proven": { "type": "integer", "minimum": 0 },
102
+ "cleanup_unproven": { "type": "integer", "minimum": 0 },
103
+ "leases_released": { "type": "integer", "minimum": 0 }
104
+ }
105
+ },
106
+ "admission": {
107
+ "type": "object",
108
+ "required": ["requested_cases", "admitted_cases", "pool", "free_vm_slots", "reserved_vm_slots", "worker_processes", "cases_per_process"],
109
+ "properties": {
110
+ "requested_cases": { "type": "integer", "minimum": 0 },
111
+ "admitted_cases": { "type": "integer", "minimum": 0 },
112
+ "pool": { "enum": ["small", "big"] },
113
+ "free_vm_slots": { "type": "integer", "minimum": 0 },
114
+ "reserved_vm_slots": { "type": "integer", "minimum": 0 },
115
+ "worker_processes": { "type": "integer", "minimum": 0 },
116
+ "cases_per_process": { "type": "integer", "minimum": 0 }
117
+ }
118
+ },
119
+ "unknown_reasons": { "type": "object", "additionalProperties": { "type": "integer", "minimum": 1 } },
120
+ "cases_returned": { "type": "integer", "minimum": 0 },
121
+ "cases_truncated": { "type": "boolean" },
122
+ "pages": { "type": "integer", "minimum": 1 },
123
+ "waves": {
124
+ "type": "array",
125
+ "items": {
126
+ "type": "object",
127
+ "required": ["wave", "at_ms", "launched"],
128
+ "properties": {
129
+ "wave": { "type": "integer", "minimum": 0 },
130
+ "at_ms": { "type": "integer", "minimum": 0 },
131
+ "launched": { "type": "integer", "minimum": 0 }
132
+ }
133
+ }
134
+ },
135
+ "waves_truncated": { "type": "boolean" },
136
+ "exit_code": { "type": "integer", "minimum": 0, "maximum": 2 },
137
+ "error": { "type": "string" },
138
+ "cancel_requested": { "const": true },
139
+ "next_command": { "type": "string" },
140
+ "out_dir": { "type": "string" },
141
+ "manifest": {
142
+ "type": "object",
143
+ "required": ["path", "sha256", "bytes"],
144
+ "properties": {
145
+ "path": { "const": "manifest.json" },
146
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
147
+ "bytes": { "type": "integer", "minimum": 0 }
148
+ }
149
+ },
150
+ "artifacts": {
151
+ "type": "array",
152
+ "items": {
153
+ "type": "object",
154
+ "required": ["path", "sha256", "bytes", "verified"],
155
+ "properties": {
156
+ "path": { "type": "string" },
157
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
158
+ "bytes": { "type": "integer", "minimum": 0 },
159
+ "verified": { "const": true }
160
+ }
161
+ }
162
+ },
163
+ "artifact_count": { "type": "integer", "minimum": 0 },
164
+ "total_bytes": { "type": "integer", "minimum": 0 }
165
+ },
166
+ "allOf": [
167
+ {
168
+ "if": { "properties": { "operation": { "const": "status" } }, "required": ["operation"] },
169
+ "then": { "required": ["repository", "status", "terminal", "timed_out", "totals", "cases", "cases_returned", "cases_truncated", "exit_code"] }
170
+ },
171
+ {
172
+ "if": { "properties": { "operation": { "const": "submit" } }, "required": ["operation"] },
173
+ "then": { "required": ["repository", "admitted", "base_commit", "head_commit", "idempotency_key", "cases", "limits", "next_command"] }
174
+ },
175
+ {
176
+ "if": { "properties": { "operation": { "const": "cancel" } }, "required": ["operation"] },
177
+ "then": { "required": ["repository", "cancel_requested"] }
178
+ },
179
+ {
180
+ "if": { "properties": { "operation": { "const": "bundle" } }, "required": ["operation"] },
181
+ "then": { "required": ["repository", "out_dir", "manifest", "artifacts", "artifact_count", "total_bytes"] }
182
+ }
183
+ ]
184
+ }