heyspex 0.2.0 → 0.2.1

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 (145) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/bin/hs.mjs +163 -11
  3. package/dist/cli/hs-check.mjs +88 -46
  4. package/dist/cli/hs-claim.mjs +84 -39
  5. package/dist/cli/hs-connect.mjs +583 -129
  6. package/dist/cli/hs-defer.mjs +251 -30
  7. package/dist/cli/hs-dev.mjs +20 -8
  8. package/dist/cli/hs-env.mjs +62 -34
  9. package/dist/cli/hs-index-rebuild.mjs +288 -83
  10. package/dist/cli/hs-index-reconcile.mjs +397 -111
  11. package/dist/cli/hs-init.mjs +2143 -91
  12. package/dist/cli/hs-install.mjs +628 -106
  13. package/dist/cli/hs-join.mjs +686 -236
  14. package/dist/cli/hs-vault-pull.mjs +3189 -179
  15. package/dist/cli/hs-vault-push.mjs +6433 -0
  16. package/dist/cli/hs-vault.mjs +396 -178
  17. package/dist/cli/hs-workspace.mjs +289 -151
  18. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
  19. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
  20. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
  21. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -28
  22. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
  23. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
  24. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
  25. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
  26. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
  27. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
  28. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -26
  29. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
  30. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
  31. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
  32. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
  33. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
  34. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -22
  35. package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
  36. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
  37. package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
  38. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
  39. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
  40. package/dist/cli-templates/harness-commands/cursor/hs-review.md +17 -17
  41. package/dist/cli-templates/harness-commands/cursor/hs-view.md +24 -22
  42. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
  43. package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
  44. package/docs/copilot-plugin-install.md +162 -0
  45. package/docs/driving-heyspex-from-copilot.md +165 -0
  46. package/package.json +20 -2
  47. package/plugins/heyspex/.claude-plugin/plugin.json +12 -12
  48. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -15
  49. package/plugins/heyspex/README.md +309 -0
  50. package/plugins/heyspex/bin/run-hook.mjs +20 -2
  51. package/plugins/heyspex/hooks/cursor-hooks.json +81 -0
  52. package/plugins/heyspex/hooks/hooks.json +94 -37
  53. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -410
  54. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +5 -0
  55. package/scripts/claude-hooks/_run.mjs +316 -23
  56. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  57. package/scripts/claude-hooks/claim-liveness-shared.mjs +2 -0
  58. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  59. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  60. package/scripts/claude-hooks/inject-communication-style.mjs +20 -0
  61. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -247
  62. package/scripts/claude-hooks/session-end.mjs +2 -0
  63. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  64. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  65. package/scripts/claude-hooks/wip-checkpoint.mjs +5 -1
  66. package/scripts/cursor-hooks/manifest.mjs +46 -0
  67. package/scripts/heyspex-mode.mjs +20 -0
  68. package/scripts/hs-mode.mjs +1 -1
  69. package/scripts/hs-view.mjs +180 -17
  70. package/scripts/install-claude-code-hooks.mjs +3 -1
  71. package/scripts/lib/console-url-resolver.mjs +3 -2
  72. package/scripts/lib/customer-kit.mjs +327 -10
  73. package/scripts/lib/dev-mode.mjs +7 -6
  74. package/scripts/lib/directive-obligations.mjs +349 -0
  75. package/scripts/lib/hook-dispositions.mjs +210 -0
  76. package/scripts/lib/hook-vault-header.mjs +30 -0
  77. package/scripts/lib/hs-source-branch.mjs +196 -0
  78. package/scripts/lib/hs-source-log.mjs +2 -1
  79. package/scripts/lib/hs-source-preflight.mjs +18 -4
  80. package/scripts/lib/hs-source-push.mjs +386 -356
  81. package/scripts/lib/window-placement.mjs +109 -0
  82. package/spexharness/src/adapters/claude-code.mjs +11 -3
  83. package/spexharness/src/adapters/copilot.mjs +236 -0
  84. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  85. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  86. package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
  87. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  88. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  89. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  90. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  91. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  92. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  93. package/spexharness/src/intent-classifier.mjs +4 -2
  94. package/spexharness/src/intent-directive.mjs +626 -541
  95. package/spexharness/src/rule-table.mjs +31 -1
  96. package/vendor/heyspex-mcp/account-switch.mjs +6 -5
  97. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
  98. package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
  99. package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
  100. package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
  101. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  102. package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
  103. package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
  104. package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
  105. package/vendor/heyspex-mcp/cli.mjs +7 -1
  106. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  107. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
  108. package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
  109. package/vendor/heyspex-mcp/console-transport.mjs +7 -1
  110. package/vendor/heyspex-mcp/credentials.mjs +9 -8
  111. package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
  112. package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
  113. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
  114. package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
  115. package/vendor/heyspex-mcp/http-transport.mjs +2 -0
  116. package/vendor/heyspex-mcp/list-tools.mjs +1 -1
  117. package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
  118. package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
  119. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  120. package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
  121. package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
  122. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
  123. package/vendor/heyspex-mcp/plan-push.mjs +3 -2
  124. package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
  125. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
  126. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
  127. package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
  128. package/vendor/heyspex-mcp/promote-research.mjs +6 -0
  129. package/vendor/heyspex-mcp/read-path.mjs +162 -6
  130. package/vendor/heyspex-mcp/request-context.mjs +25 -1
  131. package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
  132. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  133. package/vendor/heyspex-mcp/session-owner.mjs +3 -2
  134. package/vendor/heyspex-mcp/session-tools.mjs +10 -0
  135. package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
  136. package/vendor/heyspex-mcp/split-research.mjs +3 -0
  137. package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
  138. package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
  139. package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
  140. package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
  141. package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
  142. package/vendor/heyspex-mcp/vault-io.mjs +720 -724
  143. package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
  144. package/vendor/heyspex-mcp/whoami.mjs +2 -1
  145. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -5,11 +5,26 @@ var __esm = (fn, res) => function __init() {
5
5
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
6
  };
7
7
 
8
+ // scripts/lib/heyspex-home.mjs
9
+ import { homedir } from "node:os";
10
+ function heyspexHome(env = process.env) {
11
+ const override = env?.HEYSPEX_HOME;
12
+ if (typeof override === "string" && override.trim().length > 0) {
13
+ return override.trim();
14
+ }
15
+ return homedir();
16
+ }
17
+ var init_heyspex_home = __esm({
18
+ "scripts/lib/heyspex-home.mjs"() {
19
+ "use strict";
20
+ }
21
+ });
22
+
8
23
  // vendor/heyspex-mcp/credentials.mjs
9
24
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
10
- import os, { homedir } from "node:os";
25
+ import os, { homedir as homedir2 } from "node:os";
11
26
  import { dirname as dirname2, join as join2, resolve } from "node:path";
12
- function credentialsFilePath(homeDir = homedir()) {
27
+ function credentialsFilePath(homeDir = heyspexHome()) {
13
28
  return join2(homeDir, ".heyspex", "credentials.yaml");
14
29
  }
15
30
  function parseMcpToken(raw) {
@@ -122,14 +137,14 @@ function readLegacyConfigAccountId(read, homeDir) {
122
137
  const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
123
138
  return value || null;
124
139
  }
125
- function readBearer({ env = process.env, homeDir = homedir(), readFile: readFile2, accountId: accountId2 } = {}) {
140
+ function readBearer({ env = process.env, homeDir = heyspexHome(), readFile: readFile2, accountId: accountId2 } = {}) {
126
141
  const read = readFile2 ?? ((p) => readFileSync(p, "utf8"));
127
142
  const credentialsText = readTextOrNull(read, credentialsFilePath(homeDir));
128
143
  const named = typeof accountId2 === "string" && accountId2.trim() ? accountId2.trim() : null;
129
144
  const legacyAccountId = named && parseCredentialsStore(credentialsText).schemaVersion === 1 ? readLegacyConfigAccountId(read, homeDir) : null;
130
145
  return resolveBearer({ envToken: env?.[MCP_TOKEN_ENV], credentialsText, accountId: named, legacyAccountId });
131
146
  }
132
- function readCurrentAccountId({ homeDir = homedir(), readFile: readFile2 } = {}) {
147
+ function readCurrentAccountId({ homeDir = heyspexHome(), readFile: readFile2 } = {}) {
133
148
  const read = readFile2 ?? ((p) => readFileSync(p, "utf8"));
134
149
  const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
135
150
  if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
@@ -147,6 +162,7 @@ var MCP_TOKEN_ENV, MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT
147
162
  var init_credentials = __esm({
148
163
  "vendor/heyspex-mcp/credentials.mjs"() {
149
164
  "use strict";
165
+ init_heyspex_home();
150
166
  MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
151
167
  MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
152
168
  CREDENTIALS_SCHEMA_VERSION = 2;
@@ -158,11 +174,11 @@ var init_credentials = __esm({
158
174
  });
159
175
 
160
176
  // scripts/lib/dev-mode.mjs
161
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
162
- import { homedir as homedir6 } from "node:os";
163
- import { join as join7 } from "node:path";
164
- function devModeFilePath(homeDir = homedir6()) {
165
- return join7(homeDir, ".heyspex", DEV_MODE_FILENAME);
177
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
178
+ import { homedir as homedir7 } from "node:os";
179
+ import { join as join8 } from "node:path";
180
+ function devModeFilePath(homeDir = heyspexHome()) {
181
+ return join8(homeDir, ".heyspex", DEV_MODE_FILENAME);
166
182
  }
167
183
  function normalizeMode(raw) {
168
184
  const v = String(raw ?? "").trim().toLowerCase();
@@ -186,7 +202,7 @@ function deriveSemantics(mode, reliance) {
186
202
  vaultWriteMode: local ? "local" : "cloud"
187
203
  };
188
204
  }
189
- function readLocalDevModeRecord({ homeDir = homedir6(), readFile: readFile2 = defaultReadFile } = {}) {
205
+ function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile: readFile2 = defaultReadFile } = {}) {
190
206
  let raw;
191
207
  try {
192
208
  raw = readFile2(devModeFilePath(homeDir));
@@ -216,7 +232,7 @@ function readLocalDevModeRecord({ homeDir = homedir6(), readFile: readFile2 = de
216
232
  }
217
233
  function resolveDevMode({
218
234
  env = process.env,
219
- homeDir = homedir6(),
235
+ homeDir = heyspexHome(),
220
236
  readFile: readFile2 = defaultReadFile,
221
237
  cloudSetting = void 0
222
238
  } = {}) {
@@ -279,11 +295,12 @@ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
279
295
  var init_dev_mode = __esm({
280
296
  "scripts/lib/dev-mode.mjs"() {
281
297
  "use strict";
298
+ init_heyspex_home();
282
299
  init_credentials();
283
300
  CLOUD_CONSOLE_URL = "https://app.heyspex.com";
284
301
  LOCAL_CONSOLE_URL = "http://localhost:3000";
285
302
  DEV_MODE_FILENAME = "dev-mode.json";
286
- defaultReadFile = (p) => readFileSync6(p, "utf8");
303
+ defaultReadFile = (p) => readFileSync7(p, "utf8");
287
304
  }
288
305
  });
289
306
 
@@ -352,7 +369,7 @@ var init_interfaces = __esm({
352
369
  });
353
370
 
354
371
  // spex-console/src/lib/db/schema/postgres.ts
355
- var edgeTypeEnum, validationSeverityEnum, outboxOpEnum, tsvector, specs, tags, edges, wikilinks, specLinks, aliases, validationIssues, indexMeta, frontmatterFields, eventOutbox, captureSessions, handoffContext, claimEventKindEnum, activeClaimStatusEnum, pendingReasonEnum, focusEventKindEnum, specEventKindEnum, focusPin, captureFocusPin, captureFocusSelection, focusSelection, focusSkips, activeClaims, claimWaiters, fileClaims, claimEvents, focusEvents, focusAreas, focusAreasEvents, specEvents, rateLimitBuckets, userVaults, milestoneSnapshots, modelRegistryEntries, launchIntentStatusEnum, launchIntents, companionRelayPresence, companionSettingsStore, consoleSettingsStore, wipLedger, wipLedgerArchive, gatherResults, agentSessions, usageEvents, streamEvents, jobStreams, resolutionStatusEnum, resolutionFocusPin, resolutionLedger, workspaces, vaults, accountSettings, vaultGrants, pendingVaultGrants, githubInstallations, fleetMachines, fleetTasks, fleetResults, userApiKeys, apiKeys, mcpSessions, pairingCodes, companionSessions, inviteRequests, notifications, webhookIngestions, specSettlements;
372
+ var edgeTypeEnum, validationSeverityEnum, outboxOpEnum, tsvector, specs, tags, edges, wikilinks, specLinks, aliases, validationIssues, indexMeta, frontmatterFields, eventOutbox, captureSessions, handoffContext, claimEventKindEnum, activeClaimStatusEnum, pendingReasonEnum, focusEventKindEnum, specEventKindEnum, focusPin, captureFocusPin, captureFocusSelection, focusSelection, focusSkips, activeClaims, claimWaiters, fileClaims, claimEvents, focusEvents, focusAreas, focusAreasEvents, specEvents, rateLimitBuckets, userVaults, milestoneSnapshots, modelRegistryEntries, launchIntentStatusEnum, launchIntents, companionRelayPresence, companionSettingsStore, consoleSettingsStore, wipLedger, wipLedgerArchive, gatherResults, agentSessions, usageEvents, streamEvents, jobStreams, resolutionStatusEnum, resolutionFocusPin, resolutionLedger, workspaces, vaults, accountSettings, vaultGrants, pendingVaultGrants, githubInstallations, fleetMachines, fleetTasks, fleetResults, userApiKeys, apiKeys, mcpSessions, pairingCodes, companionSessions, inviteRequests, notifications, webhookIngestions, specSettlements, sessionBranchDeclarations;
356
373
  var init_postgres = __esm({
357
374
  "spex-console/src/lib/db/schema/postgres.ts"() {
358
375
  "use strict";
@@ -794,7 +811,13 @@ var init_postgres = __esm({
794
811
  "priority_change",
795
812
  "parent_change",
796
813
  "claim_acquired",
797
- "claim_released"
814
+ "claim_released",
815
+ // Collaborator write audit (HS-FWB6, migration 0077).
816
+ "spec_created",
817
+ "spec_edited",
818
+ "spec_deleted",
819
+ "capture_written",
820
+ "wip_recorded"
798
821
  ]);
799
822
  focusPin = pgTable(
800
823
  "focus_pin",
@@ -1987,20 +2010,52 @@ var init_postgres = __esm({
1987
2010
  // `owner/name`
1988
2011
  branch: text("branch").notNull(),
1989
2012
  commitSha: text("commit_sha").notNull(),
1990
- settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`)
2013
+ settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`),
2014
+ // The pull request that carries this settlement
2015
+ // ([[HS-9D25-CWPX-EPIC-branch-lifecycle-open-a-pull-request]]). NULL until
2016
+ // the branch has gone quiet and a PR was opened or found; a write made
2017
+ // after its PR merged stays NULL, which is what triggers the next PR.
2018
+ pullRequestNumber: integer("pull_request_number"),
2019
+ pullRequestUrl: text("pull_request_url")
1991
2020
  },
1992
2021
  (table) => [
1993
2022
  // "latest settlement for this spec" — the read path's only query.
1994
- index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc())
2023
+ index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc()),
2024
+ // The PR sweep's only query: branches with settlements not yet in a PR.
2025
+ index("spec_settlements_unproposed_idx").on(table.vaultId, table.repo, table.branch).where(sql`${table.pullRequestNumber} IS NULL`)
2026
+ ]
2027
+ );
2028
+ sessionBranchDeclarations = pgTable(
2029
+ "session_branch_declarations",
2030
+ {
2031
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
2032
+ vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
2033
+ actorKey: text("actor_key").notNull(),
2034
+ sessionId: text("session_id").notNull(),
2035
+ branch: text("branch").notNull(),
2036
+ declaredAt: timestamp("declared_at", { withTimezone: true }).notNull().default(sql`now()`),
2037
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull()
2038
+ },
2039
+ (table) => [
2040
+ uniqueIndex("session_branch_declarations_uq").on(
2041
+ table.vaultId,
2042
+ table.actorKey,
2043
+ table.sessionId
2044
+ ),
2045
+ index("session_branch_declarations_lookup_idx").on(
2046
+ table.vaultId,
2047
+ table.actorKey,
2048
+ table.declaredAt.desc()
2049
+ )
1995
2050
  ]
1996
2051
  );
1997
2052
  }
1998
2053
  });
1999
2054
 
2000
2055
  // spex-console/bin/hs-workspace.ts
2001
- import { readFileSync as readFileSync9 } from "node:fs";
2056
+ import { readFileSync as readFileSync10 } from "node:fs";
2002
2057
  import path3 from "node:path";
2003
- import process3 from "node:process";
2058
+ import process4 from "node:process";
2004
2059
  import { createInterface as createInterface2 } from "node:readline/promises";
2005
2060
 
2006
2061
  // spex-console/src/lib/cli/control-shared.ts
@@ -2025,17 +2080,19 @@ function shellQuote(p) {
2025
2080
  var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
2026
2081
 
2027
2082
  // vendor/heyspex-mcp/console-fetch.mjs
2083
+ init_heyspex_home();
2028
2084
  init_credentials();
2029
- import { readFileSync as readFileSync4 } from "node:fs";
2030
- import { homedir as homedir4 } from "node:os";
2031
- import { join as join5 } from "node:path";
2085
+ import { readFileSync as readFileSync5 } from "node:fs";
2086
+ import { homedir as homedir5 } from "node:os";
2087
+ import { join as join6 } from "node:path";
2032
2088
 
2033
2089
  // vendor/heyspex-mcp/session-owner.mjs
2090
+ init_heyspex_home();
2034
2091
  init_credentials();
2035
2092
  import { readFileSync as readFileSync2 } from "node:fs";
2036
- import { homedir as homedir2 } from "node:os";
2093
+ import { homedir as homedir3 } from "node:os";
2037
2094
  import { join as join3 } from "node:path";
2038
- function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir2(), readFile: readFile2 } = {}) {
2095
+ function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile: readFile2 } = {}) {
2039
2096
  const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
2040
2097
  if (fromEnv) return fromEnv;
2041
2098
  return readCurrentAccountId({ homeDir, readFile: readFile2 });
@@ -2065,10 +2122,41 @@ function isVaultLessEndpoint(method, path4) {
2065
2122
  });
2066
2123
  }
2067
2124
 
2125
+ // vendor/heyspex-mcp/write-outbox.mjs
2126
+ import {
2127
+ readdirSync,
2128
+ readFileSync as readFileSync3,
2129
+ writeFileSync as writeFileSync2,
2130
+ mkdirSync as mkdirSync2,
2131
+ existsSync as existsSync2,
2132
+ renameSync as renameSync2,
2133
+ rmSync
2134
+ } from "node:fs";
2135
+ import { basename, join as join4, isAbsolute as isAbsolute2 } from "node:path";
2136
+
2137
+ // scripts/lib/shared-path-helpers.mjs
2138
+ import { isAbsolute, resolve as resolve2, sep } from "node:path";
2139
+ function resolveCodeRoot({
2140
+ explicit,
2141
+ packageRoot,
2142
+ vaultRoot,
2143
+ env = process.env
2144
+ } = {}) {
2145
+ const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
2146
+ if (fromExplicit) return fromExplicit;
2147
+ const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
2148
+ if (fromEnv) return fromEnv;
2149
+ const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
2150
+ if (fromPackage) return fromPackage;
2151
+ const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
2152
+ return fromVault;
2153
+ }
2154
+
2068
2155
  // vendor/heyspex-mcp/vault-resolve.mjs
2069
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
2070
- import { homedir as homedir3, tmpdir } from "node:os";
2071
- import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
2156
+ init_heyspex_home();
2157
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
2158
+ import { homedir as homedir4, tmpdir } from "node:os";
2159
+ import { dirname as dirname3, join as join5, resolve as resolve3 } from "node:path";
2072
2160
 
2073
2161
  // vendor/heyspex-mcp/console-fetch.mjs
2074
2162
  var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
@@ -2077,9 +2165,18 @@ var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMAT
2077
2165
  init_credentials();
2078
2166
 
2079
2167
  // spex-console/src/lib/vault-registry/registry-file.ts
2080
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
2081
- import { homedir as homedir5 } from "node:os";
2082
- import { dirname as dirname4, join as join6, resolve as resolve3 } from "node:path";
2168
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
2169
+ import { dirname as dirname4, join as join7, resolve as resolve4 } from "node:path";
2170
+
2171
+ // spex-console/src/lib/heyspex-home.ts
2172
+ import { homedir as homedir6 } from "node:os";
2173
+ function heyspexHome2(env = process.env) {
2174
+ const override = env?.HEYSPEX_HOME;
2175
+ if (typeof override === "string" && override.trim().length > 0) {
2176
+ return override.trim();
2177
+ }
2178
+ return homedir6();
2179
+ }
2083
2180
 
2084
2181
  // spex-console/src/lib/vault-registry/types.ts
2085
2182
  var LEGACY_REGISTRY_ACCOUNT_ID = "legacy";
@@ -2096,10 +2193,10 @@ function emptyVaultRegistry() {
2096
2193
 
2097
2194
  // spex-console/src/lib/vault-registry/registry-file.ts
2098
2195
  function vaultRegistryDir(opts = {}) {
2099
- return opts.registryDir ?? join6(homedir5(), ".heyspex");
2196
+ return opts.registryDir ?? join7(heyspexHome2(), ".heyspex");
2100
2197
  }
2101
2198
  function vaultRegistryFilePath(opts = {}) {
2102
- return join6(vaultRegistryDir(opts), "registry.json");
2199
+ return join7(vaultRegistryDir(opts), "registry.json");
2103
2200
  }
2104
2201
  function isNonEmptyString(v) {
2105
2202
  return typeof v === "string" && v.trim().length > 0;
@@ -2200,9 +2297,9 @@ function parseVaultRegistryFile(json) {
2200
2297
  }
2201
2298
  function readRegistryJson(opts) {
2202
2299
  const filePath = vaultRegistryFilePath(opts);
2203
- if (!existsSync3(filePath)) return { kind: "missing" };
2300
+ if (!existsSync4(filePath)) return { kind: "missing" };
2204
2301
  try {
2205
- const raw = readFileSync5(filePath, "utf8");
2302
+ const raw = readFileSync6(filePath, "utf8");
2206
2303
  const json = JSON.parse(raw);
2207
2304
  if (registryFileSchemaOf(json) === null) return { kind: "unreadable" };
2208
2305
  return { kind: "ok", raw, json };
@@ -2256,14 +2353,14 @@ function underTestRunner() {
2256
2353
  }
2257
2354
  function samePathLoose(a, b) {
2258
2355
  const norm = (p) => {
2259
- const r = resolve3(p).replace(/[\\/]+$/, "");
2356
+ const r = resolve4(p).replace(/[\\/]+$/, "");
2260
2357
  return process.platform === "win32" ? r.toLowerCase() : r;
2261
2358
  };
2262
2359
  return norm(a) === norm(b);
2263
2360
  }
2264
2361
  function assertNotRealHomeRegistryUnderTest(filePath) {
2265
2362
  if (!underTestRunner()) return;
2266
- if (samePathLoose(dirname4(filePath), join6(homedir5(), ".heyspex"))) {
2363
+ if (samePathLoose(dirname4(filePath), join7(heyspexHome2(), ".heyspex"))) {
2267
2364
  throw new Error(
2268
2365
  `registry-file: refusing to write ${filePath} under a test runner \u2014 that is the real ~/.heyspex registry. Pass opts.registryDir (a temp dir).`
2269
2366
  );
@@ -2272,10 +2369,10 @@ function assertNotRealHomeRegistryUnderTest(filePath) {
2272
2369
  function writeVaultRegistry(registry, opts = {}) {
2273
2370
  const filePath = vaultRegistryFilePath(opts);
2274
2371
  assertNotRealHomeRegistryUnderTest(filePath);
2275
- mkdirSync2(dirname4(filePath), { recursive: true });
2372
+ mkdirSync3(dirname4(filePath), { recursive: true });
2276
2373
  const tmpPath = `${filePath}.tmp`;
2277
- writeFileSync2(tmpPath, serializeVaultRegistry(registry), "utf8");
2278
- renameSync2(tmpPath, filePath);
2374
+ writeFileSync3(tmpPath, serializeVaultRegistry(registry), "utf8");
2375
+ renameSync3(tmpPath, filePath);
2279
2376
  }
2280
2377
  function isValidId(id) {
2281
2378
  if (!isNonEmptyString(id)) return false;
@@ -2463,10 +2560,11 @@ function withCloudScope(deps, accountId2, vaultId) {
2463
2560
  }
2464
2561
 
2465
2562
  // scripts/lib/console-url-resolver.mjs
2563
+ init_heyspex_home();
2466
2564
  init_dev_mode();
2467
- import { readFileSync as readFileSync7 } from "node:fs";
2468
- import { homedir as homedir7 } from "node:os";
2469
- import { join as join8 } from "node:path";
2565
+ import { readFileSync as readFileSync8 } from "node:fs";
2566
+ import { homedir as homedir8 } from "node:os";
2567
+ import { join as join9 } from "node:path";
2470
2568
  var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
2471
2569
  function trimTrailingSlash(url) {
2472
2570
  return url.replace(/\/+$/, "");
@@ -2492,9 +2590,9 @@ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
2492
2590
  } catch {
2493
2591
  }
2494
2592
  }
2495
- function readCredentialsConsoleUrl({ homeDir = homedir7(), readFile: readFile2 = defaultReadFile2 } = {}) {
2593
+ function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile: readFile2 = defaultReadFile2 } = {}) {
2496
2594
  try {
2497
- const raw = readFile2(join8(homeDir, ".heyspex", "credentials.yaml"));
2595
+ const raw = readFile2(join9(homeDir, ".heyspex", "credentials.yaml"));
2498
2596
  const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
2499
2597
  const value = match?.[1]?.trim();
2500
2598
  return value && value.length > 0 ? trimTrailingSlash(value) : null;
@@ -2503,12 +2601,12 @@ function readCredentialsConsoleUrl({ homeDir = homedir7(), readFile: readFile2 =
2503
2601
  }
2504
2602
  }
2505
2603
  function defaultReadFile2(p) {
2506
- return readFileSync7(p, "utf8");
2604
+ return readFileSync8(p, "utf8");
2507
2605
  }
2508
2606
  function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery = defaultReadFile2 } = {}) {
2509
2607
  if (!repoRoot) return null;
2510
2608
  try {
2511
- const discovery = String(readDiscovery(join8(repoRoot, ".heyspex", "console-url")) ?? "").trim();
2609
+ const discovery = String(readDiscovery(join9(repoRoot, ".heyspex", "console-url")) ?? "").trim();
2512
2610
  if (discovery.length > 0) return trimTrailingSlash(discovery);
2513
2611
  } catch {
2514
2612
  }
@@ -2536,7 +2634,7 @@ function resolveModeEnvelope({ env, homeDir }) {
2536
2634
  function resolveConsoleUrl({
2537
2635
  repoRoot,
2538
2636
  env = process.env,
2539
- homeDir = homedir7(),
2637
+ homeDir = heyspexHome(),
2540
2638
  strict = false,
2541
2639
  devMode,
2542
2640
  log = (line) => process.stderr.write(line),
@@ -2601,35 +2699,15 @@ var HELP = [
2601
2699
 
2602
2700
  // spex-console/src/lib/cli/install-identity.ts
2603
2701
  import { spawnSync } from "node:child_process";
2604
- import { existsSync as existsSync4 } from "node:fs";
2605
- import { homedir as homedir9 } from "node:os";
2606
- import { join as join9 } from "node:path";
2702
+ import { existsSync as existsSync5 } from "node:fs";
2703
+ import { join as join10 } from "node:path";
2607
2704
  import { createInterface } from "node:readline/promises";
2608
-
2609
- // scripts/lib/shared-path-helpers.mjs
2610
- import { isAbsolute, resolve as resolve4, sep } from "node:path";
2611
- function resolveCodeRoot({
2612
- explicit,
2613
- packageRoot,
2614
- vaultRoot,
2615
- env = process.env
2616
- } = {}) {
2617
- const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
2618
- if (fromExplicit) return fromExplicit;
2619
- const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
2620
- if (fromEnv) return fromEnv;
2621
- const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
2622
- if (fromPackage) return fromPackage;
2623
- const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
2624
- return fromVault;
2625
- }
2626
-
2627
- // spex-console/src/lib/cli/install-identity.ts
2628
2705
  init_credentials();
2629
2706
 
2630
2707
  // vendor/heyspex-mcp/whoami.mjs
2708
+ init_heyspex_home();
2631
2709
  init_credentials();
2632
- import { homedir as homedir8 } from "node:os";
2710
+ import { homedir as homedir9 } from "node:os";
2633
2711
  var SESSION_PATH = "/api/mcp/session";
2634
2712
  async function probeCredentialIdentity({ consoleUrl, token, fetchImpl = fetch } = {}) {
2635
2713
  if (!token) return { kind: "no_credential" };
@@ -2724,6 +2802,10 @@ var HELP2 = [
2724
2802
  "Exit codes: 0 ok \xB7 1 usage \xB7 11 bad-arg \xB7 13 write-failed"
2725
2803
  ].join("\n");
2726
2804
 
2805
+ // spex-console/src/lib/cli/satellite.ts
2806
+ import { resolve as resolveNodePath } from "node:path";
2807
+ import process2 from "node:process";
2808
+
2727
2809
  // spex-console/src/lib/cli/install-identity.ts
2728
2810
  var SWITCH_HINT = "Sign in as another account with `hs login --switch-account` (or create one with `hs login --create-account`).";
2729
2811
  var ATTACH_WORDING = {
@@ -2958,7 +3040,7 @@ async function gateWriteVerb(opts) {
2958
3040
  if (!opts.writeVerbs.has(opts.sub)) return null;
2959
3041
  return runIdentityGate(opts.io, opts.flags, `${opts.namespace} ${opts.sub}`, WRITE_WORDING);
2960
3042
  }
2961
- function readConfiguredAccountId(env = process.env, homeDir = homedir9()) {
3043
+ function readConfiguredAccountId(env = process.env, homeDir = heyspexHome2()) {
2962
3044
  return resolveSessionOwnerUncached({ env, homeDir });
2963
3045
  }
2964
3046
  function isInteractiveTerminal() {
@@ -2966,7 +3048,7 @@ function isInteractiveTerminal() {
2966
3048
  }
2967
3049
  function defaultIdentityGateIo(opts) {
2968
3050
  const consoleUrl = opts.consoleUrl.trim().replace(/\/+$/, "");
2969
- const loginCli = [opts.platformRoot, resolveCodeRoot({})].filter((r) => Boolean(r && r.trim())).map((r) => join9(r, "vendor", "heyspex-mcp", "cli.mjs")).find((p) => existsSync4(p));
3051
+ const loginCli = [opts.platformRoot, resolveCodeRoot({})].filter((r) => Boolean(r && r.trim())).map((r) => join10(r, "vendor", "heyspex-mcp", "cli.mjs")).find((p) => existsSync5(p));
2970
3052
  return {
2971
3053
  consoleUrl,
2972
3054
  interactive: opts.interactive ?? isInteractiveTerminal(),
@@ -3014,8 +3096,7 @@ function defaultIdentityGateIo(opts) {
3014
3096
  init_sql_orm_inert_cli_shim();
3015
3097
 
3016
3098
  // spex-console/src/lib/cli/vault-workspace-shared.ts
3017
- import { readFileSync as readFileSync8 } from "node:fs";
3018
- import os2 from "node:os";
3099
+ import { readFileSync as readFileSync9 } from "node:fs";
3019
3100
 
3020
3101
  // spex-console/src/lib/cli/default-workspace.ts
3021
3102
  function defaultWorkspaceId(accountId2) {
@@ -3027,8 +3108,8 @@ function defaultWorkspaceId(accountId2) {
3027
3108
  function resolveConfiguredAccountId(opts = {}) {
3028
3109
  return resolveSessionOwnerUncached({
3029
3110
  env: opts.env ?? process.env,
3030
- homeDir: opts.homeDir ?? os2.homedir(),
3031
- readFile: opts.readFile ?? ((p) => readFileSync8(p, "utf-8"))
3111
+ homeDir: opts.homeDir ?? heyspexHome2(),
3112
+ readFile: opts.readFile ?? ((p) => readFileSync9(p, "utf-8"))
3032
3113
  });
3033
3114
  }
3034
3115
  var CLOUD_ACCOUNT_ID_REQUIRED_MESSAGE = "refusing -- no signed-in account (no HS_ACCOUNT_ID env var, and no account_id: in ~/.heyspex/config.yaml). Run `hs login` first; nothing was written.";
@@ -3036,7 +3117,11 @@ function resolveCloudAccountIdOrRefuse(opts = {}) {
3036
3117
  const accountId2 = resolveConfiguredAccountId(opts);
3037
3118
  return accountId2 ? { ok: true, accountId: accountId2 } : { ok: false, message: CLOUD_ACCOUNT_ID_REQUIRED_MESSAGE };
3038
3119
  }
3120
+ var VAULT_OWNED_BY_OTHER_ACCOUNT = "vault_owned_by_other_account";
3039
3121
  var WORKSPACE_OWNED_BY_OTHER_ACCOUNT = "workspace_owned_by_other_account";
3122
+ function workspaceNotOwnedByCallerMessage(id) {
3123
+ return `workspace "${id}" belongs to another HeySpex account, so nothing was changed. Only the account that owns a workspace can rename, archive, restore or unregister it (\`hs login --switch-account\` to act as that account).`;
3124
+ }
3040
3125
  function workspaceOwnedByOtherAccountMessage(id) {
3041
3126
  return `workspace "${id}" belongs to another HeySpex account, so nothing was changed. Pick a different workspace id (e.g. \`--workspace <new-id>\`), or sign in as the account that owns it (\`hs login --switch-account\`). Transferring a workspace between accounts is not supported.`;
3042
3127
  }
@@ -3105,8 +3190,14 @@ async function mutateHttp(http, method, path4, body, landed) {
3105
3190
  const res = await cloudHttpRequest(http, method, path4, body);
3106
3191
  if (res.ok) return landed;
3107
3192
  if (res.status === 404) return "not_found";
3193
+ if (res.status === 403 && isOwnedByOtherAccountCode(errorCodeOf(res.json))) {
3194
+ return "owned_by_other_account";
3195
+ }
3108
3196
  return cloudWriteFailure(http, res);
3109
3197
  }
3198
+ function isOwnedByOtherAccountCode(code) {
3199
+ return code === VAULT_OWNED_BY_OTHER_ACCOUNT || code === WORKSPACE_OWNED_BY_OTHER_ACCOUNT;
3200
+ }
3110
3201
  function manageVerbPath(kind, id) {
3111
3202
  return `/api/${kind}/${encodeURIComponent(id)}`;
3112
3203
  }
@@ -3463,18 +3554,31 @@ async function listWorkspacesNeon(accountId2, deps = {}) {
3463
3554
  id: workspaces.id,
3464
3555
  displayName: workspaces.displayName,
3465
3556
  githubOrg: workspaces.githubOrg,
3466
- createdAt: workspaces.createdAt
3557
+ createdAt: workspaces.createdAt,
3558
+ archived: workspaces.archived
3467
3559
  }).from(workspaces).where(eq(workspaces.accountId, accountId2));
3468
3560
  return rows.map((r) => ({
3469
3561
  id: r.id,
3470
3562
  display_name: r.displayName,
3471
3563
  created_at: (r.createdAt instanceof Date ? r.createdAt : /* @__PURE__ */ new Date()).toISOString(),
3564
+ // The flag `setWorkspaceArchivedNeon` writes; `list.ts` filters on it.
3565
+ // On a cloud host this row IS the roster (no registry.json to carry the
3566
+ // flag), so leaving it out made an archived workspace list forever.
3567
+ archived: r.archived,
3472
3568
  ...r.githubOrg ? { github_org: r.githubOrg } : {}
3473
3569
  }));
3474
3570
  } catch {
3475
3571
  return null;
3476
3572
  }
3477
3573
  }
3574
+ async function classifyZeroRowWorkspaceWrite(db, id, accountId2) {
3575
+ try {
3576
+ const rows = await db.select({ accountId: workspaces.accountId }).from(workspaces).where(eq(workspaces.id, id)).limit(1);
3577
+ return rows[0] && rows[0].accountId !== accountId2 ? "owned_by_other_account" : "not_found";
3578
+ } catch {
3579
+ return "not_found";
3580
+ }
3581
+ }
3478
3582
  async function renameWorkspaceNeon(id, displayName, accountId2, deps = {}) {
3479
3583
  if (shouldUseCloudHttp(deps)) {
3480
3584
  return mutateHttp(
@@ -3489,7 +3593,7 @@ async function renameWorkspaceNeon(id, displayName, accountId2, deps = {}) {
3489
3593
  try {
3490
3594
  const db = deps.db ?? getPostgresDb();
3491
3595
  const rows = await db.update(workspaces).set({ displayName }).where(and(eq(workspaces.id, id), eq(workspaces.accountId, accountId2))).returning({ id: workspaces.id });
3492
- return rows.length > 0 ? "updated" : "not_found";
3596
+ return rows.length > 0 ? "updated" : classifyZeroRowWorkspaceWrite(db, id, accountId2);
3493
3597
  } catch {
3494
3598
  return "unreachable";
3495
3599
  }
@@ -3508,7 +3612,7 @@ async function setWorkspaceArchivedNeon(id, archived, accountId2, deps = {}) {
3508
3612
  try {
3509
3613
  const db = deps.db ?? getPostgresDb();
3510
3614
  const rows = await db.update(workspaces).set({ archived }).where(and(eq(workspaces.id, id), eq(workspaces.accountId, accountId2))).returning({ id: workspaces.id });
3511
- return rows.length > 0 ? "updated" : "not_found";
3615
+ return rows.length > 0 ? "updated" : classifyZeroRowWorkspaceWrite(db, id, accountId2);
3512
3616
  } catch {
3513
3617
  return "unreachable";
3514
3618
  }
@@ -3521,7 +3625,7 @@ async function deleteWorkspaceNeon(id, accountId2, deps = {}) {
3521
3625
  try {
3522
3626
  const db = deps.db ?? getPostgresDb();
3523
3627
  const rows = await db.delete(workspaces).where(and(eq(workspaces.id, id), eq(workspaces.accountId, accountId2))).returning({ id: workspaces.id });
3524
- return rows.length > 0 ? "deleted" : "not_found";
3628
+ return rows.length > 0 ? "deleted" : classifyZeroRowWorkspaceWrite(db, id, accountId2);
3525
3629
  } catch {
3526
3630
  return "unreachable";
3527
3631
  }
@@ -3561,17 +3665,57 @@ async function repointActiveWorkspaceOnRemoval(accountId2, removedWorkspaceId, d
3561
3665
  return { repointed: true, newActiveId: candidate.id };
3562
3666
  }
3563
3667
 
3668
+ // spex-console/src/lib/vault-registry/side-effects.ts
3669
+ function isLocalRegistryHost() {
3670
+ const override = process.env.HEYSPEX_VAULT_WRITE_MODE?.trim().toLowerCase();
3671
+ if (override === "cloud") return false;
3672
+ if (override === "local") return true;
3673
+ return process.env.VERCEL !== "1";
3674
+ }
3675
+ function sideEffectErrorMessage(err) {
3676
+ return err instanceof Error ? err.message : String(err);
3677
+ }
3678
+ function runRegistrySideEffect(write) {
3679
+ const step = "registry_json";
3680
+ if (!isLocalRegistryHost()) return { step, ok: true, skipped: true };
3681
+ try {
3682
+ write();
3683
+ return { step, ok: true };
3684
+ } catch (err) {
3685
+ const error = sideEffectErrorMessage(err);
3686
+ console.error(`[vault-registry] post-write side step "${step}" failed: ${error}`);
3687
+ return { step, ok: false, error };
3688
+ }
3689
+ }
3690
+ function registryWritten(effect) {
3691
+ return effect.ok && effect.skipped !== true;
3692
+ }
3693
+
3564
3694
  // spex-console/src/lib/vault-registry/archive.ts
3565
3695
  function cloudWriteFailed(failure) {
3566
3696
  return { ok: false, code: "cloud_write_failed", message: failure.message };
3567
3697
  }
3698
+ function neonUnwritten(kind, id) {
3699
+ return {
3700
+ ok: false,
3701
+ code: "neon_unwritten",
3702
+ message: `${kind} "${id}" was not written to the cloud registry (database unreachable); nothing was changed. Retry once the console can reach its database.`
3703
+ };
3704
+ }
3568
3705
  function workspaceNotFound(id) {
3569
3706
  return { ok: false, code: "workspace_not_found", message: `workspace "${id}" not found` };
3570
3707
  }
3708
+ function workspaceNotOwned(id) {
3709
+ return {
3710
+ ok: false,
3711
+ code: WORKSPACE_OWNED_BY_OTHER_ACCOUNT,
3712
+ message: workspaceNotOwnedByCallerMessage(id)
3713
+ };
3714
+ }
3571
3715
  async function setWorkspaceArchivedState(input, archived, deps = {}) {
3572
- const registry = loadVaultRegistry(deps.registryOpts);
3573
- const existing = registry.workspaces.find((w) => w.id === input.id);
3574
- if (!existing) return workspaceNotFound(input.id);
3716
+ const local = isLocalRegistryHost();
3717
+ const existing = local ? loadVaultRegistry(deps.registryOpts).workspaces.find((w) => w.id === input.id) : void 0;
3718
+ if (local && !existing) return workspaceNotFound(input.id);
3575
3719
  const neonResult = await setWorkspaceArchivedNeon(
3576
3720
  input.id,
3577
3721
  archived,
@@ -3579,21 +3723,23 @@ async function setWorkspaceArchivedState(input, archived, deps = {}) {
3579
3723
  deps.neon ?? {}
3580
3724
  );
3581
3725
  if (neonResult === "not_found") return workspaceNotFound(input.id);
3726
+ if (neonResult === "owned_by_other_account") return workspaceNotOwned(input.id);
3582
3727
  if (isCloudWriteFailure(neonResult)) return cloudWriteFailed(neonResult);
3728
+ if (neonResult === "unreachable" && !existing) return neonUnwritten("workspace", input.id);
3583
3729
  if (archived) {
3584
3730
  await repointActiveWorkspaceOnRemoval(input.accountId, input.id, {
3585
3731
  registryOpts: deps.registryOpts,
3586
3732
  accountPointer: { ...deps.neon, registryOpts: deps.registryOpts }
3587
3733
  });
3588
3734
  }
3589
- registerWorkspace({ ...existing, archived }, deps.registryOpts);
3735
+ if (existing) registerWorkspace({ ...existing, archived }, deps.registryOpts);
3590
3736
  return {
3591
3737
  ok: true,
3592
3738
  workspace: {
3593
3739
  id: input.id,
3594
3740
  archived,
3595
3741
  neon_written: neonResult === "updated",
3596
- registry_written: true
3742
+ registry_written: existing !== void 0
3597
3743
  }
3598
3744
  };
3599
3745
  }
@@ -3632,35 +3778,7 @@ function buildCloseNotDestroyConfirm(kind, verb, displayName) {
3632
3778
  }
3633
3779
 
3634
3780
  // spex-console/src/lib/vault-registry/list.ts
3635
- import { existsSync as existsSync5 } from "node:fs";
3636
-
3637
- // spex-console/src/lib/vault-registry/side-effects.ts
3638
- function isLocalRegistryHost() {
3639
- const override = process.env.HEYSPEX_VAULT_WRITE_MODE?.trim().toLowerCase();
3640
- if (override === "cloud") return false;
3641
- if (override === "local") return true;
3642
- return process.env.VERCEL !== "1";
3643
- }
3644
- function sideEffectErrorMessage(err) {
3645
- return err instanceof Error ? err.message : String(err);
3646
- }
3647
- function runRegistrySideEffect(write) {
3648
- const step = "registry_json";
3649
- if (!isLocalRegistryHost()) return { step, ok: true, skipped: true };
3650
- try {
3651
- write();
3652
- return { step, ok: true };
3653
- } catch (err) {
3654
- const error = sideEffectErrorMessage(err);
3655
- console.error(`[vault-registry] post-write side step "${step}" failed: ${error}`);
3656
- return { step, ok: false, error };
3657
- }
3658
- }
3659
- function registryWritten(effect) {
3660
- return effect.ok && effect.skipped !== true;
3661
- }
3662
-
3663
- // spex-console/src/lib/vault-registry/list.ts
3781
+ import { existsSync as existsSync6 } from "node:fs";
3664
3782
  function pointerDeps2(deps) {
3665
3783
  return deps.accountPointer ?? { registryOpts: deps.registryOpts };
3666
3784
  }
@@ -3902,6 +4020,25 @@ var MAX_DISPLAY_NAME_LEN2 = 60;
3902
4020
  function notFound(id) {
3903
4021
  return { ok: false, code: "workspace_not_found", message: `workspace "${id}" not found` };
3904
4022
  }
4023
+ function notOwned(id) {
4024
+ return {
4025
+ ok: false,
4026
+ code: WORKSPACE_OWNED_BY_OTHER_ACCOUNT,
4027
+ message: workspaceNotOwnedByCallerMessage(id)
4028
+ };
4029
+ }
4030
+ function neonUnwritten2(id) {
4031
+ return {
4032
+ ok: false,
4033
+ code: "neon_unwritten",
4034
+ message: `workspace "${id}" was not written to the cloud registry (database unreachable); nothing was changed. Retry once the console can reach its database.`
4035
+ };
4036
+ }
4037
+ function localRow(id, registryOpts) {
4038
+ const local = isLocalRegistryHost();
4039
+ const existing = local ? loadVaultRegistry(registryOpts).workspaces.find((w) => w.id === id) : void 0;
4040
+ return { local, existing };
4041
+ }
3905
4042
  function cloudWriteFailed2(failure) {
3906
4043
  return { ok: false, code: "cloud_write_failed", message: failure.message };
3907
4044
  }
@@ -3914,9 +4051,8 @@ async function renameWorkspace(input, deps = {}) {
3914
4051
  message: `display name must be 1-${MAX_DISPLAY_NAME_LEN2} characters (trimmed)`
3915
4052
  };
3916
4053
  }
3917
- const registry = loadVaultRegistry(deps.registryOpts);
3918
- const existing = registry.workspaces.find((w) => w.id === input.id);
3919
- if (!existing) return notFound(input.id);
4054
+ const { local, existing } = localRow(input.id, deps.registryOpts);
4055
+ if (local && !existing) return notFound(input.id);
3920
4056
  const neonResult = await renameWorkspaceNeon(
3921
4057
  input.id,
3922
4058
  trimmedName,
@@ -3924,42 +4060,41 @@ async function renameWorkspace(input, deps = {}) {
3924
4060
  deps.neon ?? {}
3925
4061
  );
3926
4062
  if (neonResult === "not_found") return notFound(input.id);
4063
+ if (neonResult === "owned_by_other_account") return notOwned(input.id);
3927
4064
  if (isCloudWriteFailure(neonResult)) return cloudWriteFailed2(neonResult);
3928
- registerWorkspace(
3929
- {
3930
- ...existing,
3931
- display_name: trimmedName
3932
- },
3933
- deps.registryOpts
3934
- );
4065
+ if (neonResult === "unreachable" && !existing) return neonUnwritten2(input.id);
4066
+ if (existing) {
4067
+ registerWorkspace({ ...existing, display_name: trimmedName }, deps.registryOpts);
4068
+ }
3935
4069
  return {
3936
4070
  ok: true,
3937
4071
  workspace: {
3938
4072
  id: input.id,
3939
4073
  display_name: trimmedName,
3940
4074
  neon_written: neonResult === "updated",
3941
- registry_written: true
4075
+ registry_written: existing !== void 0
3942
4076
  }
3943
4077
  };
3944
4078
  }
3945
4079
  async function unregisterWorkspace(input, deps = {}) {
3946
- const registry = loadVaultRegistry(deps.registryOpts);
3947
- const existing = registry.workspaces.find((w) => w.id === input.id);
3948
- if (!existing) return notFound(input.id);
4080
+ const { local, existing } = localRow(input.id, deps.registryOpts);
4081
+ if (local && !existing) return notFound(input.id);
3949
4082
  const neonResult = await deleteWorkspaceNeon(input.id, input.accountId, deps.neon ?? {});
3950
4083
  if (neonResult === "not_found") return notFound(input.id);
4084
+ if (neonResult === "owned_by_other_account") return notOwned(input.id);
3951
4085
  if (isCloudWriteFailure(neonResult)) return cloudWriteFailed2(neonResult);
4086
+ if (neonResult === "unreachable" && !existing) return neonUnwritten2(input.id);
3952
4087
  await repointActiveWorkspaceOnRemoval(input.accountId, input.id, {
3953
4088
  registryOpts: deps.registryOpts,
3954
4089
  accountPointer: { ...deps.neon, registryOpts: deps.registryOpts }
3955
4090
  });
3956
- removeWorkspace(input.id, deps.registryOpts);
4091
+ if (existing) removeWorkspace(input.id, deps.registryOpts);
3957
4092
  return {
3958
4093
  ok: true,
3959
4094
  workspace: {
3960
4095
  id: input.id,
3961
4096
  neon_written: neonResult === "deleted",
3962
- registry_written: true
4097
+ registry_written: existing !== void 0
3963
4098
  }
3964
4099
  };
3965
4100
  }
@@ -4632,7 +4767,10 @@ async function activeWorkspaceSelection(deps, effectiveAccountId = accountId(dep
4632
4767
  }
4633
4768
  function statusForWorkspaceManageError(code) {
4634
4769
  if (code === "workspace_not_found" || code === "vault_not_found") return EXIT.NOT_FOUND;
4635
- if (code === "cloud_write_failed") return EXIT.CLOUD_WRITE_FAILED;
4770
+ if (code === "workspace_owned_by_other_account" || code === "vault_owned_by_other_account") {
4771
+ return EXIT.CONFLICT;
4772
+ }
4773
+ if (code === "cloud_write_failed" || code === "neon_unwritten") return EXIT.CLOUD_WRITE_FAILED;
4636
4774
  return EXIT.INVALID_ARG;
4637
4775
  }
4638
4776
  async function runRename(args, deps) {
@@ -4827,7 +4965,7 @@ var HELP3 = [
4827
4965
  ].join("\n");
4828
4966
 
4829
4967
  // spex-console/bin/run-entry.ts
4830
- import process2 from "node:process";
4968
+ import process3 from "node:process";
4831
4969
  var BinExit = class extends Error {
4832
4970
  code;
4833
4971
  constructor(code) {
@@ -4843,13 +4981,13 @@ function runEntry(name, main2, options = {}) {
4843
4981
  await main2();
4844
4982
  } catch (err) {
4845
4983
  if (err instanceof BinExit) {
4846
- process2.exitCode = err.code;
4984
+ process3.exitCode = err.code;
4847
4985
  return;
4848
4986
  }
4849
4987
  const message2 = err instanceof Error ? err.message : String(err);
4850
- process2.stderr.write(`${name}: ${unhandledLabel}: ${message2}
4988
+ process3.stderr.write(`${name}: ${unhandledLabel}: ${message2}
4851
4989
  `);
4852
- process2.exitCode = fallbackCode;
4990
+ process3.exitCode = fallbackCode;
4853
4991
  }
4854
4992
  })();
4855
4993
  }
@@ -4857,15 +4995,15 @@ function runEntry(name, main2, options = {}) {
4857
4995
  // spex-console/bin/hs-workspace.ts
4858
4996
  function readFile(p) {
4859
4997
  try {
4860
- return readFileSync9(p, "utf8");
4998
+ return readFileSync10(p, "utf8");
4861
4999
  } catch {
4862
5000
  return null;
4863
5001
  }
4864
5002
  }
4865
- var INTERACTIVE = process3.stdin.isTTY === true;
5003
+ var INTERACTIVE = process4.stdin.isTTY === true;
4866
5004
  async function confirm(question) {
4867
5005
  if (!INTERACTIVE) return false;
4868
- const rl = createInterface2({ input: process3.stdin, output: process3.stdout });
5006
+ const rl = createInterface2({ input: process4.stdin, output: process4.stdout });
4869
5007
  try {
4870
5008
  const answer = (await rl.question(question)).trim().toLowerCase();
4871
5009
  return answer === "y" || answer === "yes" || answer === "proceed";
@@ -4874,41 +5012,41 @@ async function confirm(question) {
4874
5012
  }
4875
5013
  }
4876
5014
  async function main() {
4877
- const identityArgv = extractWriteIdentityFlags(process3.argv.slice(3));
5015
+ const identityArgv = extractWriteIdentityFlags(process4.argv.slice(3));
4878
5016
  if (identityArgv.error) {
4879
- process3.stderr.write(`hs workspace: ${identityArgv.error}
5017
+ process4.stderr.write(`hs workspace: ${identityArgv.error}
4880
5018
  `);
4881
- process3.exitCode = EXIT.INVALID_ARG;
5019
+ process4.exitCode = EXIT.INVALID_ARG;
4882
5020
  return;
4883
5021
  }
4884
5022
  const parsed = parseWorkspaceArgs(identityArgv.rest);
4885
5023
  if (parsed.help) {
4886
- process3.stdout.write(`${HELP3}
5024
+ process4.stdout.write(`${HELP3}
4887
5025
  `);
4888
- process3.exitCode = EXIT.OK;
5026
+ process4.exitCode = EXIT.OK;
4889
5027
  return;
4890
5028
  }
4891
5029
  if (parsed.error) {
4892
- process3.stderr.write(`hs workspace: ${parsed.error.message}
5030
+ process4.stderr.write(`hs workspace: ${parsed.error.message}
4893
5031
  `);
4894
- process3.exitCode = parsed.error.code;
5032
+ process4.exitCode = parsed.error.code;
4895
5033
  return;
4896
5034
  }
4897
- const vaultRoot = process3.env.HS_VAULT_ROOT ? path3.resolve(process3.env.HS_VAULT_ROOT) : process3.cwd();
5035
+ const vaultRoot = process4.env.HS_VAULT_ROOT ? path3.resolve(process4.env.HS_VAULT_ROOT) : process4.cwd();
4898
5036
  const consoleUrl = resolveSpexConsoleUrl(vaultRoot, readFile);
4899
5037
  const gate = await gateWriteVerb({
4900
5038
  namespace: "hs workspace",
4901
5039
  sub: parsed.args.sub,
4902
5040
  writeVerbs: WORKSPACE_WRITE_VERBS,
4903
5041
  flags: identityArgv.flags,
4904
- io: defaultIdentityGateIo({ consoleUrl, platformRoot: path3.resolve(process3.cwd(), "..") })
5042
+ io: defaultIdentityGateIo({ consoleUrl, platformRoot: path3.resolve(process4.cwd(), "..") })
4905
5043
  });
4906
5044
  if (gate && !gate.ok) {
4907
- process3.stderr.write(
5045
+ process4.stderr.write(
4908
5046
  `${parsed.args.json ? JSON.stringify(gate.result.payload) : gate.result.message}
4909
5047
  `
4910
5048
  );
4911
- process3.exitCode = gate.result.code;
5049
+ process4.exitCode = gate.result.code;
4912
5050
  return;
4913
5051
  }
4914
5052
  const result = await runWorkspace(parsed.args, {
@@ -4916,9 +5054,9 @@ async function main() {
4916
5054
  confirm,
4917
5055
  neon: { http: { fetchImpl: fetch, consoleUrl, token: readPairedToken() } }
4918
5056
  });
4919
- const sink = result.isError ? process3.stderr : process3.stdout;
5057
+ const sink = result.isError ? process4.stderr : process4.stdout;
4920
5058
  sink.write(`${parsed.args.json ? JSON.stringify(result.payload) : result.message}
4921
5059
  `);
4922
- process3.exitCode = result.code;
5060
+ process4.exitCode = result.code;
4923
5061
  }
4924
5062
  runEntry("hs workspace", main, { fallbackCode: EXIT.WRITE_FAILED });