@comento/pota-harness 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.agents/workflows/pota-doctor.md +16 -0
  2. package/.agents/workflows/pota-install.md +15 -0
  3. package/.agents/workflows/pota-memory.md +17 -0
  4. package/.agents/workflows/pota-plan.md +22 -0
  5. package/.agents/workflows/pota-review.md +16 -0
  6. package/.agents/workflows/pota-toolcheck.md +9 -0
  7. package/.agents/workflows/pota-uninstall.md +9 -0
  8. package/.agents/workflows/pota-upgrade.md +15 -0
  9. package/.agents/workflows/pota-work.md +24 -0
  10. package/.claude/agents/explorer.md +7 -0
  11. package/.claude/agents/worker.md +7 -0
  12. package/.claude/commands/pota-doctor.md +5 -0
  13. package/.claude/commands/pota-install.md +9 -0
  14. package/.claude/commands/pota-memory.md +5 -0
  15. package/.claude/commands/pota-plan.md +5 -0
  16. package/.claude/commands/pota-review.md +5 -0
  17. package/.claude/commands/pota-toolcheck.md +7 -0
  18. package/.claude/commands/pota-uninstall.md +5 -0
  19. package/.claude/commands/pota-upgrade.md +9 -0
  20. package/.claude/commands/pota-work.md +7 -0
  21. package/.codex-plugin/plugin.json +33 -0
  22. package/LICENSE +21 -0
  23. package/README.de.md +166 -0
  24. package/README.es.md +166 -0
  25. package/README.fr.md +166 -0
  26. package/README.ja.md +166 -0
  27. package/README.ko.md +184 -0
  28. package/README.md +191 -2
  29. package/README.pt.md +166 -0
  30. package/README.zh.md +166 -0
  31. package/agents/explorer.md +36 -0
  32. package/agents/worker.md +45 -0
  33. package/commands/pota-doctor.md +28 -0
  34. package/commands/pota-install.md +29 -0
  35. package/commands/pota-memory.md +17 -0
  36. package/commands/pota-plan.md +22 -0
  37. package/commands/pota-review.md +16 -0
  38. package/commands/pota-toolcheck.md +17 -0
  39. package/commands/pota-uninstall.md +27 -0
  40. package/commands/pota-upgrade.md +26 -0
  41. package/commands/pota-work.md +24 -0
  42. package/docs/model-guidance/README.md +67 -0
  43. package/docs/model-guidance/claude.md +49 -0
  44. package/docs/model-guidance/codex.md +68 -0
  45. package/hooks/hooks.json +65 -0
  46. package/package.json +47 -4
  47. package/scripts/pota +14 -0
  48. package/scripts/pota-auto-upgrade.mjs +342 -0
  49. package/scripts/pota-cleanup.mjs +194 -0
  50. package/scripts/pota-goal-cli.mjs +337 -0
  51. package/scripts/pota-goal-evidence.mjs +186 -0
  52. package/scripts/pota-goal-history.mjs +316 -0
  53. package/scripts/pota-goal-operations-evidence.mjs +213 -0
  54. package/scripts/pota-goal-operations-mutations.mjs +97 -0
  55. package/scripts/pota-goal-operations.mjs +281 -0
  56. package/scripts/pota-goal-policy.mjs +62 -0
  57. package/scripts/pota-goal-state-validation.mjs +361 -0
  58. package/scripts/pota-goal-state.mjs +136 -0
  59. package/scripts/pota-goal.mjs +69 -0
  60. package/scripts/pota-index.mjs +1052 -0
  61. package/scripts/pota-lifecycle-claude.mjs +173 -0
  62. package/scripts/pota-lifecycle-commands.mjs +418 -0
  63. package/scripts/pota-lifecycle-config.mjs +153 -0
  64. package/scripts/pota-lifecycle-hosts.mjs +98 -0
  65. package/scripts/pota-lifecycle-integrity.mjs +272 -0
  66. package/scripts/pota-lifecycle-layout.mjs +52 -0
  67. package/scripts/pota-lifecycle-lock.mjs +205 -0
  68. package/scripts/pota-lifecycle-mirror.mjs +83 -0
  69. package/scripts/pota-lifecycle-staging.mjs +156 -0
  70. package/scripts/pota-lifecycle-transaction-journal.mjs +244 -0
  71. package/scripts/pota-lifecycle-transaction-targets.mjs +119 -0
  72. package/scripts/pota-lifecycle-transaction.mjs +12 -0
  73. package/scripts/pota-lifecycle.mjs +244 -0
  74. package/scripts/pota-paths.mjs +190 -0
  75. package/scripts/pota-safe-fs.mjs +277 -0
  76. package/scripts/pota-state-context.mjs +281 -0
  77. package/scripts/pota-state-hook.mjs +26 -0
  78. package/scripts/pota-state-init.mjs +80 -0
  79. package/scripts/pota-state.mjs +245 -0
  80. package/scripts/pota-toolcheck-detect.mjs +229 -0
  81. package/scripts/pota-toolcheck-runtime.mjs +178 -0
  82. package/scripts/pota-toolcheck.mjs +221 -0
  83. package/scripts/pota-update-state.mjs +219 -0
  84. package/scripts/pota.mjs +200 -0
  85. package/skills/pota-memory/SKILL.md +52 -0
  86. package/skills/pota-plan/SKILL.md +66 -0
  87. package/skills/pota-plan/delegation.md +65 -0
  88. package/skills/pota-plan/heavy-path.md +28 -0
  89. package/skills/pota-plan/planning-rules.md +78 -0
  90. package/skills/pota-review/SKILL.md +56 -0
  91. package/skills/pota-work/SKILL.md +71 -0
  92. package/skills/pota-work/delegation.md +65 -0
  93. package/skills/pota-work/heavy-path.md +18 -0
  94. package/skills/pota-work/recovery.md +29 -0
  95. package/skills/pota-work/verification.md +79 -0
  96. package/templates/MEMORY.md +12 -0
  97. package/templates/PLAN.md +9 -0
  98. package/templates/domain-memory.md +22 -0
  99. package/templates/work-plan.md +177 -0
@@ -0,0 +1,190 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, relative, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ export const RUNTIME_LOCATOR_SCHEMA_VERSION = 1;
8
+
9
+ export function potaHome(env = process.env) {
10
+ const userHome = env.HOME ?? env.USERPROFILE ?? homedir();
11
+ return resolve(env.POTA_HOME ?? join(userHome, ".pota"));
12
+ }
13
+
14
+ // Lifecycle-managed files (ownership manifest, journal, lock, canonical runtime, marketplaces) live under
15
+ // <potaHome>/install so they stay separate from goal state under <potaHome>/projects.
16
+ export function installRoot(env = process.env) {
17
+ return join(potaHome(env), "install");
18
+ }
19
+
20
+ export function installManifestPath(env = process.env) {
21
+ return join(installRoot(env), "lifecycle.json");
22
+ }
23
+
24
+ export function repositoryId(root = process.cwd()) {
25
+ return `project-${hashPath(gitLayout(root).commonDir)}`;
26
+ }
27
+
28
+ export function worktreeId(root = process.cwd()) {
29
+ return `worktree-${hashPath(gitLayout(root).gitDir)}`;
30
+ }
31
+
32
+ export function goalStateDirectory(root = process.cwd(), env = process.env) {
33
+ return join(potaHome(env), "projects", repositoryId(root), "worktrees", worktreeId(root));
34
+ }
35
+
36
+ function gitLayout(root) {
37
+ const workspaceRoot = realpathSync(resolve(root));
38
+ const gitEntry = join(workspaceRoot, ".git");
39
+ try {
40
+ const stat = lstatSync(gitEntry);
41
+ const gitDir = stat.isDirectory() ? realpathSync(gitEntry) : resolveGitFile(workspaceRoot, gitEntry);
42
+ const commonFile = join(gitDir, "commondir");
43
+ const commonDir = existsSync(commonFile)
44
+ ? realpathSync(resolve(gitDir, readFileSync(commonFile, "utf8").trim()))
45
+ : gitDir;
46
+ return { gitDir, commonDir };
47
+ } catch (error) {
48
+ if (error?.code !== "ENOENT") throw error;
49
+ return { gitDir: workspaceRoot, commonDir: workspaceRoot };
50
+ }
51
+ }
52
+
53
+ function resolveGitFile(workspaceRoot, gitEntry) {
54
+ const match = readFileSync(gitEntry, "utf8").match(/^gitdir:\s*(.+)\s*$/m);
55
+ if (!match) throw new Error(`Invalid Git worktree file: ${gitEntry}`);
56
+ return realpathSync(resolve(workspaceRoot, match[1]));
57
+ }
58
+
59
+ function hashPath(value) {
60
+ return createHash("sha256").update(value).digest("hex");
61
+ }
62
+
63
+ export function resolvePluginRoot(moduleUrl = import.meta.url) {
64
+ return resolve(dirname(fileURLToPath(moduleUrl)), "..");
65
+ }
66
+
67
+ export function parseRootArgs(args = [], options = {}) {
68
+ const remaining = [];
69
+ let explicitRoot = null;
70
+
71
+ for (let index = 0; index < args.length; index += 1) {
72
+ const argument = args[index];
73
+ if (argument === "--root") {
74
+ if (explicitRoot !== null) throw new Error("--root may only be provided once");
75
+ const value = args[index + 1];
76
+ if (!value || value.startsWith("--")) throw new Error("--root requires a path");
77
+ explicitRoot = value;
78
+ index += 1;
79
+ continue;
80
+ }
81
+ if (argument.startsWith("--root=")) {
82
+ if (explicitRoot !== null) throw new Error("--root may only be provided once");
83
+ const value = argument.slice("--root=".length);
84
+ if (!value) throw new Error("--root requires a path");
85
+ explicitRoot = value;
86
+ continue;
87
+ }
88
+ remaining.push(argument);
89
+ }
90
+
91
+ return {
92
+ args: remaining,
93
+ explicitRoot,
94
+ repoRoot: resolveRepoRoot({ ...options, explicitRoot }),
95
+ };
96
+ }
97
+
98
+ export function resolveRepoRoot({
99
+ explicitRoot = null,
100
+ hookCwd = null,
101
+ cwd = process.cwd(),
102
+ env = process.env,
103
+ } = {}) {
104
+ if (explicitRoot) return resolve(cwd, explicitRoot);
105
+ if (hookCwd) {
106
+ const hookStart = resolve(hookCwd);
107
+ return discoverRepositoryRoot(hookStart) ?? hookStart;
108
+ }
109
+ if (env.HARNESS_REPO_ROOT) return resolve(env.HARNESS_REPO_ROOT);
110
+ const start = resolve(cwd);
111
+ return discoverRepositoryRoot(start) ?? start;
112
+ }
113
+
114
+ export function discoverRepositoryRoot(start = process.cwd()) {
115
+ let current = resolve(start);
116
+ while (true) {
117
+ if (existsSync(join(current, ".git"))) return current;
118
+ const parent = dirname(current);
119
+ if (parent === current) return null;
120
+ current = parent;
121
+ }
122
+ }
123
+
124
+ export function createRuntimeLocator(pluginRoot) {
125
+ const root = resolve(pluginRoot);
126
+ return {
127
+ schemaVersion: RUNTIME_LOCATOR_SCHEMA_VERSION,
128
+ plugin: "pota",
129
+ pluginRoot: root,
130
+ entrypoints: {
131
+ cli: join(root, "scripts", "pota.mjs"),
132
+ lifecycle: join(root, "scripts", "pota-lifecycle.mjs"),
133
+ state: join(root, "scripts", "pota-state.mjs"),
134
+ toolcheck: join(root, "scripts", "pota-toolcheck.mjs"),
135
+ },
136
+ resources: {
137
+ agents: join(root, "agents"),
138
+ commands: join(root, "commands"),
139
+ modelGuidance: join(root, "docs", "model-guidance"),
140
+ skills: join(root, "skills"),
141
+ templates: join(root, "templates"),
142
+ },
143
+ };
144
+ }
145
+
146
+ const runtimeEntryRelativePaths = [
147
+ join("scripts", "pota.mjs"),
148
+ join("scripts", "pota-lifecycle.mjs"),
149
+ join("scripts", "pota-state.mjs"),
150
+ join("scripts", "pota-toolcheck.mjs"),
151
+ join("scripts", "pota-goal.mjs"),
152
+ join("scripts", "pota-cleanup.mjs"),
153
+ // Spawned by path from the SessionStart hook, so it is an entrypoint of its own.
154
+ join("scripts", "pota-auto-upgrade.mjs"),
155
+ ];
156
+ const localModuleSpecifier = /^(?:import|export)\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|[\w$]+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+))?\s+from\s+)?["'](\.[^"']+)["']/gm;
157
+
158
+ export function runtimeRequiredRelativePaths(pluginRoot) {
159
+ const root = resolve(pluginRoot);
160
+ const pending = runtimeEntryRelativePaths.map((path) => join(root, path));
161
+ const closure = new Set();
162
+
163
+ while (pending.length > 0) {
164
+ const path = pending.pop();
165
+ if (closure.has(path)) continue;
166
+ closure.add(path);
167
+ const source = readFileSync(path, "utf8");
168
+ for (const match of source.matchAll(localModuleSpecifier)) pending.push(resolve(dirname(path), match[1]));
169
+ }
170
+
171
+ return [...closure].map((path) => relative(root, path)).sort();
172
+ }
173
+
174
+ export function runtimeRequiredPaths(locator) {
175
+ if (!locator || locator.schemaVersion !== RUNTIME_LOCATOR_SCHEMA_VERSION || locator.plugin !== "pota"
176
+ || typeof locator.pluginRoot !== "string") return [];
177
+ const root = locator.pluginRoot;
178
+ return [
179
+ join(root, "package.json"),
180
+ join(root, ".codex-plugin", "plugin.json"),
181
+ join(root, "hooks", "hooks.json"),
182
+ ...runtimeRequiredRelativePaths(root).map((path) => join(root, path)),
183
+ locator.resources?.commands,
184
+ locator.resources?.agents,
185
+ locator.resources?.skills,
186
+ join(locator.resources?.templates ?? "", "work-plan.md"),
187
+ join(locator.resources?.modelGuidance ?? "", "README.md"),
188
+ join(locator.resources?.modelGuidance ?? "", "codex.md"),
189
+ ].filter(Boolean);
190
+ }
@@ -0,0 +1,277 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { hostname } from "node:os";
3
+ import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, join, relative, resolve, sep } from "node:path";
5
+
6
+ const LOCK_SCHEMA_VERSION = 1;
7
+ const DEFAULT_LOCK_TIMEOUT_MS = 5_000;
8
+ const DEFAULT_RETRY_MS = 25;
9
+ const DEFAULT_MALFORMED_STALE_MS = 30_000;
10
+ const TRANSIENT_LOCK_ERROR_CODES = new Set(["EACCES", "EBUSY", "EPERM"]);
11
+
12
+ export function stateLockPath(root = process.cwd()) {
13
+ return safePath(root, ".pota/state.lock");
14
+ }
15
+
16
+ export function safePath(root, relativePath) {
17
+ const workspaceRoot = resolve(root);
18
+ const target = resolve(workspaceRoot, relativePath);
19
+ const relation = relative(workspaceRoot, target);
20
+ if (relation === ".." || relation.startsWith(`..${sep}`)) {
21
+ throw new Error(`Managed path must remain inside the workspace: ${relativePath}`);
22
+ }
23
+ return target;
24
+ }
25
+
26
+ export async function assertNoSymlinkComponents(root, target, { allowMissing = true } = {}) {
27
+ const workspaceRoot = resolve(root);
28
+ const absoluteTarget = resolve(target);
29
+ const relation = relative(workspaceRoot, absoluteTarget);
30
+ if (relation === ".." || relation.startsWith(`..${sep}`)) {
31
+ throw new Error(`Managed path must remain inside the workspace: ${target}`);
32
+ }
33
+ const components = relation === "" ? [] : relation.split(sep);
34
+ let current = workspaceRoot;
35
+ await rejectSymlink(current);
36
+ for (const component of components) {
37
+ current = join(current, component);
38
+ try {
39
+ await rejectSymlink(current);
40
+ } catch (error) {
41
+ if (allowMissing && error?.code === "ENOENT") return;
42
+ throw error;
43
+ }
44
+ }
45
+ }
46
+
47
+ export async function safeMkdir(root, relativePath) {
48
+ const workspaceRoot = resolve(root);
49
+ const target = safePath(workspaceRoot, relativePath);
50
+ await assertNoSymlinkComponents(workspaceRoot, workspaceRoot, { allowMissing: false });
51
+ const relation = relative(workspaceRoot, target);
52
+ let current = workspaceRoot;
53
+ for (const component of relation.split(sep).filter(Boolean)) {
54
+ current = join(current, component);
55
+ try {
56
+ const stat = await lstat(current);
57
+ if (stat.isSymbolicLink()) throw symlinkError(current);
58
+ if (!stat.isDirectory()) throw new Error(`Managed directory component is not a directory: ${current}`);
59
+ } catch (error) {
60
+ if (error?.code !== "ENOENT") throw error;
61
+ try {
62
+ await mkdir(current);
63
+ } catch (mkdirError) {
64
+ if (mkdirError?.code !== "EEXIST") throw mkdirError;
65
+ const stat = await lstat(current);
66
+ if (stat.isSymbolicLink()) throw symlinkError(current);
67
+ if (!stat.isDirectory()) throw new Error(`Managed directory component is not a directory: ${current}`);
68
+ }
69
+ }
70
+ }
71
+ return target;
72
+ }
73
+
74
+ export async function safeWriteFile(root, target, data, options) {
75
+ const absoluteTarget = safePath(root, relative(resolve(root), resolve(target)));
76
+ await assertNoSymlinkComponents(root, dirname(absoluteTarget), { allowMissing: false });
77
+ await assertNoSymlinkComponents(root, absoluteTarget);
78
+ return writeFile(absoluteTarget, data, options);
79
+ }
80
+
81
+ export async function withStateLock(root, operation, {
82
+ timeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
83
+ retryMs = DEFAULT_RETRY_MS,
84
+ malformedStaleMs = DEFAULT_MALFORMED_STALE_MS,
85
+ testHooks = {},
86
+ } = {}) {
87
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new Error("lock timeoutMs must be a non-negative number");
88
+ const workspaceRoot = resolve(root);
89
+ await safeMkdir(workspaceRoot, ".pota");
90
+ const lockPath = stateLockPath(workspaceRoot);
91
+ const owner = {
92
+ schemaVersion: LOCK_SCHEMA_VERSION,
93
+ pid: process.pid,
94
+ hostname: hostname(),
95
+ nonce: randomUUID(),
96
+ acquiredAt: new Date().toISOString(),
97
+ };
98
+ const deadline = Date.now() + timeoutMs;
99
+ while (true) {
100
+ try {
101
+ await safeWriteFile(workspaceRoot, lockPath, `${JSON.stringify(owner)}\n`, { encoding: "utf8", flag: "wx" });
102
+ await testHooks.afterLockWritten?.({ lockPath, owner: structuredClone(owner) });
103
+ if (await confirmsUnclaimedLockOwnership(workspaceRoot, lockPath, owner.nonce)) break;
104
+ } catch (error) {
105
+ if (isTransientLockError(error)) {
106
+ // Windows can briefly deny a lock file that another process is closing.
107
+ // Retry within the deadline; permission errors are not evidence of staleness.
108
+ } else if (error?.code === "EEXIST") {
109
+ try {
110
+ await testHooks.beforeContendedLockConfirmation?.({ lockPath });
111
+ if (await confirmsUnclaimedLockOwnership(workspaceRoot, lockPath, owner.nonce)) break;
112
+ const reclaimed = await reclaimStaleLock(workspaceRoot, lockPath, owner, malformedStaleMs, testHooks);
113
+ if (reclaimed) continue;
114
+ } catch (inspectionError) {
115
+ if (!isTransientLockError(inspectionError)) throw inspectionError;
116
+ }
117
+ } else {
118
+ throw error;
119
+ }
120
+ }
121
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for Pota state lock after ${timeoutMs}ms`);
122
+ await delay(Math.min(retryMs, Math.max(1, deadline - Date.now())));
123
+ }
124
+
125
+ try {
126
+ return await operation();
127
+ } finally {
128
+ await releaseOwnedLock(lockPath, owner.nonce);
129
+ }
130
+ }
131
+
132
+ async function reclaimStaleLock(root, lockPath, owner, malformedStaleMs, testHooks) {
133
+ let observed;
134
+ let stat;
135
+ try {
136
+ [observed, stat] = await Promise.all([readFile(lockPath, "utf8"), lstat(lockPath)]);
137
+ } catch (error) {
138
+ if (error?.code === "ENOENT") return true;
139
+ throw error;
140
+ }
141
+ if (stat.isSymbolicLink()) throw symlinkError(lockPath);
142
+ const record = parseLockRecord(observed);
143
+ if (!isReclaimableLock(record, stat, malformedStaleMs)) return false;
144
+
145
+ await testHooks.afterStaleObserved?.({ lockPath, observed, record: record ? structuredClone(record) : null });
146
+ const claimPath = safePath(root, `.pota/state.lock.reclaim.${owner.nonce}`);
147
+ await safeWriteFile(root, claimPath, `${JSON.stringify(owner)}\n`, { encoding: "utf8", flag: "wx" });
148
+ try {
149
+ await testHooks.afterReclaimInterlock?.({ lockPath });
150
+ let claimed;
151
+ let claimedStat;
152
+ try {
153
+ [claimed, claimedStat] = await Promise.all([readFile(lockPath, "utf8"), lstat(lockPath)]);
154
+ } catch (error) {
155
+ if (error?.code === "ENOENT") return true;
156
+ throw error;
157
+ }
158
+ if (claimedStat.isSymbolicLink()) throw symlinkError(lockPath);
159
+ const claimedRecord = parseLockRecord(claimed);
160
+ if (!isReclaimableLock(claimedRecord, claimedStat, malformedStaleMs)) return false;
161
+
162
+ const quarantine = safePath(root, `.pota/state.lock.stale.${claimedRecord?.nonce ?? "malformed"}.${randomUUID()}`);
163
+ try {
164
+ await rename(lockPath, quarantine);
165
+ } catch (error) {
166
+ if (error?.code === "ENOENT") return true;
167
+ throw error;
168
+ }
169
+ const movedStat = await lstat(quarantine);
170
+ await rm(quarantine, { force: true });
171
+ return movedStat.dev === claimedStat.dev && movedStat.ino === claimedStat.ino;
172
+ } finally {
173
+ await releaseOwnedLock(claimPath, owner.nonce);
174
+ }
175
+ }
176
+
177
+ function isReclaimableLock(record, stat, malformedStaleMs) {
178
+ if (record) return lockOwnerIsDead(record);
179
+ return Date.now() - stat.mtimeMs > malformedStaleMs;
180
+ }
181
+
182
+ // A lock owner is dead only when it ran on this host and its pid no longer exists.
183
+ export function lockOwnerIsDead(record) {
184
+ return record.hostname === hostname() && !pidIsAlive(record.pid);
185
+ }
186
+
187
+ async function confirmsUnclaimedLockOwnership(root, lockPath, nonce) {
188
+ if (await activeReclaimClaimExists(root)) return false;
189
+ try {
190
+ return parseLockRecord(await readFile(lockPath, "utf8"))?.nonce === nonce;
191
+ } catch (error) {
192
+ if (error?.code === "ENOENT") return false;
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ async function activeReclaimClaimExists(root) {
198
+ const claimNames = (await readdir(safePath(root, ".pota")))
199
+ .filter((name) => name.startsWith("state.lock.reclaim."));
200
+ for (const name of claimNames) {
201
+ const claimPath = safePath(root, `.pota/${name}`);
202
+ let stat;
203
+ let record;
204
+ try {
205
+ [stat, record] = await Promise.all([lstat(claimPath), readFile(claimPath, "utf8").then(parseLockRecord)]);
206
+ } catch (error) {
207
+ if (error?.code === "ENOENT") continue;
208
+ throw error;
209
+ }
210
+ if (stat.isSymbolicLink()) throw symlinkError(claimPath);
211
+ if (record?.hostname === hostname() && !pidIsAlive(record.pid)) {
212
+ await releaseOwnedLock(claimPath, record.nonce);
213
+ continue;
214
+ }
215
+ return true;
216
+ }
217
+ return false;
218
+ }
219
+
220
+ async function releaseOwnedLock(lockPath, nonce) {
221
+ for (let attempt = 0; attempt < 8; attempt += 1) {
222
+ try {
223
+ const record = parseLockRecord(await readFile(lockPath, "utf8"));
224
+ if (record?.nonce !== nonce) return;
225
+ await rm(lockPath);
226
+ return;
227
+ } catch (error) {
228
+ if (error?.code === "ENOENT") return;
229
+ if (!isTransientLockError(error) || attempt === 7) throw error;
230
+ await delay(10 * (attempt + 1));
231
+ }
232
+ }
233
+ }
234
+
235
+ function isTransientLockError(error) {
236
+ return TRANSIENT_LOCK_ERROR_CODES.has(error?.code);
237
+ }
238
+
239
+ export function parseLockRecord(text) {
240
+ try {
241
+ const value = JSON.parse(text);
242
+ if (value?.schemaVersion !== LOCK_SCHEMA_VERSION) return null;
243
+ if (!Number.isSafeInteger(value.pid) || value.pid <= 0) return null;
244
+ if (typeof value.hostname !== "string" || value.hostname.length === 0) return null;
245
+ if (typeof value.nonce !== "string" || value.nonce.length === 0) return null;
246
+ if (typeof value.acquiredAt !== "string" || Number.isNaN(Date.parse(value.acquiredAt))) return null;
247
+ return value;
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+
253
+ function pidIsAlive(pid) {
254
+ try {
255
+ process.kill(pid, 0);
256
+ return true;
257
+ } catch (error) {
258
+ if (error?.code === "EPERM") return true;
259
+ if (error?.code === "ESRCH") return false;
260
+ return true;
261
+ }
262
+ }
263
+
264
+ async function rejectSymlink(path) {
265
+ const stat = await lstat(path);
266
+ if (stat.isSymbolicLink()) throw symlinkError(path);
267
+ }
268
+
269
+ function symlinkError(path) {
270
+ const error = new Error(`Managed path must not contain a symbolic link: ${path}`);
271
+ error.code = "POTA_SYMLINK";
272
+ return error;
273
+ }
274
+
275
+ function delay(ms) {
276
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
277
+ }