@primitive.ai/prim 0.1.0-alpha.40 → 0.1.0-alpha.42

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.
package/README.md CHANGED
@@ -34,7 +34,7 @@ One command does the whole install — auth, session hooks, daemon, git hooks,
34
34
  skill, and the welcome:
35
35
 
36
36
  ```bash
37
- prim setup # add --agent codex or --agent hermes, --no-daemon to skip the daemon
37
+ prim setup # add --agent codex or --agent hermes; --no-daemon explicitly opts out
38
38
  ```
39
39
 
40
40
  Or run the steps individually:
@@ -81,10 +81,10 @@ and the manual fallback — live in [`setup.md`](./setup.md).
81
81
  prim setup # Run the whole install in one shot
82
82
  prim setup --agent codex # Same, for OpenAI Codex
83
83
  prim setup --agent hermes # Same, for Hermes Agent (global-only config)
84
- prim setup --no-daemon # Skip the companion daemon
84
+ prim setup --no-daemon # Stop it and persistently opt out of supervised delivery
85
85
  ```
86
86
 
87
- Orchestrates auth → session hooks → daemon → git hooks → skill → welcome,
87
+ Orchestrates auth → session hooks → supervised daemon → capture-health gate → git hooks → skill → welcome,
88
88
  re-running each underlying command so every step behaves exactly as if run by
89
89
  hand (including the browser login). Idempotent — safe to re-run.
90
90
 
@@ -118,11 +118,64 @@ prim codex install # Install OpenAI Codex hooks (project scope)
118
118
  prim hermes install # Install Hermes Agent hooks (global ~/.hermes/config.yaml)
119
119
  ```
120
120
 
121
+ #### Claude decision feedback
122
+
123
+ Claude Code receives eventual, human-visible feedback when automatic capture
124
+ creates an accepted `change` Decision. A later Claude `Stop` or fresh
125
+ `SessionStart` in the same Git worktree can display:
126
+
127
+ ```text
128
+ [prim] response → created Decision (dec_a1b2c3d4): Use the stable API
129
+ ```
130
+
131
+ This is a hook `systemMessage` for the person using Claude Code; it is not
132
+ injected into the model's context. Delivery is at-least-once at the stdout
133
+ handoff boundary: prim acknowledges only after writing the hook response, so a
134
+ failed acknowledgment can show the same notification again. Notifications are
135
+ eligible for 24 hours. The originating session gets limited preference, but any
136
+ concurrent Claude session in the same worktree may consume the backlog. A hook
137
+ claims at most 40 notifications and renders at most 8,000 Unicode code points;
138
+ `hasMore` work is left for a later Stop or SessionStart rather than extending
139
+ the current hook.
140
+
141
+ Worktree scope comes from an opaque UUID stored under the worktree's Git
142
+ metadata (equivalent to `git rev-parse --git-path prim/workspace-id`), never an
143
+ absolute repository path. Linked worktrees receive distinct IDs, moving a
144
+ worktree preserves its ID, and a clone creates a new one. A corrupt or
145
+ unwritable identity is never silently replaced: capture falls back to the
146
+ legacy envelope and `prim doctor` reports the limitation. Disable/uninstall
147
+ does not delete the identity.
148
+
149
+ Feedback uses the invoking CLI's credentials for direct HTTPS calls rather
150
+ than the daemon, avoiding cross-organization token ambiguity. One absolute
151
+ three-second in-process budget covers token refresh, lease, parsing, rendering,
152
+ and acknowledgment. This is not a hard three-second wall clock: it cannot
153
+ preempt shell/PATH or `npx` resolution, Node startup, Claude's own hook
154
+ scheduling, or synchronous filesystem work. The Git fallback has a separate
155
+ short timeout.
156
+
157
+ Alternatives considered:
158
+
159
+ | Option | Benefit | Limitation |
160
+ | --- | --- | --- |
161
+ | Existing hooks + direct HTTPS (current) | No new install surface; uses the invoking credentials | In-process budget is not a host-enforced wall clock |
162
+ | Claude's native hook timeout | Host-enforced termination | Settings migration; may kill a cold `npx` startup |
163
+ | Daemon routing | Lower steady-state latency | Daemon token/org may not match a concurrent session |
164
+ | Dedicated feedback binary | Lower startup overhead | New distribution and install migration |
165
+ | Synchronous pre-Stop classification | Stronger same-turn immediacy | Adds model latency and cost to the hook path |
166
+ | Detached delivery | Does not block Stop | Cannot return the current hook's `systemMessage` |
167
+
168
+ Run `prim claude status` to verify both existing feedback handlers are installed
169
+ and `prim doctor` to inspect the worktree identity and server capability. No new
170
+ hook registration or binary is required when upgrading an existing correct
171
+ installation.
172
+
121
173
  ### Daemon
122
174
 
123
- A long-lived companion process that accelerates the in-session decision checks
124
- and powers the "team: N online" presence count. Optional — hooks fall back to
125
- direct calls if it is down.
175
+ A supervised long-lived companion process that continuously drains captured
176
+ Moves, accelerates in-session decision checks, and powers the "team: N online"
177
+ presence count. `prim setup` requires it to become healthy unless the explicit
178
+ `--no-daemon` opt-out is supplied; hooks still fail soft if it later degrades.
126
179
 
127
180
  ```bash
128
181
  prim daemon start # start (stop / restart / status)
@@ -178,6 +231,11 @@ and offers to install into `.husky/`.
178
231
  prim statusline # Render the team-presence statusline (reads the daemon)
179
232
  ```
180
233
 
234
+ Claude Code has one custom status-line slot. Installation uses a staged,
235
+ lightweight Primitive renderer when that slot is empty or already Primitive's;
236
+ an existing custom status line is preserved and reported explicitly. Use
237
+ `prim daemon status` or `prim doctor` for the same health signal in that case.
238
+
181
239
  ### Welcome
182
240
 
183
241
  ```bash
package/SKILL.md CHANGED
@@ -46,7 +46,7 @@ When enabled, the gate fail-opens on its *own* infrastructure errors (no daemon,
46
46
  ## Read the graph before large or load-bearing edits
47
47
 
48
48
  - `npx --yes @primitive.ai/prim decisions check --files "src/a.ts,src/b.ts"` -- which active decisions reference the files you're about to touch (comma-separated paths, one `--files` value). Run it before a big change.
49
- - `npx --yes @primitive.ai/prim decisions recent` -- the team's recent decisions, each row badged by author and agent (`Your Claude Code` / `Your Codex` / `Your Hermes`); `--limit <n>` and `--since <dur>` narrow it. `--author "<name>"` filters to one teammate (feed name, `"First Last"`, last name, username, email, or email local-part) -- the way to answer "what has X decided?"; an unknown or ambiguous name comes back as `unavailable` with the reason, and `authorHasDecisions` in the JSON distinguishes "no feed-visible decisions" (false) from "has decisions, none in this window" (true).
49
+ - `npx --yes @primitive.ai/prim decisions recent` -- the team's recent decisions, each row badged by author and agent (`Your Claude Code` / `Your Codex` / `Your Hermes`); `--limit <n>` and `--since <dur>` narrow it. `--author "<name>"` filters to one teammate (feed name, `"First Last"`, last name, username, email, or email local-part) -- the way to answer "what has X decided?"; an unknown or ambiguous name comes back as `unavailable` with the reason, and `authorHasDecisions` in the JSON distinguishes "no feed-visible decisions" (false) from "has decisions, none in this window" (true). The page defaults to the 10 most recent, so it can hide older ones: on an author query the JSON's `windowTotal` is how many that teammate has in the window. When `windowTotal` exceeds the rows returned, don't present the page as complete -- tell the user you're showing the most recent N of `windowTotal` and offer to pull the rest, which is a re-run with `--limit <windowTotal>` (capped at 100; `windowTotalCapped` means the count is a floor rendered `N+`, and a window past 100 can't be fetched whole).
50
50
  - `npx --yes @primitive.ai/prim decisions show <idOrShortId>` and `npx --yes @primitive.ai/prim decisions cascade <idOrShortId>` -- full detail, and the downstream blast radius a change would disturb.
51
51
 
52
52
  ## Reconcile and the verdict footer
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getClient,
3
3
  getSiteUrl
4
- } from "./chunk-26VA3ADF.js";
4
+ } from "./chunk-W6PAKEDO.js";
5
5
  import {
6
6
  daemonRequest
7
7
  } from "./chunk-UTKQTZHL.js";
@@ -3,11 +3,21 @@ import { randomUUID } from "crypto";
3
3
  import { platform } from "os";
4
4
 
5
5
  // src/protocol/move.ts
6
- var ENVELOPE_VERSION = 1;
6
+ var LEGACY_ENVELOPE_VERSION = 1;
7
+ var AGENT_ENVELOPE_VERSION = 2;
8
+ var ENVELOPE_VERSION = LEGACY_ENVELOPE_VERSION;
9
+ function withAgentProvenance(move, producer, workspaceId) {
10
+ return {
11
+ ...move,
12
+ env: { ...move.env, workspaceId },
13
+ envelopeVersion: AGENT_ENVELOPE_VERSION,
14
+ producer
15
+ };
16
+ }
7
17
 
8
18
  // src/hooks/prim-hook-core.ts
9
- function toMove(parsed, cliVersion, agent = "claude_code") {
10
- return {
19
+ function toMove(parsed, cliVersion, agent = "claude_code", workspaceId) {
20
+ const move = {
11
21
  moveId: randomUUID(),
12
22
  capturedAt: Date.now(),
13
23
  sessionId: parsed.session_id ?? "",
@@ -24,6 +34,7 @@ function toMove(parsed, cliVersion, agent = "claude_code") {
24
34
  // keeping the Claude wire shape byte-identical.
25
35
  ...agent === "claude_code" ? {} : { producer: agent }
26
36
  };
37
+ return workspaceId ? withAgentProvenance(move, agent, workspaceId) : move;
27
38
  }
28
39
  function toCommitMove(commit, cliVersion, cwd) {
29
40
  return {
@@ -1,11 +1,83 @@
1
+ // src/lib/activation.ts
2
+ import { execFileSync as execFileSync2 } from "child_process";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { join } from "path";
6
+
7
+ // src/lib/git.ts
8
+ import { execFileSync } from "child_process";
9
+ function gitToplevel(cwd) {
10
+ try {
11
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
12
+ cwd,
13
+ encoding: "utf-8",
14
+ stdio: ["ignore", "pipe", "ignore"]
15
+ }).trim();
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ // src/lib/activation.ts
22
+ var PRIM_ACTIVE_KEY = "prim.active";
23
+ var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
24
+ var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
25
+ function repoActiveFlag(cwd) {
26
+ try {
27
+ const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
28
+ cwd,
29
+ encoding: "utf-8",
30
+ stdio: ["ignore", "pipe", "ignore"]
31
+ }).trim();
32
+ if (value === "true") return "true";
33
+ if (value === "false") return "false";
34
+ return void 0;
35
+ } catch {
36
+ return void 0;
37
+ }
38
+ }
39
+ function setRepoActive(cwd, active) {
40
+ execFileSync2("git", ["config", "--local", PRIM_ACTIVE_KEY, active ? "true" : "false"], {
41
+ cwd,
42
+ stdio: ["ignore", "ignore", "pipe"]
43
+ });
44
+ }
45
+ function activateRepoBestEffort(cwd) {
46
+ try {
47
+ setRepoActive(cwd, true);
48
+ } catch {
49
+ }
50
+ }
51
+ function hasProjectPrimInstall(cwd) {
52
+ const root = gitToplevel(cwd);
53
+ if (root === null || root === homedir()) return false;
54
+ for (const rel of PROJECT_INSTALL_FILES) {
55
+ const path = join(root, rel);
56
+ if (existsSync(path)) {
57
+ try {
58
+ const content = readFileSync(path, "utf-8");
59
+ if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
60
+ } catch {
61
+ }
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+ function isRepoActiveForCapture(cwd) {
67
+ const flag = repoActiveFlag(cwd);
68
+ if (flag === "true") return true;
69
+ if (flag === "false") return false;
70
+ return hasProjectPrimInstall(cwd);
71
+ }
72
+
1
73
  // src/lib/bin-cache.ts
2
74
  import { chmodSync, mkdirSync, renameSync, writeFileSync } from "fs";
3
- import { homedir } from "os";
4
- import { join as join2 } from "path";
75
+ import { homedir as homedir2 } from "os";
76
+ import { join as join3 } from "path";
5
77
 
6
78
  // src/lib/bin-path.ts
7
- import { existsSync, readFileSync } from "fs";
8
- import { dirname, isAbsolute, join } from "path";
79
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
80
+ import { dirname, isAbsolute, join as join2 } from "path";
9
81
  import { fileURLToPath } from "url";
10
82
  var PKG_NAME = "@primitive.ai/prim";
11
83
  var ROOT_WALK_LIMIT = 6;
@@ -19,10 +91,10 @@ function locateRoot() {
19
91
  }
20
92
  let dir = dirname(fileURLToPath(import.meta.url));
21
93
  for (let depth = 0; depth < ROOT_WALK_LIMIT; depth++) {
22
- const manifestPath = join(dir, "package.json");
23
- if (existsSync(manifestPath)) {
94
+ const manifestPath = join2(dir, "package.json");
95
+ if (existsSync2(manifestPath)) {
24
96
  try {
25
- const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
97
+ const manifest = JSON.parse(readFileSync2(manifestPath, "utf-8"));
26
98
  if (manifest.name === PKG_NAME && manifest.bin) {
27
99
  resolvedRoot = { dir, bin: manifest.bin };
28
100
  return resolvedRoot;
@@ -45,7 +117,7 @@ function binFile(bin) {
45
117
  if (!root || !rel) {
46
118
  return null;
47
119
  }
48
- return isAbsolute(rel) ? rel : join(root.dir, rel);
120
+ return isAbsolute(rel) ? rel : join2(root.dir, rel);
49
121
  }
50
122
  function hookShimCommand(bin, args = "", opts = {}) {
51
123
  const invoke = (cmd) => args ? `${cmd} ${args}` : cmd;
@@ -74,8 +146,8 @@ function commandMatchesBin(command, bin) {
74
146
  // src/lib/bin-cache.ts
75
147
  var CACHED_BINS = ["prim", "prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
76
148
  function binCacheDir() {
77
- const base = process.env.XDG_CACHE_HOME || join2(homedir(), ".cache");
78
- return join2(base, "prim", "bin");
149
+ const base = process.env.XDG_CACHE_HOME || join3(homedir2(), ".cache");
150
+ return join3(base, "prim", "bin");
79
151
  }
80
152
  function writeAtomic(path, content) {
81
153
  const tmp = `${path}.${process.pid}.tmp`;
@@ -93,11 +165,11 @@ function warmBinCache() {
93
165
  const dir = binCacheDir();
94
166
  mkdirSync(dir, { recursive: true, mode: 448 });
95
167
  chmodSync(dir, 448);
96
- writeAtomic(join2(dir, "node"), process.execPath);
168
+ writeAtomic(join3(dir, "node"), process.execPath);
97
169
  for (const bin of CACHED_BINS) {
98
170
  const file = binFile(bin);
99
171
  if (file) {
100
- writeAtomic(join2(dir, bin), file);
172
+ writeAtomic(join3(dir, bin), file);
101
173
  }
102
174
  }
103
175
  } catch {
@@ -105,6 +177,10 @@ function warmBinCache() {
105
177
  }
106
178
 
107
179
  export {
180
+ gitToplevel,
181
+ setRepoActive,
182
+ activateRepoBestEffort,
183
+ isRepoActiveForCapture,
108
184
  binFile,
109
185
  hookShimCommand,
110
186
  detachedHookShimCommand,
@@ -0,0 +1,210 @@
1
+ // src/lib/workspace-id.ts
2
+ import { execFileSync } from "child_process";
3
+ import { randomUUID } from "crypto";
4
+ import {
5
+ constants,
6
+ chmodSync,
7
+ closeSync,
8
+ fchmodSync,
9
+ fsyncSync,
10
+ linkSync,
11
+ lstatSync,
12
+ mkdirSync,
13
+ openSync,
14
+ readFileSync,
15
+ statSync,
16
+ unlinkSync,
17
+ writeFileSync
18
+ } from "fs";
19
+ import { dirname, isAbsolute, join, resolve } from "path";
20
+ var WORKSPACE_ID_RELATIVE_PATH = join("prim", "workspace-id");
21
+ var MAX_ANCESTORS = 64;
22
+ var MAX_GIT_FILE_BYTES = 4096;
23
+ var MAX_ID_FILE_BYTES = 128;
24
+ var GIT_FALLBACK_TIMEOUT_MS = 250;
25
+ var RACE_READ_ATTEMPTS = 5;
26
+ var RACE_READ_DELAY_MS = 5;
27
+ var CANONICAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
28
+ var raceDelay = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
29
+ function isCanonicalWorkspaceId(value) {
30
+ return CANONICAL_UUID.test(value);
31
+ }
32
+ function inspectWorkspaceId(cwd = process.cwd()) {
33
+ const resolved = resolveWorkspaceIdPath(cwd);
34
+ if (resolved.status !== "resolved") return resolved;
35
+ return inspectPath(resolved.path);
36
+ }
37
+ function getOrCreateWorkspaceId(cwd = process.cwd()) {
38
+ const inspection = inspectWorkspaceId(cwd);
39
+ if (inspection.status !== "missing") return inspection;
40
+ return createWorkspaceId(inspection.path);
41
+ }
42
+ function resolveWorkspaceIdPath(cwd) {
43
+ if (!process.env.GIT_DIR && !process.env.GIT_WORK_TREE) {
44
+ const fromFilesystem = resolveFromFilesystem(cwd);
45
+ if (fromFilesystem) return { status: "resolved", path: fromFilesystem };
46
+ }
47
+ try {
48
+ const output = execFileSync(
49
+ "git",
50
+ ["rev-parse", "--is-inside-work-tree", "--git-path", WORKSPACE_ID_RELATIVE_PATH],
51
+ {
52
+ cwd,
53
+ encoding: "utf8",
54
+ maxBuffer: 4096,
55
+ stdio: ["ignore", "pipe", "ignore"],
56
+ timeout: GIT_FALLBACK_TIMEOUT_MS,
57
+ windowsHide: true
58
+ }
59
+ );
60
+ const [insideWorktree, gitPath, ...extra] = output.trim().split(/\r?\n/);
61
+ if (insideWorktree !== "true" || !gitPath || extra.length > 0 || gitPath.includes("\0")) {
62
+ return { status: "not_git" };
63
+ }
64
+ return {
65
+ status: "resolved",
66
+ path: isAbsolute(gitPath) ? resolve(gitPath) : resolve(cwd, gitPath)
67
+ };
68
+ } catch (error) {
69
+ const code = errorCode(error);
70
+ if (code === "ETIMEDOUT" || code === "EACCES" || code === "EPERM") {
71
+ return { status: "unavailable", operation: "resolve", errorCode: code };
72
+ }
73
+ return { status: "not_git" };
74
+ }
75
+ }
76
+ function resolveFromFilesystem(cwd) {
77
+ let current = resolve(cwd);
78
+ for (let depth = 0; depth < MAX_ANCESTORS; depth += 1) {
79
+ const dotGit = join(current, ".git");
80
+ try {
81
+ const stat = lstatSync(dotGit);
82
+ if (stat.isDirectory() && isGitDirectory(dotGit)) {
83
+ return join(dotGit, WORKSPACE_ID_RELATIVE_PATH);
84
+ }
85
+ if (stat.isFile() && stat.size <= MAX_GIT_FILE_BYTES) {
86
+ const contents = readFileSync(dotGit, "utf8");
87
+ const match = /^gitdir: (.+?)(?:\r?\n)?$/.exec(contents);
88
+ if (match?.[1]) {
89
+ const gitDirectory = isAbsolute(match[1]) ? resolve(match[1]) : resolve(current, match[1]);
90
+ if (isGitDirectory(gitDirectory)) {
91
+ return join(gitDirectory, WORKSPACE_ID_RELATIVE_PATH);
92
+ }
93
+ }
94
+ }
95
+ return null;
96
+ } catch (error) {
97
+ const code = errorCode(error);
98
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
99
+ return null;
100
+ }
101
+ }
102
+ const parent = dirname(current);
103
+ if (parent === current) break;
104
+ current = parent;
105
+ }
106
+ return null;
107
+ }
108
+ function isGitDirectory(path) {
109
+ try {
110
+ return statSync(join(path, "HEAD")).isFile();
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+ function inspectPath(path) {
116
+ let stat;
117
+ try {
118
+ stat = lstatSync(path);
119
+ } catch (error) {
120
+ const code = errorCode(error);
121
+ if (code === "ENOENT" || code === "ENOTDIR") return { status: "missing", path };
122
+ return { status: "unavailable", path, operation: "read", errorCode: code };
123
+ }
124
+ if (!stat.isFile()) return { status: "corrupt", path, reason: "not_file" };
125
+ if (stat.size > MAX_ID_FILE_BYTES) {
126
+ return { status: "corrupt", path, reason: "invalid_content" };
127
+ }
128
+ try {
129
+ const contents = readFileSync(path, "utf8");
130
+ const workspaceId = contents.endsWith("\n") ? contents.slice(0, -1) : contents;
131
+ if ((contents === workspaceId || contents === `${workspaceId}
132
+ `) && isCanonicalWorkspaceId(workspaceId)) {
133
+ return { status: "ready", workspaceId, path, created: false };
134
+ }
135
+ return { status: "corrupt", path, reason: "invalid_content" };
136
+ } catch (error) {
137
+ return {
138
+ status: "unavailable",
139
+ path,
140
+ operation: "read",
141
+ errorCode: errorCode(error)
142
+ };
143
+ }
144
+ }
145
+ function createWorkspaceId(path) {
146
+ const parent = dirname(path);
147
+ const workspaceId = randomUUID();
148
+ const temporaryPath = join(parent, `.workspace-id-${process.pid}-${randomUUID()}.tmp`);
149
+ let temporaryCreated = false;
150
+ let publishing = false;
151
+ try {
152
+ mkdirSync(parent, { recursive: true, mode: 448 });
153
+ chmodSync(parent, 448);
154
+ const fd = openSync(
155
+ temporaryPath,
156
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY,
157
+ 384
158
+ );
159
+ temporaryCreated = true;
160
+ try {
161
+ writeFileSync(fd, `${workspaceId}
162
+ `, "utf8");
163
+ fchmodSync(fd, 384);
164
+ fsyncSync(fd);
165
+ } finally {
166
+ closeSync(fd);
167
+ }
168
+ publishing = true;
169
+ linkSync(temporaryPath, path);
170
+ return { status: "ready", workspaceId, path, created: true };
171
+ } catch (error) {
172
+ if (publishing && errorCode(error) === "EEXIST") return inspectAfterRace(path);
173
+ return {
174
+ status: "unavailable",
175
+ path,
176
+ operation: "create",
177
+ errorCode: errorCode(error)
178
+ };
179
+ } finally {
180
+ if (temporaryCreated) {
181
+ try {
182
+ unlinkSync(temporaryPath);
183
+ } catch {
184
+ }
185
+ }
186
+ }
187
+ }
188
+ function inspectAfterRace(path) {
189
+ let result = { status: "missing", path };
190
+ for (let attempt = 0; attempt < RACE_READ_ATTEMPTS; attempt += 1) {
191
+ result = inspectPath(path);
192
+ if (result.status === "ready") return result;
193
+ if (attempt + 1 < RACE_READ_ATTEMPTS) {
194
+ Atomics.wait(raceDelay, 0, 0, RACE_READ_DELAY_MS);
195
+ }
196
+ }
197
+ if (result.status === "missing") {
198
+ return { status: "unavailable", path, operation: "create", errorCode: "EEXIST" };
199
+ }
200
+ return result;
201
+ }
202
+ function errorCode(error) {
203
+ if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
204
+ return typeof error.code === "string" ? error.code : void 0;
205
+ }
206
+
207
+ export {
208
+ inspectWorkspaceId,
209
+ getOrCreateWorkspaceId
210
+ };