@cassiomc1/forgeloop 1.14.0 → 2.0.0-rc.2

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 (248) hide show
  1. package/CONTRIBUTING.md +11 -7
  2. package/DOCS_INDEX.md +2 -1
  3. package/ENG/c-development-eng.md +2 -2
  4. package/ENG/cpp-development-eng.md +2 -2
  5. package/ENG/dotnet-aspnetcore-development-eng.md +7 -1
  6. package/ENG/flutter-development-eng.md +4 -2
  7. package/ENG/go-development-eng.md +2 -2
  8. package/ENG/java-development-eng.md +3 -3
  9. package/ENG/nodejs-backend-development-eng.md +8 -4
  10. package/ENG/php-development-eng.md +3 -3
  11. package/ENG/python-development-eng.md +205 -0
  12. package/ENG/rust-development-eng.md +42 -1
  13. package/ENG/sql-development-eng.md +4 -4
  14. package/ENG/swift-development-eng.md +5 -4
  15. package/ENG/test-code-eng.md +1 -1
  16. package/ENG/typescript-development-eng.md +4 -4
  17. package/GUIDE_ROUTER.md +6 -0
  18. package/LICENSE +133 -21
  19. package/LOOP_SYSTEM_DESIGN.md +10 -10
  20. package/README.md +28 -39
  21. package/THIRD_PARTY_NOTICES.md +3 -4
  22. package/completions/_forgeloop +11 -3
  23. package/completions/forgeloop.bash +19 -3
  24. package/completions/forgeloop.fish +61 -1
  25. package/docs/AGENT_PROTOCOL_SUMMARY.md +50 -40
  26. package/docs/ARTIFACT_REFERENCE.md +50 -37
  27. package/docs/CLI_REFERENCE.md +147 -3
  28. package/docs/DOCUMENTATION_GUIDE.md +6 -4
  29. package/docs/GETTING_STARTED.md +3 -3
  30. package/docs/PACKAGE_CONTENTS.md +14 -13
  31. package/docs/RELEASE_CHECKLIST.md +81 -10
  32. package/docs/SELF_HOSTED_TESTS.md +121 -0
  33. package/docs/documentation-manifest.json +25 -12
  34. package/package.json +15 -10
  35. package/schemas/task-bundle.schema.json +7 -2
  36. package/scripts/generate-agent-protocol-summary.mjs +2 -2
  37. package/scripts/run-mcp-clean-tests.mjs +30 -0
  38. package/src/adapters/agent-browser/provider.js +14 -11
  39. package/src/adapters/emulated-services/provider.js +11 -4
  40. package/src/adapters/temporary-paths.js +18 -0
  41. package/src/cli.js +146 -210
  42. package/src/commands/action-show.js +5 -2
  43. package/src/commands/advance.js +5 -1
  44. package/src/commands/baseline.js +13 -11
  45. package/src/commands/checkpoint-revalidate.js +24 -8
  46. package/src/commands/complete.js +19 -3
  47. package/src/commands/contract-create.js +3 -1
  48. package/src/commands/doctor.js +36 -13
  49. package/src/commands/gate-record.js +20 -31
  50. package/src/commands/gate-revalidate.js +11 -13
  51. package/src/commands/handoff-list.js +6 -1
  52. package/src/commands/handoff-show.js +6 -1
  53. package/src/commands/migrate-protocol.js +2 -2
  54. package/src/commands/model-route.js +9 -1
  55. package/src/commands/next.js +7 -1
  56. package/src/commands/policy-diff.js +25 -4
  57. package/src/commands/progress.js +17 -11
  58. package/src/commands/record-diagnosis.js +3 -0
  59. package/src/commands/semantic-plan.js +26 -2
  60. package/src/commands/status.js +6 -1
  61. package/src/commands/storage-backup.js +15 -0
  62. package/src/commands/storage-migrate.js +5 -0
  63. package/src/commands/storage-migration-resume.js +5 -0
  64. package/src/commands/storage-migration-status.js +44 -0
  65. package/src/commands/storage-restore-resume.js +7 -0
  66. package/src/commands/storage-restore.js +8 -0
  67. package/src/commands/task-abandon.js +1 -1
  68. package/src/commands/task-list.js +8 -43
  69. package/src/commands/task-migrate.js +3 -3
  70. package/src/commands/task-recover.js +10 -2
  71. package/src/commands/task-scope.js +18 -2
  72. package/src/commands/task-show.js +16 -2
  73. package/src/commands/validate-protocol.js +29 -14
  74. package/src/commands/validate-receipt.js +8 -1
  75. package/src/config/guides.json +4 -0
  76. package/src/core/action-ledger-projection.js +10 -3
  77. package/src/core/actions.js +38 -75
  78. package/src/core/activation.js +11 -3
  79. package/src/core/approvals.js +17 -51
  80. package/src/core/artifact-registry.js +59 -2
  81. package/src/core/artifacts.js +81 -27
  82. package/src/core/attachment-paths.js +45 -0
  83. package/src/core/attestation-coverage.js +6 -1
  84. package/src/core/attestation-verifier.js +21 -2
  85. package/src/core/attestation.js +17 -6
  86. package/src/core/audit.js +6 -1
  87. package/src/core/bundles.js +160 -66
  88. package/src/core/checkpoint-revalidation.js +12 -10
  89. package/src/core/cli-command-definitions.js +109 -7
  90. package/src/core/code-manifest.js +9 -3
  91. package/src/core/command-executors.js +182 -126
  92. package/src/core/command-input.js +21 -1
  93. package/src/core/completion-artifacts.js +71 -29
  94. package/src/core/completion-ownership.js +19 -8
  95. package/src/core/completion-recovery-rebind.js +11 -1
  96. package/src/core/completion.js +60 -9
  97. package/src/core/conformance.js +2 -1
  98. package/src/core/continuity-lint.js +13 -2
  99. package/src/core/continuity-reconciliation.js +16 -3
  100. package/src/core/continuity.js +16 -8
  101. package/src/core/contract-bootstrap-recovery.js +42 -41
  102. package/src/core/contract-revision.js +8 -9
  103. package/src/core/decision/artifact.js +2 -1
  104. package/src/core/decision/events.js +9 -6
  105. package/src/core/decision/resolver.js +19 -11
  106. package/src/core/decision/service.js +63 -38
  107. package/src/core/decision/task-bindings.js +6 -1
  108. package/src/core/diagnosis-model.js +11 -0
  109. package/src/core/diagnosis.js +10 -1
  110. package/src/core/diagnostic-projection.js +29 -12
  111. package/src/core/diagnostic-record.js +28 -3
  112. package/src/core/efficiency.js +6 -1
  113. package/src/core/events.js +316 -97
  114. package/src/core/execution-prerequisites.js +22 -13
  115. package/src/core/execution.js +26 -40
  116. package/src/core/filesystem.js +11 -0
  117. package/src/core/gate-artifact.js +105 -19
  118. package/src/core/gate-provenance.js +6 -5
  119. package/src/core/handoff.js +26 -14
  120. package/src/core/history.js +39 -35
  121. package/src/core/hypothesis-projection.js +32 -0
  122. package/src/core/information-gain-projection.js +104 -100
  123. package/src/core/inspect.js +12 -3
  124. package/src/core/integration-invocation-policy.js +10 -0
  125. package/src/core/integration-resources.js +41 -34
  126. package/src/core/ledger-event-collection.js +189 -0
  127. package/src/core/ledger-relations.js +23 -0
  128. package/src/core/native-storage.js +12 -0
  129. package/src/core/next-action-continuity.js +31 -9
  130. package/src/core/next-action-phases.js +1 -1
  131. package/src/core/next-action-review-phase.js +2 -2
  132. package/src/core/next-action-verification-phase.js +2 -1
  133. package/src/core/next-action.js +21 -2
  134. package/src/core/phase.js +138 -93
  135. package/src/core/policy-engine.js +12 -13
  136. package/src/core/preflight-consistency.js +72 -59
  137. package/src/core/preflight-loaders.js +19 -12
  138. package/src/core/preflight-model.js +6 -1
  139. package/src/core/preflight.js +29 -12
  140. package/src/core/progress.js +21 -10
  141. package/src/core/protocol-info.js +3 -0
  142. package/src/core/protocol-migration.js +7 -6
  143. package/src/core/reconcile-closure.js +162 -24
  144. package/src/core/recovery-history.js +66 -47
  145. package/src/core/reflection.js +72 -77
  146. package/src/core/responsibility.js +25 -13
  147. package/src/core/route-artifact.js +13 -3
  148. package/src/core/runtime-context.js +5 -0
  149. package/src/core/schema-validation.js +8 -3
  150. package/src/core/settlement.js +12 -1
  151. package/src/core/signing/bundle-file.js +53 -0
  152. package/src/core/signing/bundle-reference.js +15 -0
  153. package/src/core/signing/sigstore.js +60 -33
  154. package/src/core/signing/statement-file.js +27 -0
  155. package/src/core/storage-runtime-registry.js +41 -0
  156. package/src/core/structural-quality/artifacts.js +20 -14
  157. package/src/core/structural-quality/service.js +81 -56
  158. package/src/core/target-layout.js +1 -1
  159. package/src/core/task-claim-state.js +52 -18
  160. package/src/core/task-command.js +21 -1
  161. package/src/core/task-conflict-inspection.js +112 -107
  162. package/src/core/task-descriptor.js +29 -22
  163. package/src/core/task-discovery.js +288 -90
  164. package/src/core/task-lock.js +66 -306
  165. package/src/core/task-migration-validation.js +5 -5
  166. package/src/core/task-migration.js +17 -287
  167. package/src/core/task-snapshot.js +51 -8
  168. package/src/core/task-storage-migration.js +18 -0
  169. package/src/core/templates.js +20 -8
  170. package/src/core/test-intelligence/prune.js +5 -1
  171. package/src/core/trace.js +154 -99
  172. package/src/core/trajectory-evaluation.js +15 -3
  173. package/src/core/trajectory-metrics.js +47 -32
  174. package/src/core/transaction.js +16 -288
  175. package/src/core/verification-scope.js +20 -12
  176. package/src/core/work-state.js +100 -59
  177. package/src/repository-index/binary-manager.js +7 -5
  178. package/src/storage/artifact-bytes.js +22 -0
  179. package/src/storage/attachment-binding.js +11 -0
  180. package/src/storage/attachment-files.js +82 -0
  181. package/src/storage/attachment-references.js +36 -0
  182. package/src/storage/backup.js +53 -0
  183. package/src/storage/capabilities.js +31 -0
  184. package/src/storage/connection-owner.js +119 -0
  185. package/src/storage/connection.js +376 -0
  186. package/src/storage/existing-project-scope.js +45 -0
  187. package/src/storage/export-destination.js +44 -0
  188. package/src/storage/exporter.js +284 -0
  189. package/src/storage/file-durability.js +46 -0
  190. package/src/storage/importer.js +461 -0
  191. package/src/storage/index.js +74 -0
  192. package/src/storage/leases.js +63 -0
  193. package/src/storage/ledger-event-snapshot.js +150 -0
  194. package/src/storage/ledger-relations.js +220 -0
  195. package/src/storage/legacy-maintenance-boundary.js +18 -0
  196. package/src/storage/legacy-signature.js +115 -0
  197. package/src/storage/maintenance-handoff.js +129 -0
  198. package/src/storage/maintenance-owner.js +42 -0
  199. package/src/storage/maintenance.js +310 -0
  200. package/src/storage/metadata-json.js +35 -0
  201. package/src/storage/migration-activate.js +59 -0
  202. package/src/storage/migration-archive.js +57 -0
  203. package/src/storage/migration-attachment-root.js +61 -0
  204. package/src/storage/migration-candidate.js +398 -0
  205. package/src/storage/migration-publication.js +143 -0
  206. package/src/storage/migration-rollback-admission.js +54 -0
  207. package/src/storage/migration-rollback-preparation.js +132 -0
  208. package/src/storage/migration-rollback-publication.js +183 -0
  209. package/src/storage/migration-rollback-release.js +66 -0
  210. package/src/storage/migration-rollback-source-stage.js +103 -0
  211. package/src/storage/migration-rollback.js +43 -0
  212. package/src/storage/migration-source-partition.js +56 -0
  213. package/src/storage/migration-source.js +255 -0
  214. package/src/storage/migration-terminal.js +39 -0
  215. package/src/storage/migration-validation.js +75 -0
  216. package/src/storage/migration.js +43 -0
  217. package/src/storage/operational-attachments.js +49 -0
  218. package/src/storage/operational-context.js +50 -0
  219. package/src/storage/project-backup.js +98 -0
  220. package/src/storage/project-bootstrap.js +24 -0
  221. package/src/storage/project-boundary.js +79 -0
  222. package/src/storage/project-read-snapshot.js +18 -0
  223. package/src/storage/project-replacement.js +137 -0
  224. package/src/storage/project-restore.js +203 -0
  225. package/src/storage/record-definitions.js +45 -0
  226. package/src/storage/repository.js +507 -0
  227. package/src/storage/restore-journal.js +62 -0
  228. package/src/storage/restore-layout.js +30 -0
  229. package/src/storage/restore-preparation.js +84 -0
  230. package/src/storage/restore-replacement-archive.js +162 -0
  231. package/src/storage/restore-replacement-owner.js +31 -0
  232. package/src/storage/restore-replacement-preparation.js +128 -0
  233. package/src/storage/restore-replacement.js +142 -0
  234. package/src/storage/restore-terminal.js +32 -0
  235. package/src/storage/runtime.js +31 -0
  236. package/src/storage/schema.js +214 -0
  237. package/src/storage/singleton-import.js +84 -0
  238. package/src/storage/snapshot-continuity.js +16 -0
  239. package/src/storage/snapshot.js +36 -0
  240. package/src/storage/storage-marker.js +79 -0
  241. package/src/storage/task-guards.js +107 -0
  242. package/src/storage/topology.js +42 -0
  243. package/src/storage/transaction.js +179 -0
  244. package/src/storage/unit-of-work.js +663 -0
  245. package/src/storage/windows-process-incarnation.js +206 -0
  246. package/.forgeloop/forgeloop.gitignore +0 -4
  247. package/LICENSE-DOCS.md +0 -13
  248. package/src/core/transaction-maintenance.js +0 -70
@@ -1,15 +1,21 @@
1
+ import { needsExistingProjectScope, withExistingProjectScope } from "../storage/existing-project-scope.js";
1
2
  import { randomUUID } from "node:crypto";
2
- import { link, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
3
- import path from "node:path";
3
+ import { readFile } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
6
6
  import { taskLockPath } from "./task-paths.js";
7
- import {
8
- E_PROJECT_CLAIMS_LOCK_INCONSISTENT,
9
- E_TASK_LOCKED,
10
- } from "./error-codes.js";
7
+ import { getOperationalStore } from "../storage/operational-context.js";
8
+ import { assertStorageMaintenanceInactive } from "../storage/maintenance.js";
11
9
 
12
10
  export async function readLockInfo(target, taskId) {
11
+ if (await needsExistingProjectScope(target)) {
12
+ return withExistingProjectScope(target, () => readLockInfo(target, taskId), { readOnly: true });
13
+ }
14
+ const store = getOperationalStore(target);
15
+ if (store) {
16
+ const { readOperationalLease } = await import("../storage/leases.js");
17
+ return readOperationalLease(store, taskId);
18
+ }
13
19
  const relativePath = taskLockPath(taskId);
14
20
  await assertSafePath(target, relativePath);
15
21
  const fullPath = ensureWithin(target, relativePath);
@@ -50,13 +56,6 @@ export function isValidTaskLockIdentity(lock) {
50
56
  && hasLeaseIdentity(lock);
51
57
  }
52
58
 
53
- export function isValidProjectClaimsLockIdentity(lock) {
54
- return Boolean(lock)
55
- && !lock.corrupted
56
- && lock.scope === "claims-reservation"
57
- && hasLeaseIdentity(lock);
58
- }
59
-
60
59
  function classifyLeaseWindow(lock, now) {
61
60
  const heartbeat = Date.parse(lock.heartbeatAt ?? lock.acquiredAt);
62
61
  if (!Number.isFinite(heartbeat)) return { status: "UNKNOWN", stale: false };
@@ -81,191 +80,24 @@ export function currentProcessStartToken(now = Date.now(), uptimeSeconds = proce
81
80
  return `${process.pid}:${Math.max(0, Math.floor(now - (uptimeSeconds * 1000)))}`;
82
81
  }
83
82
 
84
- export const CLAIMS_LOCK_REL_PATH = ".forgeloop/.claims.lock";
85
-
86
- export async function readProjectClaimsLockInfo(target) {
87
- await assertSafePath(target, CLAIMS_LOCK_REL_PATH);
88
- const fullPath = ensureWithin(target, CLAIMS_LOCK_REL_PATH);
89
-
90
- if (!(await fileExists(fullPath))) {
91
- return null;
92
- }
93
-
94
- try {
95
- const raw = await readFile(fullPath, "utf8");
96
- return JSON.parse(raw);
97
- } catch {
98
- return { corrupted: true };
99
- }
100
- }
101
-
102
- export function classifyProjectClaimsLock(lock, now = Date.now()) {
103
- if (!lock) return { status: "NONE", stale: false };
104
- if (lock.corrupted) return { status: "CORRUPT", stale: false };
105
- if (!isValidProjectClaimsLockIdentity(lock)) return { status: "UNKNOWN", stale: false };
106
- return classifyLeaseWindow(lock, now);
107
- }
108
-
109
- function projectClaimsLockError(classification, lockInfo, reason = null) {
110
- if (classification.status === "LIVE") {
111
- const error = new Error(
112
- `Project write claims reservation is locked by operation "${lockInfo?.operation ?? "unknown"}" (pid: ${lockInfo?.pid ?? "unknown"}, acquired: ${lockInfo?.acquiredAt ?? "unknown"}).`,
113
- );
114
- error.code = E_TASK_LOCKED;
115
- error.lockInfo = lockInfo;
116
- error.classification = classification;
117
- return error;
118
- }
119
- const error = new Error(
120
- `Project write claims lock ownership is ${classification.status}${reason ? ` (${reason})` : ""}; refusing unsafe claim mutation`,
121
- );
122
- error.code = E_PROJECT_CLAIMS_LOCK_INCONSISTENT;
123
- error.lockInfo = lockInfo;
124
- error.classification = classification;
125
- if (reason) error.reason = reason;
126
- return error;
127
- }
128
-
129
- export async function releaseStaleProjectClaimsLockIfUnchanged(target, expectedLock, { now = Date.now() } = {}) {
130
- await assertSafePath(target, CLAIMS_LOCK_REL_PATH);
131
- const fullPath = ensureWithin(target, CLAIMS_LOCK_REL_PATH);
132
- const expectedClassification = classifyProjectClaimsLock(expectedLock, now);
133
- if (expectedClassification.status !== "STALE") {
134
- return { released: false, reason: "EXPECTED_LOCK_NOT_STALE", classification: expectedClassification };
135
- }
136
-
137
- const quarantinePath = `${fullPath}.releasing-${randomUUID()}`;
138
- try {
139
- await rename(fullPath, quarantinePath);
140
- } catch (error) {
141
- if (error.code === "ENOENT") return { released: false, reason: "LOCK_MISSING" };
142
- throw error;
143
- }
144
-
145
- let observed;
146
- try {
147
- observed = JSON.parse(await readFile(quarantinePath, "utf8"));
148
- } catch {
149
- await restoreQuarantinedLock(quarantinePath, fullPath);
150
- return { released: false, reason: "LOCK_CORRUPT", classification: { status: "CORRUPT", stale: false } };
151
- }
152
-
153
- const observedClassification = classifyProjectClaimsLock(observed, now);
154
- if (!sameObservedLock(observed, expectedLock, { identityIsValid: isValidProjectClaimsLockIdentity })
155
- || observedClassification.status !== "STALE") {
156
- await restoreQuarantinedLock(quarantinePath, fullPath);
157
- return {
158
- released: false,
159
- reason: sameObservedLock(observed, expectedLock, { identityIsValid: isValidProjectClaimsLockIdentity })
160
- ? "LOCK_NOT_STALE"
161
- : "LOCK_CHANGED",
162
- currentLock: observed,
163
- classification: observedClassification,
164
- };
165
- }
166
-
167
- await unlink(quarantinePath);
168
- return { released: true, previousLock: observed, classification: observedClassification };
169
- }
170
-
171
- export async function acquireProjectClaimsLock(target, operation = "claim-reservation") {
172
- await assertSafePath(target, CLAIMS_LOCK_REL_PATH);
173
- const fullPath = ensureWithin(target, CLAIMS_LOCK_REL_PATH);
174
-
175
- await mkdir(path.dirname(fullPath), { recursive: true });
176
-
177
- const acquiredAt = new Date().toISOString();
178
- const lockData = {
179
- lockId: randomUUID(),
180
- scope: "claims-reservation",
181
- operation,
182
- pid: process.pid,
183
- hostname: os.hostname(),
184
- processStartToken: currentProcessStartToken(),
185
- ownerInstanceId: randomUUID(),
186
- acquiredAt,
187
- heartbeatAt: acquiredAt,
188
- leaseMs: 300000,
189
- };
190
-
191
- let fileHandle = null;
192
- for (let attempt = 0; attempt < 2; attempt += 1) {
193
- try {
194
- fileHandle = await open(fullPath, "wx");
195
- break;
196
- } catch (error) {
197
- if (error.code !== "EEXIST") throw error;
198
- const existing = await readProjectClaimsLockInfo(target);
199
- const classification = classifyProjectClaimsLock(existing);
200
- if (classification.status === "STALE" && attempt === 0) {
201
- const released = await releaseStaleProjectClaimsLockIfUnchanged(target, existing);
202
- if (released.released || released.reason === "LOCK_MISSING") continue;
203
- throw projectClaimsLockError(
204
- released.classification ?? { status: "UNKNOWN", stale: false },
205
- released.currentLock ?? existing,
206
- released.reason,
207
- );
208
- }
209
- throw projectClaimsLockError(classification, existing);
210
- }
211
- }
212
- if (!fileHandle) {
213
- throw projectClaimsLockError({ status: "UNKNOWN", stale: false }, null, "ACQUISITION_RETRY_EXHAUSTED");
214
- }
215
-
216
- try {
217
- await fileHandle.writeFile(`${JSON.stringify(lockData, null, 2)}\n`, "utf8");
218
- await fileHandle.close();
219
- return {
220
- lockData,
221
- release: async () => {
222
- try {
223
- const current = await readProjectClaimsLockInfo(target);
224
- if (current && current.lockId === lockData.lockId) {
225
- await unlink(fullPath);
226
- }
227
- } catch {
228
- // ignore
229
- }
230
- },
231
- };
232
- } catch (error) {
233
- try {
234
- await fileHandle.close();
235
- } catch {
236
- // ignore
237
- }
238
- try {
239
- await unlink(fullPath);
240
- } catch {
241
- // ignore
242
- }
243
- throw error;
244
- }
245
- }
246
-
247
83
  export async function withProjectClaimsLock(target, operationOrCallback, callback) {
84
+ if (!getOperationalStore(target)) {
85
+ const { withProjectStorage } = await import("../storage/project-boundary.js");
86
+ return withProjectStorage(target, () => withProjectClaimsLock(target, operationOrCallback, callback));
87
+ }
248
88
  let operation = operationOrCallback;
249
89
  let fn = callback;
250
90
  if (typeof operationOrCallback === "function" && callback === undefined) {
251
91
  fn = operationOrCallback;
252
92
  operation = "claim-reservation";
253
93
  }
254
- const lock = await acquireProjectClaimsLock(target, operation);
255
- try {
256
- return await fn(lock.lockData);
257
- } finally {
258
- await lock.release();
259
- }
94
+ // The preparation scope records the authoritative claim inputs; its short
95
+ // BEGIN IMMEDIATE commit rechecks those inputs and reserves claims atomically.
96
+ return fn({ kind: "sqlite-claim-reservation", operation });
260
97
  }
261
98
 
262
99
  export async function acquireTaskLock(target, taskId, operation = "mutation") {
263
- const relativePath = taskLockPath(taskId);
264
- await assertSafePath(target, relativePath);
265
- const fullPath = ensureWithin(target, relativePath);
266
-
267
- await mkdir(path.dirname(fullPath), { recursive: true });
268
-
100
+ await assertStorageMaintenanceInactive(target);
269
101
  const acquiredAt = new Date().toISOString();
270
102
  const lockData = {
271
103
  lockId: randomUUID(),
@@ -279,80 +111,46 @@ export async function acquireTaskLock(target, taskId, operation = "mutation") {
279
111
  heartbeatAt: acquiredAt,
280
112
  leaseMs: 300000,
281
113
  };
282
-
283
- let fileHandle;
284
- try {
285
- fileHandle = await open(fullPath, "wx");
286
- } catch (error) {
287
- if (error.code === "EEXIST") {
288
- const existing = await readLockInfo(target, taskId);
289
- const err = new Error(
290
- `Task "${taskId}" is locked by operation "${existing?.operation ?? "unknown"}" (pid: ${existing?.pid ?? "unknown"}, acquired: ${existing?.acquiredAt ?? "unknown"}). Use 'forgeloop task-unlock --task ${taskId} --force' if the process died.`,
291
- );
292
- err.code = E_TASK_LOCKED;
293
- err.lockInfo = existing;
294
- err.taskId = taskId;
295
- throw err;
296
- }
297
- throw error;
298
- }
299
-
300
- try {
301
- await fileHandle.writeFile(`${JSON.stringify(lockData, null, 2)}\n`, "utf8");
302
- await fileHandle.close();
303
- return {
304
- lockData,
305
- release: async () => {
306
- try {
307
- const current = await readLockInfo(target, taskId);
308
- if (current && current.lockId === lockData.lockId) {
309
- await unlink(fullPath);
310
- }
311
- } catch {
312
- // ignore already unlinked or overwritten
313
- }
314
- },
315
- };
316
- } catch (error) {
317
- try {
318
- await fileHandle.close();
319
- } catch {
320
- // ignore
321
- }
322
- try {
323
- await unlink(fullPath);
324
- } catch {
325
- // ignore
326
- }
327
- throw error;
328
- }
114
+ const store = getOperationalStore(target);
115
+ if (store) {
116
+ const { acquireOperationalLease } = await import("../storage/leases.js");
117
+ return acquireOperationalLease(store, taskId, lockData);
118
+ }
119
+ return acquireStandaloneOperationalLease(target, taskId, operation);
120
+ }
121
+
122
+ /** Keep only the connection scope alive; acquisition/release hold no SQL transaction. */
123
+ async function acquireStandaloneOperationalLease(target, taskId, operation) {
124
+ let resolveReady;
125
+ let rejectReady;
126
+ let releaseScope;
127
+ const ready = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
128
+ const released = new Promise(resolve => { releaseScope = resolve; });
129
+ const { withProjectStorage } = await import("../storage/project-boundary.js");
130
+ const completion = withProjectStorage(target, async () => {
131
+ const handle = await acquireTaskLock(target, taskId, operation);
132
+ resolveReady(handle.lockData);
133
+ await released;
134
+ return handle.release();
135
+ }, { existingOnly: true });
136
+ completion.catch(rejectReady);
137
+ const lockData = await ready;
138
+ return { lockData, release: () => { releaseScope(); return completion; } };
329
139
  }
330
140
 
331
141
  export async function forceUnlockTask(target, taskId, { staleOnly = false } = {}) {
332
- const relativePath = taskLockPath(taskId);
333
- await assertSafePath(target, relativePath);
334
- const fullPath = ensureWithin(target, relativePath);
335
-
336
- if (!(await fileExists(fullPath))) {
337
- return { unlocked: false, message: "No active lock found" };
142
+ if (!getOperationalStore(target)) {
143
+ const { withProjectStorage } = await import("../storage/project-boundary.js");
144
+ return withProjectStorage(target, () => forceUnlockTask(target, taskId, { staleOnly }), { existingOnly: true });
338
145
  }
339
-
146
+ const store = getOperationalStore(target);
340
147
  const existing = await readLockInfo(target, taskId);
148
+ if (!existing) return { unlocked: false, message: "No active lock found" };
341
149
  const classification = classifyLockStaleness(existing);
342
- if (staleOnly) {
343
- if (!classification.stale) {
344
- return { unlocked: false, previousLock: existing, classification };
345
- }
346
- const released = await releaseStaleTaskLockIfUnchanged(target, taskId, existing);
347
- return {
348
- unlocked: released.released,
349
- previousLock: released.previousLock ?? existing,
350
- classification: released.classification ?? classification,
351
- ...(released.reason ? { reason: released.reason } : {}),
352
- };
353
- }
354
- await unlink(fullPath);
355
- return { unlocked: true, previousLock: existing, classification };
150
+ if (staleOnly && !classification.stale) return { unlocked: false, previousLock: existing, classification };
151
+ const { releaseOperationalLeaseIfUnchanged } = await import("../storage/leases.js");
152
+ const released = releaseOperationalLeaseIfUnchanged(store, taskId, existing, { force: !staleOnly, classify: classifyLockStaleness, sameIdentity: sameObservedTaskLock });
153
+ return { unlocked: released.released, previousLock: released.previousLock ?? existing, classification: released.classification ?? classification, ...(released.reason ? { reason: released.reason } : {}) };
356
154
  }
357
155
 
358
156
  function sameObservedLock(left, right, { identityIsValid = isValidTaskLockIdentity } = {}) {
@@ -369,61 +167,23 @@ function sameObservedTaskLock(left, right, taskId) {
369
167
  && right.taskId === taskId;
370
168
  }
371
169
 
372
- async function restoreQuarantinedLock(quarantinePath, fullPath) {
373
- try {
374
- await link(quarantinePath, fullPath);
375
- } catch (error) {
376
- if (error.code !== "EEXIST") throw error;
377
- } finally {
378
- try {
379
- await unlink(quarantinePath);
380
- } catch {
381
- // ignore an already-consumed quarantine entry
382
- }
383
- }
384
- }
385
-
386
170
  export async function releaseStaleTaskLockIfUnchanged(target, taskId, expectedLock, { now = Date.now() } = {}) {
387
- const relativePath = taskLockPath(taskId);
388
- await assertSafePath(target, relativePath);
389
- const fullPath = ensureWithin(target, relativePath);
390
- const expectedClassification = classifyLockStaleness(expectedLock, now);
391
- if (expectedClassification.status !== "STALE") {
392
- return { released: false, reason: "EXPECTED_LOCK_NOT_STALE", classification: expectedClassification };
393
- }
394
-
395
- const quarantinePath = `${fullPath}.releasing-${randomUUID()}`;
396
- try {
397
- await rename(fullPath, quarantinePath);
398
- } catch (error) {
399
- if (error.code === "ENOENT") return { released: false, reason: "LOCK_MISSING" };
400
- throw error;
171
+ if (!getOperationalStore(target)) {
172
+ const { withProjectStorage } = await import("../storage/project-boundary.js");
173
+ return withProjectStorage(target, () => releaseStaleTaskLockIfUnchanged(target, taskId, expectedLock, { now }), { existingOnly: true });
401
174
  }
402
-
403
- let observed;
404
- try {
405
- observed = JSON.parse(await readFile(quarantinePath, "utf8"));
406
- } catch {
407
- await restoreQuarantinedLock(quarantinePath, fullPath);
408
- return { released: false, reason: "LOCK_CORRUPT" };
409
- }
410
-
411
- const observedClassification = classifyLockStaleness(observed, now);
412
- if (!sameObservedTaskLock(observed, expectedLock, taskId) || observedClassification.status !== "STALE") {
413
- await restoreQuarantinedLock(quarantinePath, fullPath);
414
- return {
415
- released: false,
416
- reason: sameObservedTaskLock(observed, expectedLock, taskId) ? "LOCK_NOT_STALE" : "LOCK_CHANGED",
417
- currentLock: observed,
418
- classification: observedClassification,
419
- };
420
- }
421
-
422
- await unlink(quarantinePath);
423
- return { released: true, previousLock: observed, classification: observedClassification };
175
+ const store = getOperationalStore(target);
176
+ const expectedClassification = classifyLockStaleness(expectedLock, now);
177
+ if (expectedClassification.status !== "STALE") return { released: false, reason: "EXPECTED_LOCK_NOT_STALE", classification: expectedClassification };
178
+ const { releaseOperationalLeaseIfUnchanged } = await import("../storage/leases.js");
179
+ return releaseOperationalLeaseIfUnchanged(store, taskId, expectedLock, { now, classify: classifyLockStaleness, sameIdentity: sameObservedTaskLock });
424
180
  }
425
181
 
426
182
  export async function withTaskLock(target, taskId, operationOrCallback, callback) {
183
+ if (!getOperationalStore(target)) {
184
+ const { withProjectStorage } = await import("../storage/project-boundary.js");
185
+ return withProjectStorage(target, () => withTaskLock(target, taskId, operationOrCallback, callback));
186
+ }
427
187
  let operation = operationOrCallback;
428
188
  let fn = callback;
429
189
  if (typeof operationOrCallback === "function" && callback === undefined) {
@@ -2,8 +2,8 @@ import { createHash } from "node:crypto";
2
2
  import { readFile, readdir } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { ensureWithin, fileExists } from "./filesystem.js";
5
- import { readJsonArtifact } from "./artifacts.js";
6
- import { validateEventLedger } from "./events.js";
5
+ import { readPortableJsonArtifact } from "./artifacts.js";
6
+ import { validatePortableEventLedger } from "./events.js";
7
7
  import {
8
8
  E_TASK_MIGRATION_IDENTITY_MISMATCH,
9
9
  E_TASK_MIGRATION_INVALID,
@@ -42,7 +42,7 @@ export async function validateMigrationSnapshot(target, { taskId, packageRoot, p
42
42
  const absPath = ensureWithin(target, relPath);
43
43
  if (await fileExists(absPath)) {
44
44
  try {
45
- const { value } = await readJsonArtifact(target, relPath, schemaName, packageRoot);
45
+ const { value } = await readPortableJsonArtifact(target, relPath, schemaName, packageRoot);
46
46
  checkIdentity(value, relPath);
47
47
  artifactFingerprints[key] = await fingerprintFile(absPath);
48
48
  } catch (err) {
@@ -59,7 +59,7 @@ export async function validateMigrationSnapshot(target, { taskId, packageRoot, p
59
59
  if (paths.events) {
60
60
  const absPath = ensureWithin(target, paths.events);
61
61
  if (await fileExists(absPath)) {
62
- const ledger = await validateEventLedger(target, packageRoot, { eventsPath: paths.events });
62
+ const ledger = await validatePortableEventLedger(target, packageRoot, { eventsPath: paths.events });
63
63
  if (!ledger.valid) {
64
64
  const error = new Error(`Event ledger validation failed for ${paths.events}: ${ledger.errors?.[0]?.message ?? "invalid ledger"}`);
65
65
  error.code = E_TASK_MIGRATION_INVALID;
@@ -103,7 +103,7 @@ export async function validateMigrationSnapshot(target, { taskId, packageRoot, p
103
103
  const fileAbsPath = path.join(absPath, entry.name);
104
104
 
105
105
  try {
106
- const { value } = await readJsonArtifact(target, fileRelPath, schemaName, packageRoot);
106
+ const { value } = await readPortableJsonArtifact(target, fileRelPath, schemaName, packageRoot);
107
107
  checkIdentity(value, fileRelPath);
108
108
  const hash = await fingerprintFile(fileAbsPath);
109
109
  fileHashes.push({ relPath: entry.name, hash });