heyspex 0.1.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 (104) hide show
  1. package/README.md +157 -0
  2. package/bin/hs.mjs +1028 -0
  3. package/bin/postinstall.mjs +88 -0
  4. package/dev-factory/lib/fleet-readiness.mjs +274 -0
  5. package/dev-factory/lib/fleet-state.mjs +250 -0
  6. package/dev-factory/lib/sufficiency-score.mjs +441 -0
  7. package/dist/cli/hs-check.mjs +747 -0
  8. package/dist/cli/hs-claim.mjs +541 -0
  9. package/dist/cli/hs-connect.mjs +3436 -0
  10. package/dist/cli/hs-defer.mjs +8455 -0
  11. package/dist/cli/hs-env.mjs +36845 -0
  12. package/dist/cli/hs-init.mjs +1145 -0
  13. package/dist/cli/hs-install.mjs +1390 -0
  14. package/dist/cli/hs-join.mjs +38553 -0
  15. package/dist/cli/hs-vault-pull.mjs +7782 -0
  16. package/dist/cli/hs-vault.mjs +4166 -0
  17. package/dist/cli/hs-workspace.mjs +2898 -0
  18. package/dist/cli-templates/customer-vault/specs/entity-model.md +1735 -0
  19. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +531 -0
  20. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +48 -0
  21. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +548 -0
  22. package/docs/mcp-client-config.md +200 -0
  23. package/package.json +124 -0
  24. package/scripts/check-mcp-freshness.mjs +379 -0
  25. package/scripts/dev-server-status.mjs +722 -0
  26. package/scripts/gateway-skills-lib.mjs +421 -0
  27. package/scripts/lib/console-url-resolver.mjs +152 -0
  28. package/scripts/lib/dev-mode.mjs +369 -0
  29. package/scripts/lib/hs-link.mjs +154 -0
  30. package/scripts/lib/hs-source-commit.mjs +207 -0
  31. package/scripts/lib/hs-source-log.mjs +366 -0
  32. package/scripts/lib/hs-source-preflight.mjs +402 -0
  33. package/scripts/lib/hs-source-push.mjs +307 -0
  34. package/scripts/lib/hs-source-secret-scan.mjs +263 -0
  35. package/scripts/lib/standalone-doctor.mjs +89 -0
  36. package/scripts/vault-id-lib.mjs +173 -0
  37. package/vendor/heyspex-mcp/alignment-gate.mjs +1347 -0
  38. package/vendor/heyspex-mcp/blocker-resolver.mjs +891 -0
  39. package/vendor/heyspex-mcp/breakdown-ladder.mjs +309 -0
  40. package/vendor/heyspex-mcp/breakdown-source.mjs +236 -0
  41. package/vendor/heyspex-mcp/breakdown-writer.mjs +524 -0
  42. package/vendor/heyspex-mcp/brief-observability.mjs +67 -0
  43. package/vendor/heyspex-mcp/canvas-inbox.mjs +130 -0
  44. package/vendor/heyspex-mcp/capture-active-rehydrate.mjs +76 -0
  45. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +147 -0
  46. package/vendor/heyspex-mcp/capture-channel.mjs +577 -0
  47. package/vendor/heyspex-mcp/capture-console-client.mjs +290 -0
  48. package/vendor/heyspex-mcp/capture-focus.mjs +1366 -0
  49. package/vendor/heyspex-mcp/capture-kind-suggest.mjs +121 -0
  50. package/vendor/heyspex-mcp/capture-outbox.mjs +239 -0
  51. package/vendor/heyspex-mcp/capture-stage-gate.mjs +160 -0
  52. package/vendor/heyspex-mcp/cli.mjs +15754 -0
  53. package/vendor/heyspex-mcp/config-settings.mjs +419 -0
  54. package/vendor/heyspex-mcp/console-fetch.mjs +342 -0
  55. package/vendor/heyspex-mcp/defer-spec.mjs +527 -0
  56. package/vendor/heyspex-mcp/deferred-reclaim.mjs +284 -0
  57. package/vendor/heyspex-mcp/design-tools.mjs +449 -0
  58. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +66 -0
  59. package/vendor/heyspex-mcp/diagnose-skeleton.mjs +114 -0
  60. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +241 -0
  61. package/vendor/heyspex-mcp/hooks-engine.mjs +399 -0
  62. package/vendor/heyspex-mcp/login-handoff.mjs +438 -0
  63. package/vendor/heyspex-mcp/mcp-subprocess.mjs +136 -0
  64. package/vendor/heyspex-mcp/mvp-mode.mjs +101 -0
  65. package/vendor/heyspex-mcp/next-resolution.mjs +240 -0
  66. package/vendor/heyspex-mcp/next-review.mjs +548 -0
  67. package/vendor/heyspex-mcp/peer-presence.mjs +430 -0
  68. package/vendor/heyspex-mcp/plan-ground-conflicts.mjs +321 -0
  69. package/vendor/heyspex-mcp/plan-ground.mjs +2001 -0
  70. package/vendor/heyspex-mcp/plan-ingest.mjs +476 -0
  71. package/vendor/heyspex-mcp/plan-push.mjs +1826 -0
  72. package/vendor/heyspex-mcp/plan-review.mjs +1419 -0
  73. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  74. package/vendor/heyspex-mcp/procedure-tools.mjs +970 -0
  75. package/vendor/heyspex-mcp/promote-capture.mjs +1280 -0
  76. package/vendor/heyspex-mcp/promote-diagnose.mjs +187 -0
  77. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +338 -0
  78. package/vendor/heyspex-mcp/promote-research.mjs +1553 -0
  79. package/vendor/heyspex-mcp/promotion-helpers.mjs +280 -0
  80. package/vendor/heyspex-mcp/read-path.mjs +138 -0
  81. package/vendor/heyspex-mcp/recipe-tools.mjs +258 -0
  82. package/vendor/heyspex-mcp/regen-tasks.mjs +326 -0
  83. package/vendor/heyspex-mcp/register-spec.mjs +417 -0
  84. package/vendor/heyspex-mcp/research-focus.mjs +476 -0
  85. package/vendor/heyspex-mcp/research-tools.mjs +109 -0
  86. package/vendor/heyspex-mcp/research-type-suggest.mjs +112 -0
  87. package/vendor/heyspex-mcp/resolution-focus.mjs +45 -0
  88. package/vendor/heyspex-mcp/review-focus.mjs +55 -0
  89. package/vendor/heyspex-mcp/review-rollup.mjs +47 -0
  90. package/vendor/heyspex-mcp/run-front.mjs +60 -0
  91. package/vendor/heyspex-mcp/scheduler.mjs +813 -0
  92. package/vendor/heyspex-mcp/seat-identity.mjs +281 -0
  93. package/vendor/heyspex-mcp/settings-tools.mjs +119 -0
  94. package/vendor/heyspex-mcp/spec-id-lib.mjs +790 -0
  95. package/vendor/heyspex-mcp/split-research.mjs +832 -0
  96. package/vendor/heyspex-mcp/submit-for-review.mjs +190 -0
  97. package/vendor/heyspex-mcp/summary-contract.mjs +162 -0
  98. package/vendor/heyspex-mcp/test-helpers.mjs +43 -0
  99. package/vendor/heyspex-mcp/validate-linkage.mjs +574 -0
  100. package/vendor/heyspex-mcp/vault-resolve.mjs +297 -0
  101. package/vendor/heyspex-mcp/vault-settings.mjs +87 -0
  102. package/vendor/heyspex-mcp/verify-reproduction.mjs +314 -0
  103. package/vendor/heyspex-mcp/why-now.mjs +64 -0
  104. package/vendor/heyspex-mcp/yaml-scalar.mjs +34 -0
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Console HTTP helpers — extracted from cli.mjs so the timeout / retry /
3
+ * error-kind classification are unit-testable without booting the full MCP
4
+ * stdio server.
5
+ *
6
+ * Transport recovery (recurring false `CONSOLE_UNREACHABLE` when the server is up):
7
+ *
8
+ * 1. Default timeout is 30 s; one retry on `AbortError` → `CONSOLE_TIMEOUT`.
9
+ * 2. On network failure (`status: 0`), try alternate loopback hosts (localhost /
10
+ * 127.0.0.1 / [::1]) — Windows often binds one family only.
11
+ * 2b. On loopback network failure, backoff-retry the same URL (Next dev restart).
12
+ * 3. Optional `recoverFromTransportFailure` (wired in cli.mjs to
13
+ * `getDevServerStatus`) scans candidate ports; when the console is live on a
14
+ * different port than `.heyspex/console-url`, retries once and heals the file.
15
+ * 4. When discovery finds a live server but the retry still fails →
16
+ * `CONSOLE_URL_MISMATCH` (not "server down").
17
+ *
18
+ * Factory pattern matches `capture-console-client.mjs`: cli.mjs binds the real
19
+ * `getConsoleUrl`; tests pass stubs for both URL resolution and the fetch impl.
20
+ *
21
+ * TRANSPORT AUTH ([[HS-GD45-MCPC-TASK-mcp-cloud-auth-shared-console-resolver]]):
22
+ * {@link getAuthHeader} resolves an `Authorization: Bearer <token>` header
23
+ * (precedence: `HEYSPEX_MCP_TOKEN` env → `~/.heyspex/credentials.yaml`
24
+ * `mcp_token`, written by `hs mcp pair <code>`) and `makeConsoleFetch` attaches
25
+ * it to every outbound request — `consoleFetch` AND `postClaimEndpoint` share
26
+ * one transport, so both get it for free. No token resolvable → no header,
27
+ * exactly today's (unauthenticated) behavior against a local dev console.
28
+ */
29
+ import { readFileSync } from 'node:fs';
30
+ import { homedir } from 'node:os';
31
+ import { join } from 'node:path';
32
+
33
+ export const DEFAULT_CONSOLE_TIMEOUT_MS = 30000;
34
+
35
+ /** Backoff when Next dev restarts (next.config change, etc.) — ECONNREFUSED is transient. */
36
+ export const DEV_RESTART_RETRY_DELAYS_MS = [1500, 2500, 4000];
37
+
38
+ function sleep(ms) {
39
+ return new Promise((resolve) => setTimeout(resolve, ms));
40
+ }
41
+
42
+ /**
43
+ * Detect AbortError from a thrown fetch error. Covers the standard
44
+ * `AbortError` name and the substring fallback some runtimes use
45
+ * ("This operation was aborted").
46
+ */
47
+ export function isAbortError(err) {
48
+ if (!err) return false;
49
+ if (err.name === 'AbortError') return true;
50
+ const msg = typeof err.message === 'string' ? err.message : String(err);
51
+ return /aborted/i.test(msg);
52
+ }
53
+
54
+ /**
55
+ * Same port/path, alternate loopback hosts. Mirrors the probe rationale in
56
+ * scripts/dev-server-status.mjs (Next may listen on ::1 while fetch tried IPv4).
57
+ */
58
+ export function loopbackHostVariants(urlString) {
59
+ let base;
60
+ try {
61
+ base = new URL(urlString);
62
+ } catch {
63
+ return [urlString];
64
+ }
65
+ const alts = new Set([base.hostname]);
66
+ if (base.hostname === 'localhost' || base.hostname === '127.0.0.1' || base.hostname === '::1') {
67
+ alts.add('localhost');
68
+ alts.add('127.0.0.1');
69
+ alts.add('::1');
70
+ }
71
+ const out = [];
72
+ for (const host of alts) {
73
+ const u = new URL(base.href);
74
+ u.hostname = host;
75
+ out.push(u.href);
76
+ }
77
+ return [...new Set(out)];
78
+ }
79
+
80
+ function isLoopbackUrl(urlString) {
81
+ try {
82
+ const { hostname } = new URL(urlString);
83
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Rewrite the origin of `urlString` to match `originUrl` (port-discovery heal).
91
+ */
92
+ export function rewriteUrlOrigin(urlString, originUrl) {
93
+ const target = new URL(urlString);
94
+ const origin = new URL(originUrl);
95
+ target.protocol = origin.protocol;
96
+ target.hostname = origin.hostname;
97
+ target.port = origin.port;
98
+ return target.href;
99
+ }
100
+
101
+ /**
102
+ * Resolve the outbound `Authorization` header, or `{}` when no token
103
+ * resolves (no header at all — matches today's unauthenticated behavior
104
+ * against a local dev console). Precedence: `HEYSPEX_MCP_TOKEN` env →
105
+ * `~/.heyspex/credentials.yaml` `mcp_token`. Injectable `env`/`homeDir` for
106
+ * tests; never throws (an unreadable credentials file is just "no token").
107
+ */
108
+ export function getAuthHeader({ env = process.env, homeDir = homedir() } = {}) {
109
+ const envToken = env.HEYSPEX_MCP_TOKEN?.trim();
110
+ if (envToken) return { Authorization: `Bearer ${envToken}` };
111
+
112
+ try {
113
+ const raw = readFileSync(join(homeDir, '.heyspex', 'credentials.yaml'), 'utf8');
114
+ const match = raw.match(/^mcp_token:\s*["']?([^"'\r\n#]+)/m);
115
+ const token = match?.[1]?.trim();
116
+ if (token) return { Authorization: `Bearer ${token}` };
117
+ } catch {
118
+ // Missing file, missing key, or unreadable — no credentials-file token.
119
+ }
120
+ return {};
121
+ }
122
+
123
+ /**
124
+ * Build a console-fetch client bound to the given URL resolver + fetch impl.
125
+ *
126
+ * @param {object} deps
127
+ * @param {() => string} deps.getConsoleUrl
128
+ * @param {typeof fetch} [deps.fetchImpl]
129
+ * @param {number} [deps.defaultTimeoutMs]
130
+ * @param {(failedResult: object, ctx: { url: string, init: object, timeoutMs: number }) => Promise<{ retryUrl?: string, reason?: string, healedUrl?: string, discoveryHealed?: boolean, devServerStatus?: object } | null | undefined>} [deps.recoverFromTransportFailure]
131
+ * @param {() => object} [deps.getAuthHeader] Resolves the outbound Authorization header (default: {@link getAuthHeader}).
132
+ */
133
+ export function makeConsoleFetch({
134
+ getConsoleUrl,
135
+ fetchImpl = fetch,
136
+ defaultTimeoutMs = DEFAULT_CONSOLE_TIMEOUT_MS,
137
+ recoverFromTransportFailure,
138
+ getAuthHeader: getAuthHeaderDep = getAuthHeader,
139
+ }) {
140
+ async function attempt(url, init, timeoutMs) {
141
+ const controller = new AbortController();
142
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
143
+ try {
144
+ const response = await fetchImpl(url, { ...init, signal: controller.signal });
145
+ const text = await response.text();
146
+ let parsed = null;
147
+ try {
148
+ parsed = text ? JSON.parse(text) : null;
149
+ } catch {
150
+ parsed = { raw: text };
151
+ }
152
+ return { ok: response.ok, status: response.status, body: parsed, url };
153
+ } catch (err) {
154
+ const errorKind = isAbortError(err) ? 'timeout' : 'network';
155
+ return {
156
+ ok: false,
157
+ status: 0,
158
+ body: null,
159
+ url,
160
+ error: err?.message || String(err),
161
+ errorKind,
162
+ };
163
+ } finally {
164
+ clearTimeout(timer);
165
+ }
166
+ }
167
+
168
+ async function recoverNetworkFailure(result, ctx) {
169
+ const { url, init, timeoutMs } = ctx;
170
+
171
+ for (const alt of loopbackHostVariants(url)) {
172
+ if (alt === url) continue;
173
+ const retry = await attempt(alt, init, timeoutMs);
174
+ if (retry.ok) {
175
+ return {
176
+ ...retry,
177
+ recoveredVia: 'loopback_host',
178
+ attemptedUrl: url,
179
+ };
180
+ }
181
+ }
182
+
183
+ // Next.js dev restart window (e.g. next.config.mjs change) — loopback only, brief ECONNREFUSED.
184
+ if (isLoopbackUrl(url)) {
185
+ let last = result;
186
+ for (const delay of DEV_RESTART_RETRY_DELAYS_MS) {
187
+ await sleep(delay);
188
+ const retry = await attempt(url, init, timeoutMs);
189
+ if (retry.ok) {
190
+ return {
191
+ ...retry,
192
+ recoveredVia: 'dev_restart',
193
+ attemptedUrl: url,
194
+ };
195
+ }
196
+ last = retry;
197
+ if (retry.errorKind === 'timeout') break;
198
+ }
199
+ result = last;
200
+ }
201
+
202
+ if (!recoverFromTransportFailure) return result;
203
+
204
+ let recovery;
205
+ try {
206
+ recovery = await recoverFromTransportFailure(result, ctx);
207
+ } catch {
208
+ return result;
209
+ }
210
+ if (!recovery) return result;
211
+
212
+ if (recovery.retryUrl && recovery.retryUrl !== url) {
213
+ const retry = await attempt(recovery.retryUrl, init, timeoutMs);
214
+ if (retry.ok) {
215
+ return {
216
+ ...retry,
217
+ recoveredVia: recovery.reason || 'port_discovery',
218
+ attemptedUrl: url,
219
+ ...(recovery.discoveryHealed
220
+ ? { discoveryHealed: true, healedUrl: recovery.healedUrl || recovery.retryUrl }
221
+ : {}),
222
+ };
223
+ }
224
+ return {
225
+ ...result,
226
+ urlMismatch: true,
227
+ suggestedUrl: recovery.healedUrl || recovery.retryUrl,
228
+ devServerStatus: recovery.devServerStatus,
229
+ discoveryAttempted: recovery.retryUrl,
230
+ discoveryHealed: recovery.discoveryHealed === true,
231
+ };
232
+ }
233
+
234
+ if (recovery.devServerStatus?.running) {
235
+ const live = recovery.devServerStatus.servers?.find((s) => s.probe === 'console');
236
+ return {
237
+ ...result,
238
+ urlMismatch: true,
239
+ suggestedUrl: live?.url,
240
+ devServerStatus: recovery.devServerStatus,
241
+ };
242
+ }
243
+
244
+ return result;
245
+ }
246
+
247
+ async function withRetry(url, init, timeoutMs, { retryOnTimeout = true } = {}) {
248
+ let result = await attempt(url, init, timeoutMs);
249
+ if (!result.ok && result.errorKind === 'timeout' && retryOnTimeout) {
250
+ result = await attempt(url, init, timeoutMs);
251
+ }
252
+ if (result.ok) return result;
253
+ if (result.errorKind === 'network') {
254
+ result = await recoverNetworkFailure(result, { url, init, timeoutMs });
255
+ }
256
+ return result;
257
+ }
258
+
259
+ /**
260
+ * POST/DELETE/GET against `/api/specs/:id/claim`.
261
+ */
262
+ async function postClaimEndpoint(method, specId, bodyJson) {
263
+ const consoleUrl = getConsoleUrl();
264
+ const url = `${consoleUrl}/api/specs/${encodeURIComponent(specId)}/claim`;
265
+ const init = {
266
+ method,
267
+ headers: { 'content-type': 'application/json', ...getAuthHeaderDep() },
268
+ };
269
+ if (bodyJson !== undefined) {
270
+ init.body = JSON.stringify(bodyJson);
271
+ }
272
+ return withRetry(url, init, defaultTimeoutMs);
273
+ }
274
+
275
+ /**
276
+ * Generic fetch helper for console API with timeout + JSON parsing + 1 retry
277
+ * on AbortError. Returns `{ ok, status, body, url, error?, errorKind? }`.
278
+ *
279
+ * `options.retryOnTimeout: false` opts a NON-IDEMPOTENT call (a create) out
280
+ * of the blind timeout-retry against a REMOTE console ([[HS-2FQY]]): the
281
+ * server-side Idempotency-Key dedup ([[HS-70TD-DIAG]]) lives in a
282
+ * per-process in-memory registry, so on a serverless deployment
283
+ * (app.heyspex.com) the retry can land on a DIFFERENT instance and mint a
284
+ * second spec id for the same logical create. Against a loopback console
285
+ * the retry is kept — one dev process, the registry replays reliably.
286
+ */
287
+ async function consoleFetch(path, options = {}) {
288
+ const consoleUrl = getConsoleUrl();
289
+ const url = `${consoleUrl}${path}`;
290
+ const init = {
291
+ method: options.method || 'GET',
292
+ headers: { 'content-type': 'application/json', ...getAuthHeaderDep(), ...options.headers },
293
+ };
294
+ if (options.body !== undefined) {
295
+ init.body = typeof options.body === 'string' ? options.body : JSON.stringify(options.body);
296
+ }
297
+ const timeoutMs = options.timeout || defaultTimeoutMs;
298
+ const retryOnTimeout = options.retryOnTimeout === false ? isLoopbackUrl(url) : true;
299
+ return withRetry(url, init, timeoutMs, { retryOnTimeout });
300
+ }
301
+
302
+ return { postClaimEndpoint, consoleFetch };
303
+ }
304
+
305
+ /**
306
+ * Map a failed transport result to an agent-facing error code. Splits
307
+ * timeout (server up but slow / cold-compiling) from unreachable (server
308
+ * actually down). Caller supplies the HTTP-error fallback used when the
309
+ * transport succeeded but the console returned a 4xx/5xx.
310
+ */
311
+ export function consoleErrorCode(result, httpFailureCode) {
312
+ if (result.urlMismatch) return 'CONSOLE_URL_MISMATCH';
313
+ if (result.errorKind === 'timeout') return 'CONSOLE_TIMEOUT';
314
+ if (result.status === 0) return 'CONSOLE_UNREACHABLE';
315
+ return httpFailureCode;
316
+ }
317
+
318
+ /**
319
+ * Agent-facing message for a failed transport result. Tailored per
320
+ * error kind so the agent can act: retry on timeout vs start the dev
321
+ * server on network error.
322
+ */
323
+ export function consoleErrorMessage(result, httpFailureMessage) {
324
+ if (result.urlMismatch) {
325
+ const live =
326
+ result.suggestedUrl
327
+ || result.devServerStatus?.servers?.find((s) => s.probe === 'console')?.url;
328
+ const healNote = result.discoveryHealed
329
+ ? ' Updated `.heyspex/console-url` — retry the tool call.'
330
+ : ' Call `heyspex_dev_server_status` and retry — do NOT hand-write vault files.';
331
+ return live
332
+ ? `Spex-console dev server is running at ${live} but MCP tried ${result.url}.${healNote}`
333
+ : `Spex-console dev server appears to be running on another port than ${result.url}.${healNote}`;
334
+ }
335
+ if (result.errorKind === 'timeout') {
336
+ return `Console at ${result.url} timed out (retried once). It may be cold-compiling or under load — retry shortly. Do not conclude the dev server is down.`;
337
+ }
338
+ if (result.status === 0) {
339
+ return `Could not reach spex-console at ${result.url}: ${result.error}. Call heyspex_dev_server_status before concluding the server is down.`;
340
+ }
341
+ return httpFailureMessage;
342
+ }