heyspex 0.2.4 → 0.2.6

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 (48) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/bin/hs.mjs +281 -30
  3. package/dev-factory/lib/dispatch-guard.mjs +75 -0
  4. package/dev-factory/lib/runner-path.mjs +51 -0
  5. package/dev-factory/spexcrawl/spexcrawl-cli.mjs +734 -0
  6. package/dist/cli/hs-connect.mjs +20 -1
  7. package/dist/cli/hs-defer.mjs +3802 -3744
  8. package/dist/cli/hs-events.mjs +1296 -0
  9. package/dist/cli/hs-export-events.mjs +1252 -0
  10. package/dist/cli/hs-handoff.mjs +2576 -0
  11. package/dist/cli/hs-index-rebuild.mjs +209 -36
  12. package/dist/cli/hs-index-reconcile.mjs +212 -39
  13. package/dist/cli/hs-init.mjs +20 -1
  14. package/dist/cli/hs-install.mjs +20 -1
  15. package/dist/cli/hs-join.mjs +20 -1
  16. package/dist/cli/hs-next-launch.mjs +2698 -0
  17. package/dist/cli/hs-pin.mjs +1336 -0
  18. package/dist/cli/hs-prereq.mjs +1380 -0
  19. package/dist/cli/hs-revalidate.mjs +3257 -0
  20. package/dist/cli/hs-spawn.mjs +12377 -0
  21. package/dist/cli/hs-test.mjs +1046 -0
  22. package/dist/cli/hs-trash.mjs +362 -0
  23. package/dist/cli/hs-trust.mjs +197 -0
  24. package/dist/cli/hs-vault-pull.mjs +214 -41
  25. package/dist/cli/hs-vault-push.mjs +212 -39
  26. package/dist/cli-templates/harness-commands/claude/hs-log.md +1 -1
  27. package/dist/cli-templates/harness-commands/claude/hs-res.md +71 -63
  28. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
  29. package/dist/cli-templates/harness-commands/cursor/hs-log.md +1 -1
  30. package/dist/cli-templates/harness-commands/cursor/hs-res.md +66 -58
  31. package/dist/cli-templates/harness-commands/cursor/hs-review.md +18 -18
  32. package/docs/copilot-plugin-install.md +2 -2
  33. package/package.json +9 -5
  34. package/plugins/heyspex/.claude-plugin/plugin.json +1 -1
  35. package/plugins/heyspex/.cursor-plugin/plugin.json +1 -1
  36. package/scripts/claude-hooks/wip-checkpoint.mjs +17 -2
  37. package/scripts/claude-statusline/statusline-command.mjs +287 -27
  38. package/scripts/hs-companion.mjs +8 -0
  39. package/scripts/install-claude-code-hooks.mjs +104 -3
  40. package/scripts/install-copilot-hooks.mjs +21 -1
  41. package/scripts/install-cursor-hooks.mjs +4 -3
  42. package/scripts/lib/capture-execution-guard.mjs +221 -0
  43. package/scripts/lib/customer-kit.mjs +48 -0
  44. package/scripts/lib/hs-companion-settings.mjs +25 -0
  45. package/scripts/record-grounding.mjs +38 -0
  46. package/vendor/heyspex-mcp/http-transport.mjs +1 -1
  47. package/vendor/heyspex-mcp/test-helpers.mjs +37 -23
  48. package/bin/postinstall.mjs +0 -88
@@ -1073,10 +1073,10 @@ var require_stringify = __commonJS({
1073
1073
  data = Object.assign({}, file.data, data);
1074
1074
  const open = opts.delimiters[0];
1075
1075
  const close = opts.delimiters[1];
1076
- const matter4 = engine.stringify(data, options2).trim();
1076
+ const matter5 = engine.stringify(data, options2).trim();
1077
1077
  let buf = "";
1078
- if (matter4 !== "{}") {
1079
- buf = newline(open) + newline(matter4) + newline(close);
1078
+ if (matter5 !== "{}") {
1079
+ buf = newline(open) + newline(matter5) + newline(close);
1080
1080
  }
1081
1081
  if (typeof file.excerpt === "string" && file.excerpt !== "") {
1082
1082
  if (str2.indexOf(file.excerpt.trim()) === -1) {
@@ -1182,19 +1182,19 @@ var require_gray_matter = __commonJS({
1182
1182
  var toFile = require_to_file();
1183
1183
  var parse2 = require_parse();
1184
1184
  var utils = require_utils();
1185
- function matter4(input, options2) {
1185
+ function matter5(input, options2) {
1186
1186
  if (input === "") {
1187
1187
  return { data: {}, content: input, excerpt: "", orig: input };
1188
1188
  }
1189
1189
  let file = toFile(input);
1190
- const cached = matter4.cache[file.content];
1190
+ const cached = matter5.cache[file.content];
1191
1191
  if (!options2) {
1192
1192
  if (cached) {
1193
1193
  file = Object.assign({}, cached);
1194
1194
  file.orig = cached.orig;
1195
1195
  return file;
1196
1196
  }
1197
- matter4.cache[file.content] = file;
1197
+ matter5.cache[file.content] = file;
1198
1198
  }
1199
1199
  return parseMatter(file, options2);
1200
1200
  }
@@ -1216,7 +1216,7 @@ var require_gray_matter = __commonJS({
1216
1216
  }
1217
1217
  str2 = str2.slice(openLen);
1218
1218
  const len = str2.length;
1219
- const language = matter4.language(str2, opts);
1219
+ const language = matter5.language(str2, opts);
1220
1220
  if (language.name) {
1221
1221
  file.language = language.name;
1222
1222
  str2 = str2.slice(language.raw.length);
@@ -1251,24 +1251,24 @@ var require_gray_matter = __commonJS({
1251
1251
  }
1252
1252
  return file;
1253
1253
  }
1254
- matter4.engines = engines2;
1255
- matter4.stringify = function(file, data, options2) {
1256
- if (typeof file === "string") file = matter4(file, options2);
1254
+ matter5.engines = engines2;
1255
+ matter5.stringify = function(file, data, options2) {
1256
+ if (typeof file === "string") file = matter5(file, options2);
1257
1257
  return stringify(file, data, options2);
1258
1258
  };
1259
- matter4.read = function(filepath, options2) {
1259
+ matter5.read = function(filepath, options2) {
1260
1260
  const str2 = fs12.readFileSync(filepath, "utf8");
1261
- const file = matter4(str2, options2);
1261
+ const file = matter5(str2, options2);
1262
1262
  file.path = filepath;
1263
1263
  return file;
1264
1264
  };
1265
- matter4.test = function(str2, options2) {
1265
+ matter5.test = function(str2, options2) {
1266
1266
  return utils.startsWith(str2, defaults(options2).delimiters[0]);
1267
1267
  };
1268
- matter4.language = function(str2, options2) {
1268
+ matter5.language = function(str2, options2) {
1269
1269
  const opts = defaults(options2);
1270
1270
  const open = opts.delimiters[0];
1271
- if (matter4.test(str2)) {
1271
+ if (matter5.test(str2)) {
1272
1272
  str2 = str2.slice(open.length);
1273
1273
  }
1274
1274
  const language = str2.slice(0, str2.search(/\r?\n/));
@@ -1277,11 +1277,11 @@ var require_gray_matter = __commonJS({
1277
1277
  name: language ? language.trim() : ""
1278
1278
  };
1279
1279
  };
1280
- matter4.cache = {};
1281
- matter4.clearCache = function() {
1282
- matter4.cache = {};
1280
+ matter5.cache = {};
1281
+ matter5.clearCache = function() {
1282
+ matter5.cache = {};
1283
1283
  };
1284
- module2.exports = matter4;
1284
+ module2.exports = matter5;
1285
1285
  }
1286
1286
  });
1287
1287
 
@@ -3068,11 +3068,12 @@ function getPostgresAdminDb() {
3068
3068
  function inertClient() {
3069
3069
  return false;
3070
3070
  }
3071
- var isNeonPool;
3071
+ var isNeonPool, registerVaultScopedDbAccessor;
3072
3072
  var init_postgres_client_cli_shim = __esm({
3073
3073
  "dist/cli/._postgres-client.cli-shim.mjs"() {
3074
3074
  "use strict";
3075
3075
  isNeonPool = inertClient;
3076
+ registerVaultScopedDbAccessor = inertClient;
3076
3077
  }
3077
3078
  });
3078
3079
 
@@ -3323,6 +3324,141 @@ var init_clients = __esm({
3323
3324
  }
3324
3325
  });
3325
3326
 
3327
+ // spex-console/src/lib/rate-limits.ts
3328
+ var RATE_LIMIT_BUDGETS;
3329
+ var init_rate_limits = __esm({
3330
+ "spex-console/src/lib/rate-limits.ts"() {
3331
+ "use strict";
3332
+ RATE_LIMIT_BUDGETS = {
3333
+ mcp_tool_calls: { daily: 500, hourly: 125 },
3334
+ spec_writes: { daily: 100, hourly: 25 },
3335
+ capture_sessions: { daily: 20, hourly: 5 },
3336
+ // Paul's Advocate free-text chat ([[HS-4YYB-XEBI-TASK-paul-s-advocate-answers-real-questions]]):
3337
+ // a public YC reviewer can otherwise run up LLM spend on an unauthenticated-feeling
3338
+ // widget. 30/hour matches the builder brief's "e.g. 30 messages/hour" starting point;
3339
+ // daily is 4x hourly (looser than the 25%-of-daily pattern above — a reviewer chatting
3340
+ // steadily through a demo session should not get cut off mid-conversation).
3341
+ advocate_chat: { daily: 120, hourly: 30 }
3342
+ };
3343
+ }
3344
+ });
3345
+
3346
+ // spex-console/src/lib/plan-tier.ts
3347
+ function scaledBudgets(multiplier) {
3348
+ const out = {};
3349
+ for (const [bucket, budget] of Object.entries(RATE_LIMIT_BUDGETS)) {
3350
+ out[bucket] = {
3351
+ daily: Math.round(budget.daily * multiplier),
3352
+ hourly: Math.round(budget.hourly * multiplier)
3353
+ };
3354
+ }
3355
+ return Object.freeze(out);
3356
+ }
3357
+ var PLAN_TIER_RATE_LIMIT_MULTIPLIER, PLAN_TIER_LIMITS;
3358
+ var init_plan_tier = __esm({
3359
+ "spex-console/src/lib/plan-tier.ts"() {
3360
+ "use strict";
3361
+ init_sql_orm_inert_cli_shim();
3362
+ init_postgres_client_cli_shim();
3363
+ init_postgres();
3364
+ init_rate_limits();
3365
+ PLAN_TIER_RATE_LIMIT_MULTIPLIER = {
3366
+ free: 1,
3367
+ pro: 4,
3368
+ enterprise: 20
3369
+ };
3370
+ PLAN_TIER_LIMITS = {
3371
+ free: {
3372
+ statement_timeout_ms: 5e3,
3373
+ read_work_mem: "4MB",
3374
+ rate_limit_budgets: scaledBudgets(PLAN_TIER_RATE_LIMIT_MULTIPLIER.free)
3375
+ },
3376
+ pro: {
3377
+ statement_timeout_ms: 15e3,
3378
+ read_work_mem: "16MB",
3379
+ rate_limit_budgets: scaledBudgets(PLAN_TIER_RATE_LIMIT_MULTIPLIER.pro)
3380
+ },
3381
+ enterprise: {
3382
+ statement_timeout_ms: 3e4,
3383
+ read_work_mem: "64MB",
3384
+ rate_limit_budgets: scaledBudgets(PLAN_TIER_RATE_LIMIT_MULTIPLIER.enterprise)
3385
+ }
3386
+ };
3387
+ }
3388
+ });
3389
+
3390
+ // spex-console/src/lib/db/clients/vault-router.ts
3391
+ import { existsSync as existsSync8, readFileSync as readFileSync13 } from "node:fs";
3392
+ import { homedir as homedir10 } from "node:os";
3393
+ import { join as join11 } from "node:path";
3394
+ var CREDENTIALS_PATH, ROUTER_KEY, globalRouter, router;
3395
+ var init_vault_router = __esm({
3396
+ "spex-console/src/lib/db/clients/vault-router.ts"() {
3397
+ "use strict";
3398
+ init_sql_orm_inert_cli_shim();
3399
+ init_sql_orm_inert_cli_shim();
3400
+ init_postgres();
3401
+ init_postgres_client_cli_shim();
3402
+ CREDENTIALS_PATH = join11(homedir10(), ".heyspex", "credentials.yaml");
3403
+ ROUTER_KEY = Symbol.for("heyspex.vaultRouter");
3404
+ globalRouter = globalThis;
3405
+ if (!globalRouter[ROUTER_KEY]) {
3406
+ globalRouter[ROUTER_KEY] = {
3407
+ dedicated: /* @__PURE__ */ new Map(),
3408
+ refCache: /* @__PURE__ */ new Map(),
3409
+ warnedSchemaMissing: false
3410
+ };
3411
+ }
3412
+ router = globalRouter[ROUTER_KEY];
3413
+ }
3414
+ });
3415
+
3416
+ // spex-console/src/lib/db/clients/vault-scope.ts
3417
+ import { AsyncLocalStorage } from "node:async_hooks";
3418
+ function warnScopeClosedOnce(ctx) {
3419
+ if (ctx.warnedClosed) return;
3420
+ ctx.warnedClosed = true;
3421
+ console.warn(
3422
+ `[vault-scope] db use after vault scope closed; falling back to the pool connection (unscoped) (vaultId: ${ctx.vaultId})`
3423
+ );
3424
+ }
3425
+ function handleScopeClosedAccess(ctx) {
3426
+ if (ctx.onStaleUse === "throw") {
3427
+ throw new VaultScopeClosedError(ctx.vaultId);
3428
+ }
3429
+ warnScopeClosedOnce(ctx);
3430
+ }
3431
+ var VaultScopeClosedError, vaultScopeStorage;
3432
+ var init_vault_scope = __esm({
3433
+ "spex-console/src/lib/db/clients/vault-scope.ts"() {
3434
+ "use strict";
3435
+ init_sql_orm_inert_cli_shim();
3436
+ init_plan_tier();
3437
+ init_postgres_client_cli_shim();
3438
+ init_vault_router();
3439
+ VaultScopeClosedError = class extends Error {
3440
+ vaultId;
3441
+ constructor(vaultId) {
3442
+ super(
3443
+ `Vault scope for "${vaultId}" is closed; this path opted into onStaleUseAfterClose: "throw", so a db handle used after the scope's transaction settled fails loudly instead of silently falling back to the unscoped pool connection.`
3444
+ );
3445
+ this.name = "VaultScopeClosedError";
3446
+ this.vaultId = vaultId;
3447
+ }
3448
+ };
3449
+ vaultScopeStorage = new AsyncLocalStorage();
3450
+ registerVaultScopedDbAccessor(() => {
3451
+ const ctx = vaultScopeStorage.getStore();
3452
+ if (!ctx) return null;
3453
+ if (ctx.closed) {
3454
+ handleScopeClosedAccess(ctx);
3455
+ return null;
3456
+ }
3457
+ return ctx.proxy;
3458
+ });
3459
+ }
3460
+ });
3461
+
3326
3462
  // spex-console/src/lib/scheduler/types.ts
3327
3463
  var init_types2 = __esm({
3328
3464
  "spex-console/src/lib/scheduler/types.ts"() {
@@ -3857,10 +3993,45 @@ var init_config = __esm({
3857
3993
  }
3858
3994
  });
3859
3995
 
3860
- // spex-console/src/lib/vault-frontmatter.ts
3996
+ // spex-console/src/lib/safe-yaml.ts
3861
3997
  import yaml2 from "js-yaml";
3998
+ function dropUndefinedDeep(value) {
3999
+ if (Array.isArray(value)) {
4000
+ return value.filter((item) => item !== void 0).map((item) => dropUndefinedDeep(item));
4001
+ }
4002
+ if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
4003
+ const out = {};
4004
+ for (const [k, v] of Object.entries(value)) {
4005
+ if (v === void 0) continue;
4006
+ out[k] = dropUndefinedDeep(v);
4007
+ }
4008
+ return out;
4009
+ }
4010
+ return value;
4011
+ }
4012
+ function safeYamlDump(value, options2 = {}) {
4013
+ const { schema: _ignoredSchema, ...rest } = options2;
4014
+ const data = dropUndefinedDeep(value);
4015
+ if (grayMatterYamlStringify) return grayMatterYamlStringify(data, rest);
4016
+ return yaml2.dump(data, { ...rest, schema: SAFE_YAML_SCHEMA });
4017
+ }
4018
+ var import_gray_matter, SAFE_YAML_SCHEMA, grayMatterYamlStringify, YAML_FAILSAFE_SCHEMA;
4019
+ var init_safe_yaml = __esm({
4020
+ "spex-console/src/lib/safe-yaml.ts"() {
4021
+ "use strict";
4022
+ import_gray_matter = __toESM(require_gray_matter(), 1);
4023
+ SAFE_YAML_SCHEMA = yaml2.DEFAULT_SAFE_SCHEMA ?? yaml2.DEFAULT_SCHEMA;
4024
+ grayMatterYamlStringify = (() => {
4025
+ const engine = import_gray_matter.default.engines?.yaml;
4026
+ return typeof engine?.stringify === "function" ? engine.stringify : void 0;
4027
+ })();
4028
+ YAML_FAILSAFE_SCHEMA = yaml2.FAILSAFE_SCHEMA;
4029
+ }
4030
+ });
4031
+
4032
+ // spex-console/src/lib/vault-frontmatter.ts
3862
4033
  function normalizeFrontmatterForYaml(frontmatter) {
3863
- const processed = { ...frontmatter };
4034
+ const processed = dropUndefinedDeep(frontmatter);
3864
4035
  for (const [key, value] of Object.entries(processed)) {
3865
4036
  if (value instanceof Date) {
3866
4037
  processed[key] = value.toISOString();
@@ -3869,20 +4040,21 @@ function normalizeFrontmatterForYaml(frontmatter) {
3869
4040
  return processed;
3870
4041
  }
3871
4042
  function stringifySpecMarkdown(body, frontmatter) {
3872
- const fm = yaml2.dump(normalizeFrontmatterForYaml(frontmatter), YAML_STRINGIFY_OPTIONS).trim();
4043
+ const fm = safeYamlDump(normalizeFrontmatterForYaml(frontmatter), YAML_STRINGIFY_OPTIONS).trim();
3873
4044
  return `---
3874
4045
  ${fm}
3875
4046
  ---
3876
4047
  ${body}`;
3877
4048
  }
3878
4049
  function parseSpecMarkdown(raw) {
3879
- return (0, import_gray_matter.default)(raw);
4050
+ return (0, import_gray_matter2.default)(raw);
3880
4051
  }
3881
- var import_gray_matter, YAML_STRINGIFY_OPTIONS;
4052
+ var import_gray_matter2, YAML_STRINGIFY_OPTIONS;
3882
4053
  var init_vault_frontmatter = __esm({
3883
4054
  "spex-console/src/lib/vault-frontmatter.ts"() {
3884
4055
  "use strict";
3885
- import_gray_matter = __toESM(require_gray_matter(), 1);
4056
+ import_gray_matter2 = __toESM(require_gray_matter(), 1);
4057
+ init_safe_yaml();
3886
4058
  YAML_STRINGIFY_OPTIONS = {
3887
4059
  quotingType: '"',
3888
4060
  skipInvalid: true,
@@ -4777,11 +4949,11 @@ var init_guardrail_naming = __esm({
4777
4949
  import { createHash as createHash3 } from "node:crypto";
4778
4950
  import { promises as fs6 } from "node:fs";
4779
4951
  import path9 from "node:path";
4780
- var import_gray_matter2;
4952
+ var import_gray_matter3;
4781
4953
  var init_parser = __esm({
4782
4954
  "spex-console/src/lib/indexer/parser.ts"() {
4783
4955
  "use strict";
4784
- import_gray_matter2 = __toESM(require_gray_matter(), 1);
4956
+ import_gray_matter3 = __toESM(require_gray_matter(), 1);
4785
4957
  init_vault_id();
4786
4958
  init_vault_shared();
4787
4959
  }
@@ -4920,12 +5092,12 @@ var init_linkage_validation = __esm({
4920
5092
  });
4921
5093
 
4922
5094
  // spex-console/src/lib/perf-timing.ts
4923
- import { AsyncLocalStorage } from "node:async_hooks";
5095
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
4924
5096
  var storage;
4925
5097
  var init_perf_timing = __esm({
4926
5098
  "spex-console/src/lib/perf-timing.ts"() {
4927
5099
  "use strict";
4928
- storage = new AsyncLocalStorage();
5100
+ storage = new AsyncLocalStorage2();
4929
5101
  }
4930
5102
  });
4931
5103
 
@@ -4947,12 +5119,12 @@ var init_planning_flow_model = __esm({
4947
5119
  });
4948
5120
 
4949
5121
  // spex-console/src/lib/request-memo.ts
4950
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
5122
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
4951
5123
  var storage2;
4952
5124
  var init_request_memo = __esm({
4953
5125
  "spex-console/src/lib/request-memo.ts"() {
4954
5126
  "use strict";
4955
- storage2 = new AsyncLocalStorage2();
5127
+ storage2 = new AsyncLocalStorage3();
4956
5128
  }
4957
5129
  });
4958
5130
 
@@ -5200,18 +5372,17 @@ var init_search_body_hit = __esm({
5200
5372
 
5201
5373
  // spex-console/src/lib/vault.ts
5202
5374
  import { spawn } from "node:child_process";
5203
- import { readdirSync as readdirSync2, readFileSync as readFileSync13, statSync as statSync5 } from "node:fs";
5375
+ import { readdirSync as readdirSync2, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
5204
5376
  import { promises as fs9 } from "node:fs";
5205
5377
  import path14 from "node:path";
5206
- import yaml3 from "js-yaml";
5207
5378
  function vaultRoot() {
5208
5379
  return getActiveSpecsDir();
5209
5380
  }
5210
- var import_gray_matter3, VAULT_CACHE_TTL_WATCHED_MS, VAULT_CACHE_KEY, vaultCacheStore, VAULT_CACHE_DELTA_LISTENERS_KEY, vaultCacheDeltaListeners, SEARCH_TEXT_KEY, searchTextStore, VAULT_SNAPSHOT_KEY, vaultSnapshotStore;
5381
+ var import_gray_matter4, VAULT_CACHE_TTL_WATCHED_MS, VAULT_CACHE_KEY, vaultCacheStore, VAULT_CACHE_DELTA_LISTENERS_KEY, vaultCacheDeltaListeners, SEARCH_TEXT_KEY, searchTextStore, VAULT_SNAPSHOT_KEY, vaultSnapshotStore;
5211
5382
  var init_vault = __esm({
5212
5383
  "spex-console/src/lib/vault.ts"() {
5213
5384
  "use strict";
5214
- import_gray_matter3 = __toESM(require_gray_matter(), 1);
5385
+ import_gray_matter4 = __toESM(require_gray_matter(), 1);
5215
5386
  init_diagnose_skeleton();
5216
5387
  init_auth();
5217
5388
  init_capture_vault();
@@ -5222,6 +5393,7 @@ var init_vault = __esm({
5222
5393
  init_skips();
5223
5394
  init_clients();
5224
5395
  init_postgres_client_cli_shim();
5396
+ init_vault_scope();
5225
5397
  init_helpers();
5226
5398
  init_index_write_service();
5227
5399
  init_spec_read_service();
@@ -5238,6 +5410,7 @@ var init_vault = __esm({
5238
5410
  init_planning_flow_model();
5239
5411
  init_request_memo();
5240
5412
  init_review_mode();
5413
+ init_safe_yaml();
5241
5414
  init_active_milestone();
5242
5415
  init_area();
5243
5416
  init_claim_eligibility();
@@ -5322,7 +5495,7 @@ var init_vault = __esm({
5322
5495
  });
5323
5496
 
5324
5497
  // spex-console/bin/hs-vault-push.ts
5325
- import { readFileSync as readFileSync15 } from "node:fs";
5498
+ import { readFileSync as readFileSync16 } from "node:fs";
5326
5499
  import path15 from "node:path";
5327
5500
  import process4 from "node:process";
5328
5501
 
@@ -5706,7 +5879,7 @@ async function writeDirtyMarker(vaultRoot2, id, now = () => /* @__PURE__ */ new
5706
5879
  }
5707
5880
 
5708
5881
  // spex-console/src/lib/vault-sync/pull-io.ts
5709
- import { promises as fs10, readFileSync as readFileSync14 } from "node:fs";
5882
+ import { promises as fs10, readFileSync as readFileSync15 } from "node:fs";
5710
5883
  import { EOL } from "node:os";
5711
5884
  import nodePath3 from "node:path";
5712
5885
 
@@ -6352,7 +6525,7 @@ function runEntry(name, main2, options2 = {}) {
6352
6525
  // spex-console/bin/hs-vault-push.ts
6353
6526
  function readMarkerVaultId(vaultRoot2) {
6354
6527
  try {
6355
- return parseVaultId(readFileSync15(path15.join(vaultRoot2, "heyspex.yaml"), "utf8"));
6528
+ return parseVaultId(readFileSync16(path15.join(vaultRoot2, "heyspex.yaml"), "utf8"));
6356
6529
  } catch {
6357
6530
  return null;
6358
6531
  }
@@ -20,7 +20,7 @@ Admin/dev-only — resolves paths via `dev-factory/watch-dev-log.mjs`, then watc
20
20
  2. **Watch it.** Start a background `tail -f` over the resolved path(s) and stream it:
21
21
  - Default: filtered to signal only — `grep -E --line-buffered -i "error|warn|exception|fatal|crash| 4[0-9][0-9] | 5[0-9][0-9] "` — so routine 200-OK request logs stay silent and only real problems surface.
22
22
  - `--raw` in `$ARGUMENTS`: skip the filter, stream everything (useful for a first look, like the very first time this was set up).
23
- - Multiple paths (e.g. `companion`'s 4 files) can be tailed together: `tail -n 0 -f path1 path2 ...`.
23
+ - Multiple paths (e.g. `companion`'s 3 files) can be tailed together: `tail -n 0 -f path1 path2 ...`.
24
24
 
25
25
  3. **Report** the resolved path(s) and whether you're running filtered or raw, then stay quiet until something actually worth flagging comes through — don't narrate routine matched lines individually if they're clearly benign.
26
26
 
@@ -1,63 +1,71 @@
1
- ---
2
- description: Confidence-ladder research method's entrypoint — SpexCrawl via the Companion (search/gather) is PRIMARY + DeepSeek (synthesis); self-hosted Firecrawl is an AUTOMATIC fallback (no permission needed, owner ruling 2026-08-22), zero Claude/Anthropic involvement.
3
- argument-hint: "[research question/topic]"
4
- ---
5
-
6
- # Research (`/hs-res`)
7
-
8
- > Parity: `.commandcode/skills/hs-res/SKILL.md` · `.cursor/skills/hs-res/SKILL.md` · `.agents/skills/hs-res/SKILL.md`
9
-
10
- ## What to do
11
-
12
- 1. **Take the question.** `$ARGUMENTS` is the research question/topic. If empty, load the
13
- console doc-toolbar Promote pin via `heyspex_active_research`
14
- (`.heyspex/research-focus.yaml` → `source_idea_id`, `scope`, `spec_path`) and read the source
15
- capture body as the question. Either way, restate the scope and confirm with the user.
16
-
17
- 2. **Gather via SpexCrawl (the Companion) — the PRIMARY method** (owner ruling 2026-08-14,
18
- [[spexcrawl-through-the-companion-is-the]]: "Companion > SpexCrawl is now default method
19
- for research"). Decompose into a few independent search angles, then per angle:
20
- `node dev-factory/spexcrawl/spexcrawl-cli.mjs search "<query>" --json` — one call
21
- searches AND returns the cleaned pages of the top results. Specific extra URLs:
22
- `node dev-factory/spexcrawl/spexcrawl-cli.mjs scrape "<url>" --json`.
23
- Prerequisites: the Companion running and paired to the console. LAN mode
24
- (`npm run console:log:lan`) is needed ONLY when the Companion runs on a DIFFERENT
25
- machine than the console — since HS-V9WH the Companion polls loopback consoles too
26
- ([[a-queue-is-dead-if-its-only-consumer]]: the LAN line was a bug workaround
27
- misdocumented as a requirement). When missing, the CLI exits 3 with
28
- `SPEXCRAWL_UNAVAILABLE` naming the fix; a silent hang means the Companion is not
29
- running or not polling this console.
30
-
31
- **Fallback — self-hosted Firecrawl, AUTOMATIC, no permission needed (owner ruling
32
- 2026-08-22).** When the SpexCrawl path malfunctions, times out, hits a gate, or is otherwise
33
- unavailable, fall back immediately: `node dev-factory/spexcrawl/firecrawl-cli.mjs
34
- search "<query>" --json` and `node dev-factory/spexcrawl/firecrawl-cli.mjs scrape
35
- "<url>" --json`. May run on any fleet machine (coffee-geek, basement-geek, spec-ops) —
36
- prefer other reachable machines over the one the orchestrating session runs on; check
37
- `docker info`, start Docker/the stack if needed, poll the real endpoint for readiness before
38
- use. If this task's changed/claimed files are SpexCrawl-related, skip SpexCrawl entirely and
39
- go straight to Firecrawl. Hosted `api.firecrawl.dev` credits are unchanged — still
40
- ask-before-spend every time. Full contract: `.commandcode/skills/hs-res/SKILL.md`.
41
-
42
- 3. **Assemble a numbered sources block** (`[S1]`, `[S2]`, …) from the gathered
43
- `{title, url, snippet, markdown}` results, write it plus the question and cite-only
44
- instructions to a temp prompt file.
45
-
46
- 4. **Synthesize via DeepSeek** —
47
- `node dev-factory/spexcrawl/synthesize-cli.mjs --prompt-file <path> --json`
48
- (defaults `--max-tokens` to 32000 and auto-escalates ×3 up to a 96000 ceiling if DeepSeek's
49
- default-on "thinking" mode burns the whole budget on reasoning — never pass a lower
50
- `--max-tokens` yourself; the CLI self-heals this failure mode. Exhausted escalation exits 3
51
- with `SYNTHESIZE_UNAVAILABLE`).
52
-
53
- 5. **Save** via `heyspex_save_research` (or `heyspex_complete_research` under the console pin
54
- pipeline), with `sources` populated from the gathered URLs.
55
-
56
- **Never fall back** to `deep-research`/`deep-research-quick|medium|thorough`, the
57
- `Workflow({ scriptPath: '.claude/workflows/deep-research-*.js' })` launch, or native
58
- `WebSearch`/`WebFetch` for this method. Self-hosted Firecrawl fallback is automatic (no
59
- permission needed, 2026-08-22 ruling); hosted `api.firecrawl.dev` credits still need the
60
- user's explicit per-run permission, every time. If a CLI reports unavailable
61
- (`SPEXCRAWL_UNAVAILABLE` / `FIRECRAWL_UNAVAILABLE` / `SYNTHESIZE_UNAVAILABLE`, exit 3),
62
- **stop** and tell the user — never silently substitute another backend. Full method,
63
- escalation rule, and hard prohibitions: `.commandcode/skills/hs-res/SKILL.md`.
1
+ ---
2
+ description: Confidence-ladder research method's entrypoint — SpexCrawl via the Companion (search/gather) is PRIMARY + DeepSeek (synthesis); self-hosted Firecrawl is an AUTOMATIC fallback (no permission needed, owner ruling 2026-08-22), zero Claude/Anthropic involvement.
3
+ argument-hint: "[research question/topic]"
4
+ ---
5
+
6
+ # Research (`/hs-res`)
7
+
8
+ > Parity: `.commandcode/skills/hs-res/SKILL.md` · `.cursor/skills/hs-res/SKILL.md` · `.agents/skills/hs-res/SKILL.md`
9
+
10
+ ## What to do
11
+
12
+ 1. **Take the question.** `$ARGUMENTS` is the research question/topic. If empty, load the
13
+ console doc-toolbar Promote pin via `heyspex_active_research`
14
+ (`.heyspex/research-focus.yaml` → `source_idea_id`, `scope`, `spec_path`) and read the source
15
+ capture body as the question. Either way, restate the scope and confirm with the user.
16
+
17
+ 2. **Gather via SpexCrawl (the Companion) — the PRIMARY method** (owner ruling 2026-08-14,
18
+ [[spexcrawl-through-the-companion-is-the]]: "Companion > SpexCrawl is now default method
19
+ for research"). Decompose into a few independent search angles, then per angle:
20
+ `node dev-factory/spexcrawl/spexcrawl-cli.mjs search "<query>" --json` — one call
21
+ searches AND returns the cleaned pages of the top results. Specific extra URLs:
22
+ `node dev-factory/spexcrawl/spexcrawl-cli.mjs scrape "<url>" --json`.
23
+ Prerequisites: the Companion running and paired to the console. LAN mode
24
+ (`npm run console:log:lan`) is needed ONLY when the Companion runs on a DIFFERENT
25
+ machine than the console — since HS-V9WH the Companion polls loopback consoles too
26
+ ([[a-queue-is-dead-if-its-only-consumer]]: the LAN line was a bug workaround
27
+ misdocumented as a requirement). When missing, the CLI exits 3 with
28
+ `SPEXCRAWL_UNAVAILABLE` naming the fix; a silent hang means the Companion is not
29
+ running or not polling this console.
30
+
31
+ **Fallback — self-hosted Firecrawl, AUTOMATIC, no permission needed (owner ruling
32
+ 2026-08-22).** When the SpexCrawl path malfunctions, times out, hits a gate, or is otherwise
33
+ unavailable, fall back immediately: `node dev-factory/spexcrawl/firecrawl-cli.mjs
34
+ search "<query>" --json` and `node dev-factory/spexcrawl/firecrawl-cli.mjs scrape
35
+ "<url>" --json`. May run on any fleet machine (coffee-geek, basement-geek, spec-ops) —
36
+ prefer other reachable machines over the one the orchestrating session runs on; check
37
+ `docker info`, start Docker/the stack if needed, poll the real endpoint for readiness before
38
+ use. If this task's changed/claimed files are SpexCrawl-related, skip SpexCrawl entirely and
39
+ go straight to Firecrawl. Hosted `api.firecrawl.dev` credits are unchanged — still
40
+ ask-before-spend every time. Full contract: `.commandcode/skills/hs-res/SKILL.md`.
41
+
42
+ **In a repo that uses the `heyspex` npm package** (not a HeySpex platform checkout): the
43
+ package ships only the SpexCrawl client (`dev-factory/spexcrawl/spexcrawl-cli.mjs` under
44
+ `node_modules/heyspex/`). It needs the HeySpex Companion, which the npm package does not
45
+ distribute yet, so it usually exits 3 `SPEXCRAWL_UNAVAILABLE`. The Firecrawl fallback and
46
+ the DeepSeek synthesis CLI (`firecrawl-cli.mjs`, `synthesize-cli.mjs`) are HeySpex-platform
47
+ only and are not in the package. When a CLI named in steps 2 or 4 is missing or reports
48
+ unavailable, stop and tell the user this research method is not available in their install.
49
+
50
+ 3. **Assemble a numbered sources block** (`[S1]`, `[S2]`, …) from the gathered
51
+ `{title, url, snippet, markdown}` results, write it plus the question and cite-only
52
+ instructions to a temp prompt file.
53
+
54
+ 4. **Synthesize via DeepSeek** —
55
+ `node dev-factory/spexcrawl/synthesize-cli.mjs --prompt-file <path> --json`
56
+ (defaults `--max-tokens` to 32000 and auto-escalates ×3 up to a 96000 ceiling if DeepSeek's
57
+ default-on "thinking" mode burns the whole budget on reasoning — never pass a lower
58
+ `--max-tokens` yourself; the CLI self-heals this failure mode. Exhausted escalation exits 3
59
+ with `SYNTHESIZE_UNAVAILABLE`).
60
+
61
+ 5. **Save** via `heyspex_save_research` (or `heyspex_complete_research` under the console pin
62
+ pipeline), with `sources` populated from the gathered URLs.
63
+
64
+ **Never fall back** to `deep-research`/`deep-research-quick|medium|thorough`, the
65
+ `Workflow({ scriptPath: '.claude/workflows/deep-research-*.js' })` launch, or native
66
+ `WebSearch`/`WebFetch` for this method. Self-hosted Firecrawl fallback is automatic (no
67
+ permission needed, 2026-08-22 ruling); hosted `api.firecrawl.dev` credits still need the
68
+ user's explicit per-run permission, every time. If a CLI reports unavailable
69
+ (`SPEXCRAWL_UNAVAILABLE` / `FIRECRAWL_UNAVAILABLE` / `SYNTHESIZE_UNAVAILABLE`, exit 3),
70
+ **stop** and tell the user — never silently substitute another backend. Full method,
71
+ escalation rule, and hard prohibitions: `.commandcode/skills/hs-res/SKILL.md`.
@@ -1,23 +1,23 @@
1
- ---
2
- description: Pull the next review-ready spec from the HeySpex review queue (review-focus.yaml → manifest traversal of `status: in_review` with HS-REV companion → folder scan).
3
- argument-hint: "[HS-XXXX-TASK | HS-XXXX-EPIC] [--ignore-focus]"
4
- allowed-tools: mcp__heyspex__heyspex_next_review
5
- ---
6
-
7
- Invoke the `mcp__heyspex__heyspex_next_review` MCP tool to fetch the next review-ready spec from the HeySpex vault, then paste the returned `prompt_markdown` verbatim so the user (reviewer) can walk acceptance against the parent spec + HS-REV companion.
8
-
9
- Argument parsing for `$ARGUMENTS` (all optional; if `$ARGUMENTS` is empty, call the tool with no parameters):
10
-
11
- - A bare token matching `HS-TASK-*` / `HS-EPIC-*` / `HS-*-*` → pass as `spec_id`.
12
- - A bare token matching `HS-ROADMAP-*` → pass as `roadmap_id`.
13
- - `--ignore-focus` → set `ignore_focus: true`.
14
-
15
- Resolution precedence (built into the tool, do not re-implement here): `args.spec_id` → `.heyspex/review-focus.yaml` pin → manifest traversal for `status: in_review` specs with an `HS-REV-<suffix>-*.md` companion → folder scan as fallback. Sorted by `updated:` ascending (oldest first).
16
-
17
- After the tool returns, output the `prompt_markdown` field as-is, then on a final line state the source: `_Source: {source_kind} · {spec_id} · phase {phase_id} · age {age_in_review_days}d_`. Do not summarize, paraphrase, or start executing the returned prompt.
18
-
19
- This skill does NOT claim the spec. Reviewing is a human-in-loop activity by design (CLAUDE.md §Session Completion); the reviewer walks the four sections of the HS-REV companion (`## Acceptance verification`, `## Reviewer notes`, `## Memory candidates`, `## Decision: close or reopen`) and decides pass/reopen. Memory candidates are scored against the four-axis rubric (non-derivability / reusability / surprise-cost / concreteness, 0–3 each, threshold ≥ 7/12) — the prompt body includes the rubric and the conflict-check instruction.
20
-
21
- If the tool errors with `NO_REVIEW_READY`, tell the user the review queue is empty (no specs at `status: in_review` with an HS-REV companion) and suggest checking `specs/reviews/` for stale companions or filing the missing review doc. If `SPEC_NOT_IN_REVIEW`, surface the spec's actual status so the user can pick the right `/hs-review HS-XXXX` target. If `NO_REVIEW_DOC`, the parent spec is `in_review` but has no `HS-REV-<suffix>-*.md` companion — point the user at CLAUDE.md §Session Completion §3 for the four-section template.
22
-
23
- Cross-reference: [[HS-XRO1-RVDC-TASK-heyspex-review-on-deck-mcp-and-hs-review]] (spec), [[HS-UKD6-CLR4-RES-close-out-review-screen-jtbd-architecture]] §J17 (downstream Review surface that will call this tool from the "Copy `/hs-review`" affordance).
1
+ ---
2
+ description: 'Pull the next review-ready spec from the HeySpex review queue (review-focus.yaml → manifest traversal of `status: in_review` with HS-REV companion → folder scan).'
3
+ argument-hint: "[HS-XXXX-TASK | HS-XXXX-EPIC] [--ignore-focus]"
4
+ allowed-tools: mcp__heyspex__heyspex_next_review
5
+ ---
6
+
7
+ Invoke the `mcp__heyspex__heyspex_next_review` MCP tool to fetch the next review-ready spec from the HeySpex vault, then paste the returned `prompt_markdown` verbatim so the user (reviewer) can walk acceptance against the parent spec + HS-REV companion.
8
+
9
+ Argument parsing for `$ARGUMENTS` (all optional; if `$ARGUMENTS` is empty, call the tool with no parameters):
10
+
11
+ - A bare token matching `HS-TASK-*` / `HS-EPIC-*` / `HS-*-*` → pass as `spec_id`.
12
+ - A bare token matching `HS-ROADMAP-*` → pass as `roadmap_id`.
13
+ - `--ignore-focus` → set `ignore_focus: true`.
14
+
15
+ Resolution precedence (built into the tool, do not re-implement here): `args.spec_id` → `.heyspex/review-focus.yaml` pin → manifest traversal for `status: in_review` specs with an `HS-REV-<suffix>-*.md` companion → folder scan as fallback. Sorted by `updated:` ascending (oldest first).
16
+
17
+ After the tool returns, output the `prompt_markdown` field as-is, then on a final line state the source: `_Source: {source_kind} · {spec_id} · phase {phase_id} · age {age_in_review_days}d_`. Do not summarize, paraphrase, or start executing the returned prompt.
18
+
19
+ This skill does NOT claim the spec. Reviewing is a human-in-loop activity by design (CLAUDE.md §Session Completion); the reviewer walks the four sections of the HS-REV companion (`## Acceptance verification`, `## Reviewer notes`, `## Memory candidates`, `## Decision: close or reopen`) and decides pass/reopen. Memory candidates are scored against the four-axis rubric (non-derivability / reusability / surprise-cost / concreteness, 0–3 each, threshold ≥ 7/12) — the prompt body includes the rubric and the conflict-check instruction.
20
+
21
+ If the tool errors with `NO_REVIEW_READY`, tell the user the review queue is empty (no specs at `status: in_review` with an HS-REV companion) and suggest checking `specs/reviews/` for stale companions or filing the missing review doc. If `SPEC_NOT_IN_REVIEW`, surface the spec's actual status so the user can pick the right `/hs-review HS-XXXX` target. If `NO_REVIEW_DOC`, the parent spec is `in_review` but has no `HS-REV-<suffix>-*.md` companion — point the user at CLAUDE.md §Session Completion §3 for the four-section template.
22
+
23
+ Cross-reference: [[HS-XRO1-RVDC-TASK-heyspex-review-on-deck-mcp-and-hs-review]] (spec), [[HS-UKD6-CLR4-RES-close-out-review-screen-jtbd-architecture]] §J17 (downstream Review surface that will call this tool from the "Copy `/hs-review`" affordance).
@@ -24,7 +24,7 @@ Admin/dev-only — resolves paths via `dev-factory/watch-dev-log.mjs`, then watc
24
24
  2. **Watch it.** Start a background `tail -f` over the resolved path(s) and stream it:
25
25
  - Default: filtered to signal only — `grep -E --line-buffered -i "error|warn|exception|fatal|crash| 4[0-9][0-9] | 5[0-9][0-9] "` — so routine 200-OK request logs stay silent and only real problems surface.
26
26
  - `--raw` in `$ARGUMENTS`: skip the filter, stream everything (useful for a first look, like the very first time this was set up).
27
- - Multiple paths (e.g. `companion`'s 4 files) can be tailed together: `tail -n 0 -f path1 path2 ...`.
27
+ - Multiple paths (e.g. `companion`'s 3 files) can be tailed together: `tail -n 0 -f path1 path2 ...`.
28
28
 
29
29
  3. **Report** the resolved path(s) and whether you're running filtered or raw, then stay quiet until something actually worth flagging comes through — don't narrate routine matched lines individually if they're clearly benign.
30
30