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,747 @@
1
+ import { createRequire as __hsCreateRequire } from 'node:module';
2
+ const require = __hsCreateRequire(import.meta.url);
3
+
4
+ // spex-console/bin/hs-check.ts
5
+ import { spawn, spawnSync } from "node:child_process";
6
+ import { existsSync, readFileSync as readFileSync4 } from "node:fs";
7
+ import path2 from "node:path";
8
+ import process2 from "node:process";
9
+
10
+ // spex-console/src/lib/cli/checks-manifest.ts
11
+ var EXIT = {
12
+ /** Every machine/property criterion passed (judgment-only criteria don't fail the run). */
13
+ OK: 0,
14
+ /** Usage error — unknown flag, missing target, or bad verb. */
15
+ USAGE: 1,
16
+ /** Manifest / target not found. */
17
+ NOT_FOUND: 10,
18
+ /** Manifest present but malformed. */
19
+ INVALID_MANIFEST: 11,
20
+ /** A check executor was required but unavailable (e.g. the command runner). */
21
+ RUNNER_MISSING: 12,
22
+ /** The manifest ran and at least one machine criterion failed (a real red). */
23
+ CHECKS_FAILED: 20
24
+ };
25
+ function parseCheckRunArgs(argv) {
26
+ const verbRaw = argv[0];
27
+ if (verbRaw === "--help" || verbRaw === "-h") return { help: true };
28
+ if (verbRaw !== "run") {
29
+ return { error: { code: EXIT.USAGE, message: `unknown verb: ${verbRaw ?? "(none)"}` } };
30
+ }
31
+ let target;
32
+ let json = false;
33
+ let report = false;
34
+ for (let i = 1; i < argv.length; i++) {
35
+ const arg = argv[i];
36
+ if (arg === "--help" || arg === "-h") return { help: true };
37
+ else if (arg === "--json") json = true;
38
+ else if (arg === "--report") report = true;
39
+ else if (arg === "--manifest" || arg.startsWith("--manifest=")) {
40
+ const value = arg.includes("=") ? arg.slice("--manifest=".length) : argv[++i];
41
+ if (value === void 0) {
42
+ return { error: { code: EXIT.USAGE, message: "--manifest requires a value" } };
43
+ }
44
+ target = value;
45
+ } else if (arg.startsWith("-")) {
46
+ return { error: { code: EXIT.USAGE, message: `unknown flag: ${arg}` } };
47
+ } else if (target === void 0) {
48
+ target = arg;
49
+ } else {
50
+ return { error: { code: EXIT.USAGE, message: `unexpected argument: ${arg}` } };
51
+ }
52
+ }
53
+ if (!target) {
54
+ return { error: { code: EXIT.USAGE, message: "run requires a spec id or manifest path" } };
55
+ }
56
+ return { args: { verb: "run", target, json, report } };
57
+ }
58
+ function buildVerdict(report) {
59
+ return {
60
+ manifestPassed: report.passed,
61
+ source: report.frozenAt ? `hs check run @ ${report.frozenAt}` : "hs check run",
62
+ failed: report.failedCount,
63
+ judgmentOnly: report.judgmentOnlyCount
64
+ };
65
+ }
66
+ var KIND_ALIASES = {
67
+ tests: "test",
68
+ unit: "test",
69
+ grep: "command",
70
+ diff: "command",
71
+ live: "command"
72
+ };
73
+ function joinUnderRoot(root, rel) {
74
+ if (rel === "" || rel === ".") return root;
75
+ if (/^(?:[a-zA-Z]:[\\/]|[\\/])/.test(rel)) return rel;
76
+ const sep = root.includes("\\") && !root.includes("/") ? "\\" : "/";
77
+ return `${root.replace(/[\\/]+$/, "")}${sep}${rel.replace(/^[\\/]+/, "")}`;
78
+ }
79
+ function resolveCheckCwds(check, roots) {
80
+ if (typeof check.cwd === "string" && check.cwd.trim() !== "") {
81
+ return [joinUnderRoot(roots.repoRoot, check.cwd.trim())];
82
+ }
83
+ return roots.repoRoot === roots.consoleRoot ? [roots.repoRoot] : [roots.repoRoot, roots.consoleRoot];
84
+ }
85
+ var VALID_METHODS = /* @__PURE__ */ new Set([
86
+ "machine",
87
+ "property",
88
+ "human"
89
+ ]);
90
+ var VALID_KINDS = /* @__PURE__ */ new Set([
91
+ "test",
92
+ "lint",
93
+ "types",
94
+ "command",
95
+ "guard"
96
+ ]);
97
+ var VALID_EXPECTS = /* @__PURE__ */ new Set([
98
+ "exit-0",
99
+ "no-net-new",
100
+ "empty-output"
101
+ ]);
102
+ function parseChecksManifest(text) {
103
+ let raw;
104
+ try {
105
+ raw = JSON.parse(text);
106
+ } catch (err) {
107
+ return {
108
+ error: `manifest is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
109
+ };
110
+ }
111
+ if (typeof raw !== "object" || raw === null) {
112
+ return { error: "manifest must be an object" };
113
+ }
114
+ const rec = raw;
115
+ if (typeof rec.spec !== "string" || rec.spec.trim() === "") {
116
+ return { error: 'manifest is missing a non-empty "spec"' };
117
+ }
118
+ if (!Array.isArray(rec.criteria)) {
119
+ return { error: 'manifest must have a "criteria" array' };
120
+ }
121
+ const criteria = [];
122
+ for (let i = 0; i < rec.criteria.length; i++) {
123
+ const c = rec.criteria[i];
124
+ if (typeof c !== "object" || c === null) {
125
+ return { error: `criteria[${i}] must be an object` };
126
+ }
127
+ const cr = c;
128
+ const ac = typeof cr.ac === "number" ? cr.ac : i + 1;
129
+ const ctext = typeof cr.text === "string" ? cr.text : "";
130
+ const method = cr.method;
131
+ if (typeof method !== "string" || !VALID_METHODS.has(method)) {
132
+ return { error: `criteria[${i}].method must be one of machine|property|human` };
133
+ }
134
+ const rawChecks = cr.checks ?? [];
135
+ if (!Array.isArray(rawChecks)) {
136
+ return { error: `criteria[${i}].checks must be an array` };
137
+ }
138
+ if (method === "human" && rawChecks.length > 0) {
139
+ return {
140
+ error: `criteria[${i}] is method:human but has checks \u2014 judgment-only criteria carry none`
141
+ };
142
+ }
143
+ if (method !== "human" && rawChecks.length === 0) {
144
+ return { error: `criteria[${i}] is method:${method} but has no checks to run` };
145
+ }
146
+ const checks = [];
147
+ for (let j = 0; j < rawChecks.length; j++) {
148
+ const t = rawChecks[j];
149
+ if (typeof t !== "object" || t === null) {
150
+ return { error: `criteria[${i}].checks[${j}] must be an object` };
151
+ }
152
+ const tr = t;
153
+ if (typeof tr.kind !== "string") {
154
+ return {
155
+ error: `criteria[${i}].checks[${j}].kind must be one of test|lint|types|command|guard`
156
+ };
157
+ }
158
+ const kind = KIND_ALIASES[tr.kind] ?? tr.kind;
159
+ if (!VALID_KINDS.has(kind)) {
160
+ return {
161
+ error: `criteria[${i}].checks[${j}].kind must be one of test|lint|types|command|guard`
162
+ };
163
+ }
164
+ if (typeof tr.run !== "string" || tr.run.trim() === "") {
165
+ return { error: `criteria[${i}].checks[${j}] is missing a non-empty "run"` };
166
+ }
167
+ const expectRaw = tr.expect ?? "exit-0";
168
+ if (typeof expectRaw !== "string" || !VALID_EXPECTS.has(expectRaw)) {
169
+ return {
170
+ error: `criteria[${i}].checks[${j}].expect must be one of exit-0|no-net-new|empty-output`
171
+ };
172
+ }
173
+ const baseline = typeof tr.baseline === "number" && Number.isFinite(tr.baseline) ? tr.baseline : 0;
174
+ const cwd = typeof tr.cwd === "string" && tr.cwd.trim() !== "" ? tr.cwd.trim() : void 0;
175
+ checks.push({
176
+ kind,
177
+ run: tr.run,
178
+ expect: expectRaw,
179
+ baseline,
180
+ ...cwd ? { cwd } : {}
181
+ });
182
+ }
183
+ criteria.push({ ac, text: ctext, method, checks });
184
+ }
185
+ const frozenAt = rec.frozen_at ?? rec.frozenAt;
186
+ const authoredBy = rec.authored_by ?? rec.authoredBy;
187
+ return {
188
+ manifest: {
189
+ spec: rec.spec,
190
+ frozenAt: typeof frozenAt === "string" && frozenAt.trim() !== "" ? frozenAt : null,
191
+ authoredBy: typeof authoredBy === "string" && authoredBy.trim() !== "" ? authoredBy : null,
192
+ criteria
193
+ }
194
+ };
195
+ }
196
+ var ZERO_MATCH_RE = /Passed:\s*0\b|No test is available|No test files found|No test matches the given testcase filter/i;
197
+ function detectZeroMatch(rawOutput) {
198
+ if (!rawOutput) return null;
199
+ const line = rawOutput.split(/\r?\n/).find((l) => ZERO_MATCH_RE.test(l));
200
+ return line ? line.trim() : null;
201
+ }
202
+ function evaluateCheck(check, outcome) {
203
+ if (detectZeroMatch(outcome.rawOutput) !== null) return false;
204
+ if (check.expect === "no-net-new") {
205
+ return (outcome.errorCount ?? 0) <= check.baseline;
206
+ }
207
+ if (check.expect === "empty-output") {
208
+ return outcome.exitCode === 0 && (outcome.stdout ?? "\0").trim() === "";
209
+ }
210
+ return outcome.exitCode === 0;
211
+ }
212
+ async function runChecksManifest(manifest, deps) {
213
+ const criteria = [];
214
+ let durationMs = 0;
215
+ for (const entry of manifest.criteria) {
216
+ if (entry.method === "human") {
217
+ criteria.push({
218
+ ac: entry.ac,
219
+ text: entry.text,
220
+ method: entry.method,
221
+ verdict: "judgment_only",
222
+ checks: []
223
+ });
224
+ continue;
225
+ }
226
+ const checkResults = [];
227
+ for (const check of entry.checks) {
228
+ let outcome;
229
+ try {
230
+ outcome = await deps.runCheck(check);
231
+ } catch (err) {
232
+ outcome = {
233
+ exitCode: -1,
234
+ durationMs: 0,
235
+ message: err instanceof Error ? err.message : String(err)
236
+ };
237
+ }
238
+ const passed = evaluateCheck(check, outcome);
239
+ durationMs += outcome.durationMs;
240
+ checkResults.push({
241
+ check,
242
+ passed,
243
+ durationMs: outcome.durationMs,
244
+ message: passed ? void 0 : outcome.message ?? failureMessage(check, outcome)
245
+ });
246
+ }
247
+ const verdict = checkResults.every((r) => r.passed) ? "pass" : "fail";
248
+ criteria.push({
249
+ ac: entry.ac,
250
+ text: entry.text,
251
+ method: entry.method,
252
+ verdict,
253
+ checks: checkResults
254
+ });
255
+ }
256
+ const passedCount = criteria.filter((c) => c.verdict === "pass").length;
257
+ const failedCount = criteria.filter((c) => c.verdict === "fail").length;
258
+ const judgmentOnlyCount = criteria.filter((c) => c.verdict === "judgment_only").length;
259
+ return {
260
+ spec: manifest.spec,
261
+ frozenAt: manifest.frozenAt,
262
+ authoredBy: manifest.authoredBy,
263
+ criteria,
264
+ passed: failedCount === 0,
265
+ total: criteria.length,
266
+ passedCount,
267
+ failedCount,
268
+ judgmentOnlyCount,
269
+ durationMs
270
+ };
271
+ }
272
+ function failureMessage(check, outcome) {
273
+ const zeroMatch = detectZeroMatch(outcome.rawOutput);
274
+ if (zeroMatch !== null) {
275
+ return `zero-match: the check's filter matched nothing to run \u2014 "${zeroMatch}"`;
276
+ }
277
+ if (check.expect === "no-net-new") {
278
+ return `${outcome.errorCount ?? "?"} errors exceeds baseline ${check.baseline}`;
279
+ }
280
+ return `command exited ${outcome.exitCode}`;
281
+ }
282
+ function reportExitCode(report) {
283
+ return report.passed ? EXIT.OK : EXIT.CHECKS_FAILED;
284
+ }
285
+ var VERDICT_MARK = {
286
+ pass: "PASS",
287
+ fail: "FAIL",
288
+ judgment_only: "JUDG"
289
+ };
290
+ function toText(report) {
291
+ if (report.total === 0) {
292
+ return "No criteria in manifest.";
293
+ }
294
+ const lines = [];
295
+ lines.push(`Verifying ${report.spec}` + (report.frozenAt ? ` (frozen ${report.frozenAt})` : ""));
296
+ for (const c of report.criteria) {
297
+ const head = `[${VERDICT_MARK[c.verdict]}] AC${c.ac} (${c.method}) \u2014 ${c.text}`;
298
+ lines.push(head);
299
+ if (c.verdict === "judgment_only") {
300
+ lines.push(" \u2192 judgment-only: no machine check; handed to the council.");
301
+ }
302
+ for (const r of c.checks) {
303
+ const m = r.passed ? "ok " : "FAIL";
304
+ const detail = !r.passed && r.message ? ` \u2014 ${r.message}` : "";
305
+ lines.push(` ${m} ${r.check.kind}: ${r.check.run} (${r.durationMs}ms)${detail}`);
306
+ }
307
+ }
308
+ const overall = report.passed ? "PASS" : "FAIL";
309
+ lines.push("");
310
+ lines.push(
311
+ `${overall}: ${report.passedCount} passed, ${report.failedCount} failed, ${report.judgmentOnlyCount} judgment-only of ${report.total} criteria (${report.durationMs}ms)`
312
+ );
313
+ return lines.join("\n");
314
+ }
315
+ function formatReport(report) {
316
+ return toText(report);
317
+ }
318
+ function reportToJson(report) {
319
+ return {
320
+ ok: report.passed,
321
+ spec: report.spec,
322
+ frozenAt: report.frozenAt,
323
+ authoredBy: report.authoredBy,
324
+ total: report.total,
325
+ passed: report.passedCount,
326
+ failed: report.failedCount,
327
+ judgmentOnly: report.judgmentOnlyCount,
328
+ durationMs: report.durationMs,
329
+ criteria: report.criteria.map((c) => ({
330
+ ac: c.ac,
331
+ method: c.method,
332
+ verdict: c.verdict,
333
+ checks: c.checks.map((r) => ({
334
+ kind: r.check.kind,
335
+ run: r.check.run,
336
+ passed: r.passed,
337
+ durationMs: r.durationMs,
338
+ ...r.passed ? {} : { message: r.message }
339
+ }))
340
+ }))
341
+ };
342
+ }
343
+ var HELP = [
344
+ "Usage:",
345
+ " hs check run <spec-id|manifest.json> [--json]",
346
+ "",
347
+ "Verify a spec's acceptance criteria against its frozen checks manifest, and",
348
+ "report pass/fail PER CRITERION against the real working tree (HS-EPIC-DCEP).",
349
+ "Machine criteria run their checks; method:human criteria are reported",
350
+ "judgment-only and handed to the council \u2014 never silently auto-passed.",
351
+ "",
352
+ "Manifest shape (specs/checks/<spec-id>.checks.json):",
353
+ ' { "spec": "[[HS-EPIC-XYZ]]", "frozen_at": "...", "authored_by": "...",',
354
+ ' "criteria": [ { "ac": 1, "text": "...", "method": "machine",',
355
+ ' "checks": [ { "kind": "types", "run": "npx tsc --noEmit",',
356
+ ' "expect": "no-net-new", "baseline": 60 } ] } ] }',
357
+ "",
358
+ "Arguments:",
359
+ " <spec-id|manifest.json> A manifest path, or a spec id resolved to",
360
+ " specs/checks/<id>.checks.json under the vault root.",
361
+ "",
362
+ "Flags:",
363
+ " --manifest <p> Manifest path (alias for the positional argument).",
364
+ " --report PATCH the verdict to the spec's active claim (the PCFL gate).",
365
+ " Requires a spec-id target (identifies the claim). The verify",
366
+ " exit code is unchanged; reporting is a side effect.",
367
+ " --json Emit a machine-readable JSON result (the gate's input shape).",
368
+ " --help, -h Show this help.",
369
+ "",
370
+ "Exit codes:",
371
+ " 0 ok \xB7 1 usage \xB7 10 not-found \xB7 11 bad-manifest \xB7 12 runner-missing \xB7 20 checks-failed"
372
+ ].join("\n");
373
+
374
+ // spex-console/src/lib/cli/control-shared.ts
375
+ import { readFileSync } from "node:fs";
376
+ import { homedir } from "node:os";
377
+ import { join } from "node:path";
378
+ var EXIT2 = {
379
+ /** Success. */
380
+ OK: 0,
381
+ /** Usage error — unknown subcommand/flag, or a missing required argument. */
382
+ USAGE: 1,
383
+ /** Claim race lost — the spec is already claimed by another agent (409). */
384
+ CONFLICT: 9,
385
+ /** Target not found — release of an unheld claim, etc. (404). */
386
+ NOT_FOUND: 10,
387
+ /** Invalid argument — bad `--ttl`, unknown `--kind`, unparseable date. */
388
+ INVALID_ARG: 11,
389
+ /** SpexConsole unreachable (network error reaching the dev server). */
390
+ CONSOLE_UNREACHABLE: 12,
391
+ /** Server failure — a non-OK response that isn't 400/402/404/409. */
392
+ WRITE_FAILED: 13,
393
+ /** Neon control plane absent — the endpoint returned 402 (upgrade required). */
394
+ NEON_REQUIRED: 14,
395
+ /**
396
+ * A cross-account `--account <id>` override was refused — the caller is
397
+ * not a builder/admin (`resolveScopedAccountId`'s `forbidden_account_override`,
398
+ * [[HS-X3GB-EKLJ-TASK-hs-cli-account-flag-for-cross-account]]). Distinct
399
+ * from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
400
+ * malformed command.
401
+ */
402
+ FORBIDDEN: 15
403
+ };
404
+ function readPairedToken() {
405
+ try {
406
+ const raw = readFileSync(join(homedir(), ".heyspex", "credentials.yaml"), "utf-8");
407
+ const match = raw.match(/\b(hsmcp_[A-Za-z0-9_-]+|hscmp_[A-Za-z0-9_-]+)\b/);
408
+ return match?.[1];
409
+ } catch {
410
+ return void 0;
411
+ }
412
+ }
413
+ function bearerHeaderFor(consoleUrl) {
414
+ try {
415
+ const host = new URL(consoleUrl).hostname;
416
+ if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1") {
417
+ return {};
418
+ }
419
+ const token = readPairedToken();
420
+ return token ? { authorization: `Bearer ${token}` } : {};
421
+ } catch {
422
+ return {};
423
+ }
424
+ }
425
+ async function controlRequest(deps, req) {
426
+ const url = `${deps.consoleUrl}${req.path}`;
427
+ let res;
428
+ try {
429
+ res = await deps.fetchImpl(url, {
430
+ method: req.method,
431
+ headers: {
432
+ "content-type": "application/json",
433
+ // A remote console (cloud vault write mode, [[HS-PH7O-ILNC-DIAG-hs-check-report-cannot-authenticate]])
434
+ // authenticates every control call; loopback never asked, so the header
435
+ // is attached only when the target is non-loopback and a paired token
436
+ // exists. Same `hsmcp_` bearer the MCP server presents.
437
+ ...bearerHeaderFor(deps.consoleUrl)
438
+ },
439
+ ...req.body !== void 0 ? { body: JSON.stringify(req.body) } : {}
440
+ });
441
+ } catch (err) {
442
+ const message = err instanceof Error ? err.message : String(err);
443
+ return {
444
+ ok: false,
445
+ error: {
446
+ code: EXIT2.CONSOLE_UNREACHABLE,
447
+ message: `could not reach console at ${deps.consoleUrl}: ${message}
448
+ Start the dev server or set HEYSPEX_CONSOLE_URL.`,
449
+ payload: {
450
+ ok: false,
451
+ error: "console_unreachable",
452
+ url: deps.consoleUrl,
453
+ message
454
+ },
455
+ isError: true
456
+ }
457
+ };
458
+ }
459
+ let parsed = null;
460
+ try {
461
+ parsed = await res.json();
462
+ } catch {
463
+ }
464
+ if (res.status === 402) {
465
+ return {
466
+ ok: false,
467
+ error: {
468
+ code: EXIT2.NEON_REQUIRED,
469
+ message: "this command requires the Neon control plane, which is not configured (console returned 402).",
470
+ payload: {
471
+ ok: false,
472
+ error: "neon_required",
473
+ status: 402,
474
+ console_response: parsed
475
+ },
476
+ isError: true
477
+ }
478
+ };
479
+ }
480
+ return { ok: true, status: res.status, body: parsed };
481
+ }
482
+
483
+ // spex-console/src/lib/cli/defer.ts
484
+ import { readFileSync as readFileSync3 } from "node:fs";
485
+ import os from "node:os";
486
+ import path from "node:path";
487
+
488
+ // scripts/lib/console-url-resolver.mjs
489
+ import { readFileSync as readFileSync2 } from "node:fs";
490
+ import { homedir as homedir2 } from "node:os";
491
+ import { join as join2 } from "node:path";
492
+ var DEFAULT_CONSOLE_URL = "https://app.heyspex.com";
493
+
494
+ // spex-console/src/lib/cli/defer.ts
495
+ function resolveSpexConsoleUrl(vaultRoot, readDiscovery, opts = {}) {
496
+ const env = opts.env ?? process.env;
497
+ const raw = (env.HEYSPEX_CONSOLE_URL ?? env.HEYSPEX_COCKPIT_URL)?.trim();
498
+ if (raw && raw.length > 0) return raw.replace(/\/+$/, "");
499
+ try {
500
+ const homeDir = opts.homeDir ?? os.homedir();
501
+ const readFile = opts.readFile ?? ((p) => readFileSync3(p, "utf-8"));
502
+ const credentials = readFile(path.join(homeDir, ".heyspex", "credentials.yaml"));
503
+ const match = credentials.match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
504
+ const value = match?.[1]?.trim();
505
+ if (value && value.length > 0) return value.replace(/\/+$/, "");
506
+ } catch {
507
+ }
508
+ for (const name of ["console-url", "cockpit-url"]) {
509
+ try {
510
+ const discovery = readDiscovery(`${vaultRoot}/.heyspex/${name}`);
511
+ if (discovery && discovery.trim().length > 0) {
512
+ return discovery.trim().replace(/\/+$/, "");
513
+ }
514
+ } catch {
515
+ }
516
+ }
517
+ return DEFAULT_CONSOLE_URL;
518
+ }
519
+ var HELP2 = [
520
+ "Usage:",
521
+ " hs defer <spec-id> [--until <when>] [--json] [--no-daemon]",
522
+ " hs undefer <spec-id> [--json] [--no-daemon]",
523
+ "",
524
+ "Time-shift a spec via its `defer_until` frontmatter field. A deferred spec is",
525
+ "scheduler-invisible until the date passes, then auto-revives.",
526
+ "",
527
+ "Arguments:",
528
+ " <spec-id> The spec to defer/undefer (e.g. HS-TASK-EWL5).",
529
+ "",
530
+ "Flags:",
531
+ " --until <when> ISO date, or relative offset +Nd / +Nw / +Nm / +Ny.",
532
+ " Defaults to +1y (matches the console Defer button).",
533
+ " Ignored by `undefer` (which always clears the field).",
534
+ " --json Emit a machine-readable JSON result.",
535
+ " --no-daemon Write straight to the vault (no console needed; CI path).",
536
+ " --help, -h Show this help.",
537
+ "",
538
+ "Exit codes: 0 ok \xB7 1 usage \xB7 10 not-found \xB7 11 bad-arg \xB7 12 console-unreachable \xB7 13 write-failed"
539
+ ].join("\n");
540
+
541
+ // spex-console/bin/hs-check.ts
542
+ function manifestForSpecId(vaultRoot, specId) {
543
+ return path2.join(vaultRoot, "specs", "checks", `${specId}.checks.json`);
544
+ }
545
+ function resolveManifestPath(target, vaultRoot, invokedCwd) {
546
+ const literal = path2.resolve(invokedCwd, target);
547
+ if (existsSync(literal)) return literal;
548
+ if (target.endsWith(".json")) return null;
549
+ const byId = manifestForSpecId(vaultRoot, target);
550
+ return existsSync(byId) ? byId : null;
551
+ }
552
+ function countTscErrors(output) {
553
+ const matches = output.match(/error TS\d+/g);
554
+ return matches ? matches.length : 0;
555
+ }
556
+ var cachedPosixShell;
557
+ function resolveWindowsPosixShell() {
558
+ if (cachedPosixShell !== void 0) return cachedPosixShell;
559
+ const candidates = [
560
+ process2.env.HEYSPEX_CHECK_SHELL,
561
+ "C:\\Program Files\\Git\\bin\\bash.exe"
562
+ ].filter((c) => typeof c === "string" && c.length > 0);
563
+ for (const candidate of candidates) {
564
+ if (existsSync(candidate)) return cachedPosixShell = candidate;
565
+ }
566
+ const where = spawnSync("where", ["bash"], { encoding: "utf8", windowsHide: true });
567
+ if (where.status === 0 && where.stdout) {
568
+ const hit = where.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).find((line) => !/\\system32\\/i.test(line));
569
+ if (hit && existsSync(hit)) return cachedPosixShell = hit;
570
+ }
571
+ return cachedPosixShell = null;
572
+ }
573
+ var MISSING_BINARY_RE = /command not found|is not recognized as an internal or external command/i;
574
+ var warnedCmdFallback = false;
575
+ function runCheckInDirectory(check, cwd) {
576
+ return new Promise((resolve) => {
577
+ const started = Date.now();
578
+ const needsErrorCount = check.expect === "no-net-new";
579
+ const needsStdout = check.expect === "empty-output";
580
+ const stdio = ["ignore", "pipe", "pipe"];
581
+ const posixShell = process2.platform === "win32" ? resolveWindowsPosixShell() : null;
582
+ if (process2.platform === "win32" && !posixShell && !warnedCmdFallback) {
583
+ warnedCmdFallback = true;
584
+ process2.stderr.write(
585
+ "hs check: WARNING \u2014 no POSIX shell found (git-bash missing; set HEYSPEX_CHECK_SHELL to a bash.exe).\n Falling back to cmd.exe: POSIX-authored checks (test/grep/find/globs) will MISREPORT \u2014 false-fails, and inverted checks can false-PASS (HS-FCW8).\n"
586
+ );
587
+ }
588
+ const child = posixShell ? spawn(posixShell, ["-lc", check.run], { cwd, stdio, windowsHide: true }) : spawn(check.run, { shell: true, cwd, stdio });
589
+ let captured = "";
590
+ let stdoutOnly = "";
591
+ child.stdout?.on("data", (chunk) => {
592
+ captured += chunk.toString();
593
+ stdoutOnly += chunk.toString();
594
+ });
595
+ child.stderr?.on("data", (chunk) => {
596
+ captured += chunk.toString();
597
+ });
598
+ child.on("error", (err) => {
599
+ resolve({ exitCode: -1, durationMs: Date.now() - started, message: err.message });
600
+ });
601
+ child.on("close", (code) => {
602
+ const durationMs = Date.now() - started;
603
+ let exitCode = code ?? -1;
604
+ const missingBinary = exitCode === 0 && MISSING_BINARY_RE.test(captured);
605
+ if (missingBinary) exitCode = 127;
606
+ const outcome = { exitCode, durationMs, rawOutput: captured };
607
+ if (needsStdout) outcome.stdout = stdoutOnly;
608
+ if (needsErrorCount) {
609
+ outcome.errorCount = missingBinary ? check.baseline + 1 : countTscErrors(captured);
610
+ }
611
+ if (missingBinary) {
612
+ const line = captured.split("\n").find((l) => MISSING_BINARY_RE.test(l))?.trim();
613
+ outcome.message = `errored, not ok \u2014 shell reported a missing binary (${line ?? "command not found"})`;
614
+ } else if (exitCode !== 0 && check.expect === "exit-0") {
615
+ const trimmed = captured.trim();
616
+ outcome.message = trimmed.length > 0 ? trimmed.split("\n").slice(-3).join(" ") : `command exited ${exitCode}`;
617
+ }
618
+ resolve(outcome);
619
+ });
620
+ });
621
+ }
622
+ function makeRunCheck(vaultRoot) {
623
+ return async (check) => {
624
+ const cwds = resolveCheckCwds(check, {
625
+ repoRoot: vaultRoot,
626
+ consoleRoot: path2.join(vaultRoot, "spex-console")
627
+ });
628
+ let lastFailure = null;
629
+ for (const cwd of cwds) {
630
+ const outcome = await runCheckInDirectory(check, cwd);
631
+ if (evaluateCheck(check, outcome)) return outcome;
632
+ lastFailure = outcome;
633
+ }
634
+ if (!lastFailure) {
635
+ return { exitCode: -1, durationMs: 0, message: "no candidate cwd resolved for check" };
636
+ }
637
+ const zeroMatch = detectZeroMatch(lastFailure.rawOutput);
638
+ const detail = lastFailure.message ?? (zeroMatch !== null ? `zero-match: the check's filter matched nothing to run \u2014 "${zeroMatch}"` : check.expect === "no-net-new" && lastFailure.errorCount !== void 0 ? `${lastFailure.errorCount} errors exceeds baseline ${check.baseline}` : check.expect === "empty-output" ? `command printed output or exited ${lastFailure.exitCode}` : `command exited ${lastFailure.exitCode}`);
639
+ return {
640
+ ...lastFailure,
641
+ message: `all ${cwds.length} candidate cwd(s) tried \u2014 ${cwds.join(", ")}; last failure: ${detail}`
642
+ };
643
+ };
644
+ }
645
+ async function main() {
646
+ const vaultRoot = process2.env.HS_VAULT_ROOT;
647
+ if (!vaultRoot) {
648
+ process2.stderr.write(
649
+ "hs check: HS_VAULT_ROOT is not set (the dispatcher must resolve and pass it).\n"
650
+ );
651
+ process2.exit(EXIT.USAGE);
652
+ }
653
+ const parsed = parseCheckRunArgs(process2.argv.slice(3));
654
+ if (parsed.help) {
655
+ process2.stdout.write(`${HELP}
656
+ `);
657
+ process2.exit(EXIT.OK);
658
+ }
659
+ if (parsed.error) {
660
+ process2.stderr.write(`hs check: ${parsed.error.message}
661
+ `);
662
+ process2.exit(parsed.error.code);
663
+ }
664
+ const args = parsed.args;
665
+ const invokedCwd = process2.env.HS_INVOKED_CWD?.trim() || process2.cwd();
666
+ const manifestPath = resolveManifestPath(args.target, vaultRoot, invokedCwd);
667
+ if (!manifestPath) {
668
+ process2.stderr.write(
669
+ `hs check: no manifest for "${args.target}".
670
+ Pass a manifest path, or create ${path2.join("specs", "checks", `${args.target}.checks.json`)} under the vault root.
671
+ `
672
+ );
673
+ process2.exit(EXIT.NOT_FOUND);
674
+ }
675
+ const { manifest, error } = parseChecksManifest(readFileSync4(manifestPath, "utf-8"));
676
+ if (error) {
677
+ process2.stderr.write(`hs check: ${manifestPath}: ${error}
678
+ `);
679
+ process2.exit(EXIT.INVALID_MANIFEST);
680
+ }
681
+ const report = await runChecksManifest(manifest, { runCheck: makeRunCheck(vaultRoot) });
682
+ const sink = report.passed ? process2.stdout : process2.stderr;
683
+ if (args.json) {
684
+ sink.write(`${JSON.stringify(reportToJson(report))}
685
+ `);
686
+ } else {
687
+ sink.write(`${formatReport(report)}
688
+ `);
689
+ }
690
+ if (args.report) {
691
+ await reportVerdict(args.target, report, vaultRoot);
692
+ }
693
+ process2.exit(reportExitCode(report));
694
+ }
695
+ async function reportVerdict(target, report, vaultRoot) {
696
+ if (existsSync(target) || target.endsWith(".json")) {
697
+ process2.stderr.write(
698
+ "hs check: --report needs a spec-id target (to identify the claim), not a manifest path.\n"
699
+ );
700
+ return;
701
+ }
702
+ const consoleUrl = resolveSpexConsoleUrl(vaultRoot, (p) => {
703
+ try {
704
+ return readFileSync4(p, "utf-8");
705
+ } catch {
706
+ return null;
707
+ }
708
+ });
709
+ const verdict = buildVerdict(report);
710
+ const res = await controlRequest(
711
+ { fetchImpl: fetch, consoleUrl },
712
+ {
713
+ method: "PATCH",
714
+ path: `/api/specs/${encodeURIComponent(target)}/claim/status`,
715
+ body: { status: "working", verification: verdict }
716
+ }
717
+ );
718
+ if (!res.ok) {
719
+ process2.stderr.write(`hs check: verdict not reported \u2014 ${res.error.message}
720
+ `);
721
+ return;
722
+ }
723
+ if (res.status === 404) {
724
+ process2.stderr.write(
725
+ `hs check: no active claim for ${target} \u2014 verdict not reported (claim it first).
726
+ `
727
+ );
728
+ return;
729
+ }
730
+ if (res.status !== 200) {
731
+ process2.stderr.write(`hs check: verdict not reported \u2014 console returned ${res.status}.
732
+ `);
733
+ return;
734
+ }
735
+ const flipped = res.body?.autoShip;
736
+ const note = flipped && flipped.flipped !== false ? ` \u2192 gate flipped to ${flipped.effective_status}` : "";
737
+ process2.stderr.write(
738
+ `hs check: reported verdict (manifestPassed=${verdict.manifestPassed})${note}
739
+ `
740
+ );
741
+ }
742
+ main().catch((err) => {
743
+ const message = err instanceof Error ? err.message : String(err);
744
+ process2.stderr.write(`hs check: unhandled: ${message}
745
+ `);
746
+ process2.exit(EXIT.RUNNER_MISSING);
747
+ });