@cassiomc1/forgeloop 1.14.0 → 2.0.0-rc.4

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 (250) 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 +30 -40
  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 +110 -10
  32. package/docs/SELF_HOSTED_TESTS.md +121 -0
  33. package/docs/TROUBLESHOOTING.md +33 -0
  34. package/docs/documentation-manifest.json +25 -12
  35. package/package.json +15 -10
  36. package/schemas/task-bundle.schema.json +7 -2
  37. package/scripts/generate-agent-protocol-summary.mjs +2 -2
  38. package/scripts/run-mcp-clean-tests.mjs +30 -0
  39. package/src/adapters/agent-browser/provider.js +14 -11
  40. package/src/adapters/emulated-services/provider.js +11 -4
  41. package/src/adapters/temporary-paths.js +18 -0
  42. package/src/cli.js +146 -210
  43. package/src/commands/action-show.js +5 -2
  44. package/src/commands/advance.js +5 -1
  45. package/src/commands/baseline.js +13 -11
  46. package/src/commands/checkpoint-revalidate.js +24 -8
  47. package/src/commands/complete.js +19 -3
  48. package/src/commands/contract-create.js +3 -1
  49. package/src/commands/doctor.js +36 -13
  50. package/src/commands/gate-record.js +20 -31
  51. package/src/commands/gate-revalidate.js +11 -13
  52. package/src/commands/handoff-list.js +6 -1
  53. package/src/commands/handoff-show.js +6 -1
  54. package/src/commands/migrate-protocol.js +2 -2
  55. package/src/commands/model-route.js +9 -1
  56. package/src/commands/next.js +7 -1
  57. package/src/commands/policy-diff.js +25 -4
  58. package/src/commands/progress.js +17 -11
  59. package/src/commands/record-diagnosis.js +3 -0
  60. package/src/commands/semantic-plan.js +26 -2
  61. package/src/commands/status.js +6 -1
  62. package/src/commands/storage-backup.js +15 -0
  63. package/src/commands/storage-migrate.js +5 -0
  64. package/src/commands/storage-migration-resume.js +5 -0
  65. package/src/commands/storage-migration-status.js +44 -0
  66. package/src/commands/storage-restore-resume.js +7 -0
  67. package/src/commands/storage-restore.js +8 -0
  68. package/src/commands/task-abandon.js +1 -1
  69. package/src/commands/task-list.js +8 -43
  70. package/src/commands/task-migrate.js +3 -3
  71. package/src/commands/task-recover.js +10 -2
  72. package/src/commands/task-scope.js +18 -2
  73. package/src/commands/task-show.js +16 -2
  74. package/src/commands/validate-protocol.js +29 -14
  75. package/src/commands/validate-receipt.js +8 -1
  76. package/src/config/guides.json +4 -0
  77. package/src/core/action-ledger-projection.js +10 -3
  78. package/src/core/actions.js +38 -75
  79. package/src/core/activation.js +11 -3
  80. package/src/core/approvals.js +17 -51
  81. package/src/core/artifact-registry.js +59 -2
  82. package/src/core/artifacts.js +81 -27
  83. package/src/core/attachment-paths.js +45 -0
  84. package/src/core/attestation-coverage.js +6 -1
  85. package/src/core/attestation-verifier.js +21 -2
  86. package/src/core/attestation.js +17 -6
  87. package/src/core/audit.js +6 -1
  88. package/src/core/bundles.js +160 -66
  89. package/src/core/checkpoint-revalidation.js +12 -10
  90. package/src/core/cli-command-definitions.js +109 -7
  91. package/src/core/code-manifest.js +9 -3
  92. package/src/core/command-executors.js +182 -126
  93. package/src/core/command-input.js +21 -1
  94. package/src/core/completion-artifacts.js +85 -36
  95. package/src/core/completion-ownership.js +19 -8
  96. package/src/core/completion-recovery-rebind.js +11 -1
  97. package/src/core/completion.js +60 -9
  98. package/src/core/conformance.js +2 -1
  99. package/src/core/continuity-lint.js +13 -2
  100. package/src/core/continuity-reconciliation.js +16 -3
  101. package/src/core/continuity.js +16 -8
  102. package/src/core/contract-bootstrap-recovery.js +42 -41
  103. package/src/core/contract-revision.js +8 -9
  104. package/src/core/decision/artifact.js +2 -1
  105. package/src/core/decision/events.js +9 -6
  106. package/src/core/decision/resolver.js +19 -11
  107. package/src/core/decision/service.js +63 -38
  108. package/src/core/decision/task-bindings.js +6 -1
  109. package/src/core/diagnosis-model.js +11 -0
  110. package/src/core/diagnosis.js +10 -1
  111. package/src/core/diagnostic-projection.js +29 -12
  112. package/src/core/diagnostic-record.js +28 -3
  113. package/src/core/efficiency.js +6 -1
  114. package/src/core/events.js +316 -97
  115. package/src/core/execution-prerequisites.js +22 -13
  116. package/src/core/execution.js +26 -40
  117. package/src/core/filesystem.js +11 -0
  118. package/src/core/gate-artifact.js +105 -19
  119. package/src/core/gate-provenance.js +6 -5
  120. package/src/core/handoff.js +26 -14
  121. package/src/core/history.js +39 -35
  122. package/src/core/hypothesis-projection.js +32 -0
  123. package/src/core/information-gain-projection.js +104 -100
  124. package/src/core/inspect.js +12 -3
  125. package/src/core/integration-invocation-policy.js +10 -0
  126. package/src/core/integration-resources.js +41 -34
  127. package/src/core/ledger-event-collection.js +189 -0
  128. package/src/core/ledger-relations.js +23 -0
  129. package/src/core/native-storage.js +12 -0
  130. package/src/core/next-action-continuity.js +31 -9
  131. package/src/core/next-action-phases.js +1 -1
  132. package/src/core/next-action-review-phase.js +2 -2
  133. package/src/core/next-action-verification-phase.js +2 -1
  134. package/src/core/next-action.js +21 -2
  135. package/src/core/phase.js +138 -93
  136. package/src/core/policy-engine.js +12 -13
  137. package/src/core/preflight-consistency.js +72 -59
  138. package/src/core/preflight-loaders.js +19 -12
  139. package/src/core/preflight-model.js +6 -1
  140. package/src/core/preflight.js +29 -12
  141. package/src/core/progress.js +21 -10
  142. package/src/core/protocol-info.js +3 -0
  143. package/src/core/protocol-migration.js +7 -6
  144. package/src/core/reconcile-closure.js +162 -24
  145. package/src/core/recovery-history.js +66 -47
  146. package/src/core/reflection.js +72 -77
  147. package/src/core/responsibility.js +25 -13
  148. package/src/core/route-artifact.js +13 -3
  149. package/src/core/runtime-context.js +5 -0
  150. package/src/core/schema-validation.js +8 -3
  151. package/src/core/settlement.js +12 -1
  152. package/src/core/signing/bundle-file.js +53 -0
  153. package/src/core/signing/bundle-reference.js +15 -0
  154. package/src/core/signing/sigstore.js +60 -33
  155. package/src/core/signing/statement-file.js +27 -0
  156. package/src/core/storage-runtime-registry.js +41 -0
  157. package/src/core/structural-quality/artifacts.js +20 -14
  158. package/src/core/structural-quality/service.js +81 -56
  159. package/src/core/target-layout.js +1 -1
  160. package/src/core/task-claim-state.js +52 -18
  161. package/src/core/task-command.js +21 -1
  162. package/src/core/task-conflict-inspection.js +112 -107
  163. package/src/core/task-descriptor.js +29 -22
  164. package/src/core/task-discovery.js +288 -90
  165. package/src/core/task-lock.js +66 -306
  166. package/src/core/task-migration-validation.js +5 -5
  167. package/src/core/task-migration.js +17 -287
  168. package/src/core/task-snapshot.js +51 -8
  169. package/src/core/task-storage-migration.js +18 -0
  170. package/src/core/templates.js +20 -8
  171. package/src/core/test-intelligence/prune.js +5 -1
  172. package/src/core/trace.js +154 -99
  173. package/src/core/trajectory-evaluation.js +15 -3
  174. package/src/core/trajectory-metrics.js +47 -32
  175. package/src/core/transaction.js +16 -288
  176. package/src/core/verification-scope.js +20 -12
  177. package/src/core/work-state.js +100 -59
  178. package/src/repository-index/binary-manager.js +15 -7
  179. package/src/storage/artifact-bytes.js +22 -0
  180. package/src/storage/attachment-binding.js +11 -0
  181. package/src/storage/attachment-files.js +82 -0
  182. package/src/storage/attachment-references.js +36 -0
  183. package/src/storage/backup.js +53 -0
  184. package/src/storage/capabilities.js +31 -0
  185. package/src/storage/connection-owner.js +119 -0
  186. package/src/storage/connection.js +376 -0
  187. package/src/storage/existing-project-scope.js +45 -0
  188. package/src/storage/export-destination.js +44 -0
  189. package/src/storage/exporter.js +284 -0
  190. package/src/storage/file-durability.js +46 -0
  191. package/src/storage/importer.js +461 -0
  192. package/src/storage/index.js +74 -0
  193. package/src/storage/leases.js +63 -0
  194. package/src/storage/ledger-event-snapshot.js +150 -0
  195. package/src/storage/ledger-relations.js +220 -0
  196. package/src/storage/legacy-maintenance-boundary.js +18 -0
  197. package/src/storage/legacy-signature.js +115 -0
  198. package/src/storage/maintenance-handoff.js +129 -0
  199. package/src/storage/maintenance-owner.js +42 -0
  200. package/src/storage/maintenance.js +310 -0
  201. package/src/storage/metadata-json.js +35 -0
  202. package/src/storage/migration-activate.js +59 -0
  203. package/src/storage/migration-archive.js +57 -0
  204. package/src/storage/migration-attachment-root.js +61 -0
  205. package/src/storage/migration-candidate.js +398 -0
  206. package/src/storage/migration-publication.js +143 -0
  207. package/src/storage/migration-rollback-admission.js +54 -0
  208. package/src/storage/migration-rollback-preparation.js +132 -0
  209. package/src/storage/migration-rollback-publication.js +183 -0
  210. package/src/storage/migration-rollback-release.js +66 -0
  211. package/src/storage/migration-rollback-source-stage.js +103 -0
  212. package/src/storage/migration-rollback.js +43 -0
  213. package/src/storage/migration-source-partition.js +56 -0
  214. package/src/storage/migration-source.js +255 -0
  215. package/src/storage/migration-terminal.js +39 -0
  216. package/src/storage/migration-validation.js +75 -0
  217. package/src/storage/migration.js +43 -0
  218. package/src/storage/operational-attachments.js +49 -0
  219. package/src/storage/operational-context.js +50 -0
  220. package/src/storage/project-backup.js +98 -0
  221. package/src/storage/project-bootstrap.js +24 -0
  222. package/src/storage/project-boundary.js +79 -0
  223. package/src/storage/project-read-snapshot.js +18 -0
  224. package/src/storage/project-replacement.js +137 -0
  225. package/src/storage/project-restore.js +203 -0
  226. package/src/storage/record-definitions.js +45 -0
  227. package/src/storage/repository.js +507 -0
  228. package/src/storage/restore-execution-provenance.js +26 -0
  229. package/src/storage/restore-journal.js +62 -0
  230. package/src/storage/restore-layout.js +30 -0
  231. package/src/storage/restore-preparation.js +84 -0
  232. package/src/storage/restore-replacement-archive.js +162 -0
  233. package/src/storage/restore-replacement-owner.js +31 -0
  234. package/src/storage/restore-replacement-preparation.js +128 -0
  235. package/src/storage/restore-replacement.js +142 -0
  236. package/src/storage/restore-terminal.js +32 -0
  237. package/src/storage/runtime.js +31 -0
  238. package/src/storage/schema.js +214 -0
  239. package/src/storage/singleton-import.js +84 -0
  240. package/src/storage/snapshot-continuity.js +16 -0
  241. package/src/storage/snapshot.js +36 -0
  242. package/src/storage/storage-marker.js +79 -0
  243. package/src/storage/task-guards.js +107 -0
  244. package/src/storage/topology.js +42 -0
  245. package/src/storage/transaction.js +179 -0
  246. package/src/storage/unit-of-work.js +667 -0
  247. package/src/storage/windows-process-incarnation.js +206 -0
  248. package/.forgeloop/forgeloop.gitignore +0 -4
  249. package/LICENSE-DOCS.md +0 -13
  250. package/src/core/transaction-maintenance.js +0 -70
@@ -1,50 +1,29 @@
1
- import { randomUUID } from "node:crypto";
2
- import { AsyncLocalStorage } from "node:async_hooks";
3
- import { mkdir, open, readdir, readFile, realpath, rename, stat, truncate, unlink } from "node:fs/promises";
4
- import { setTimeout as delay } from "node:timers/promises";
5
- import path from "node:path";
1
+ import { readdir } from "node:fs/promises";
6
2
 
7
- import { assertSafePath, ensureWithin, fileExists, writeFileAtomic } from "./filesystem.js";
8
- import { withTaskLock } from "./task-lock.js";
9
- import { E_TASK_LOCKED } from "./error-codes.js";
3
+ import { assertSafePath, fileExists } from "./filesystem.js";
4
+ import { getOperationalStore } from "../storage/operational-context.js";
10
5
 
11
6
  const TRANSACTION_ROOT = ".forgeloop/.txn";
12
- const LOCK_WAIT_TIMEOUT_MS = 5_000;
13
- const transactionContext = new AsyncLocalStorage();
14
-
15
- export function getActiveTaskTransaction() {
16
- return transactionContext.getStore() ?? null;
17
- }
18
7
 
19
8
  export async function getTaskTransaction(target) {
20
- const transaction = getActiveTaskTransaction();
21
- if (transaction && transaction.target !== await realpath(target)) {
22
- const error = new Error("Cannot reuse a task transaction for a different project");
23
- error.code = "E_TASK_CONTEXT_MISMATCH";
24
- throw error;
25
- }
26
- return transaction;
9
+ return getOperationalStore(target)?.transaction ?? null;
27
10
  }
28
11
 
29
12
  const TERMINAL_STATUSES = new Set(["COMMITTED", "ROLLED_BACK", "ABORTED"]);
30
13
 
31
- function transactionPath(transactionId) {
32
- return `${TRANSACTION_ROOT}/${transactionId}`;
33
- }
34
-
35
- async function writeManifest(target, relativePath, manifest) {
36
- await writeFileAtomic(ensureWithin(target, relativePath), `${JSON.stringify(manifest, null, 2)}\n`);
37
- }
38
-
39
14
  export async function findIncompleteTransactions(target) {
40
- const root = ensureWithin(target, TRANSACTION_ROOT);
15
+ const root = await assertSafePath(target, TRANSACTION_ROOT);
41
16
  if (!(await fileExists(root))) return [];
42
17
  const entries = await readdir(root, { withFileTypes: true });
43
18
  const found = [];
44
19
  for (const entry of entries) {
20
+ await assertSafePath(target, `${TRANSACTION_ROOT}/${entry.name}`);
45
21
  if (!entry.isDirectory()) continue;
22
+ const relative = `${TRANSACTION_ROOT}/${entry.name}/manifest.json`;
23
+ await assertSafePath(target, relative);
46
24
  try {
47
- const manifest = JSON.parse(await readFile(path.join(root, entry.name, "manifest.json"), "utf8"));
25
+ const { readStorageMetadataJson } = await import("../storage/metadata-json.js");
26
+ const manifest = await readStorageMetadataJson(target, relative);
48
27
  if (!TERMINAL_STATUSES.has(manifest.status)) found.push(manifest);
49
28
  } catch {
50
29
  found.push({ transactionId: entry.name, status: "ABANDONED", malformed: true });
@@ -53,263 +32,12 @@ export async function findIncompleteTransactions(target) {
53
32
  return found;
54
33
  }
55
34
 
56
- function writePath(entry) {
57
- return typeof entry === "string" ? entry : entry.path;
58
- }
59
-
60
- async function rollbackPublishedWrites(target, root, manifest) {
61
- const writes = [...manifest.writes].reverse();
62
- for (const entry of writes) {
63
- if (!entry || typeof entry !== "object") continue;
64
- const relativePath = writePath(entry);
65
- await assertSafePath(target, relativePath);
66
- if (entry.kind === "APPEND") {
67
- if (!entry.appendStarted) continue;
68
- const destination = ensureWithin(target, relativePath);
69
- const originalSize = Number.isInteger(entry.originalSize) && entry.originalSize >= 0 ? entry.originalSize : 0;
70
- if (await fileExists(destination)) {
71
- if (originalSize === 0) await unlink(destination);
72
- else await truncate(destination, originalSize);
73
- }
74
- continue;
75
- }
76
- if (!entry.published && !entry.backupPending && !entry.backupCreated) continue;
77
- const destination = ensureWithin(target, relativePath);
78
- const backup = ensureWithin(target, `${root}/backup/${relativePath}`);
79
- if (entry.published && await fileExists(destination)) await unlink(destination);
80
- if (entry.hadPrevious && await fileExists(backup)) {
81
- await mkdir(path.dirname(destination), { recursive: true });
82
- await rename(backup, destination);
83
- }
84
- }
85
- }
86
-
87
- export async function recoverIncompleteTransactions(target) {
88
- const rootPath = ensureWithin(target, TRANSACTION_ROOT);
89
- if (!(await fileExists(rootPath))) return [];
90
- const recovered = [];
91
- for (const entry of await readdir(rootPath, { withFileTypes: true })) {
92
- if (!entry.isDirectory()) continue;
93
- const root = transactionPath(entry.name);
94
- const manifestPath = `${root}/manifest.json`;
95
- try {
96
- const manifest = JSON.parse(await readFile(ensureWithin(target, manifestPath), "utf8"));
97
- if (manifest.status !== "COMMITTING") continue;
98
- await withTaskLock(target, manifest.lockTaskId ?? manifest.taskId ?? "transaction-recovery", "recover-transaction", async () => {
99
- const current = JSON.parse(await readFile(ensureWithin(target, manifestPath), "utf8"));
100
- if (current.status !== "COMMITTING") return;
101
- await rollbackPublishedWrites(target, root, current);
102
- current.status = "ROLLED_BACK";
103
- current.recoveredAt = new Date().toISOString();
104
- await writeManifest(target, manifestPath, current);
105
- recovered.push({ transactionId: current.transactionId, status: current.status });
106
- });
107
- } catch (error) {
108
- recovered.push({ transactionId: entry.name, status: "RECOVERY_FAILED", error: error.message });
109
- }
110
- }
111
- return recovered;
112
- }
113
-
114
- export async function withTaskTransaction({
115
- target,
116
- taskId,
117
- lockTaskId = taskId,
118
- operation = "mutation",
119
- packageRoot,
120
- recordCommitEvent = false,
121
- } = {}, callback) {
35
+ export async function withTaskTransaction({ target, taskId, operation = "mutation", packageRoot, recordCommitEvent = false } = {}, callback) {
122
36
  if (!target || !taskId) throw new Error("target and taskId are required for a task transaction");
123
- target = await realpath(target);
124
- const activeTransaction = await getTaskTransaction(target);
125
- if (activeTransaction) {
126
- if (activeTransaction.taskId !== taskId) {
127
- throw new Error(`cannot nest task transaction for ${taskId} inside ${activeTransaction.taskId}`);
128
- }
129
- return callback(activeTransaction);
130
- }
131
- const started = Date.now();
132
- const runWithLock = async () => withTaskLock(target, lockTaskId, operation, async (lock) => {
133
- const transactionId = `txn-${randomUUID()}`;
134
- const root = transactionPath(transactionId);
135
- const stageRoot = `${root}/stage`;
136
- const manifestPath = `${root}/manifest.json`;
137
- await assertSafePath(target, manifestPath);
138
- await mkdir(ensureWithin(target, stageRoot), { recursive: true });
139
- const manifest = { schemaVersion: 1, transactionId, taskId, lockTaskId, operation, lockId: lock.lockId, startedAt: new Date().toISOString(), status: "STAGING", writes: [] };
140
- await writeManifest(target, manifestPath, manifest);
141
- const tx = {
142
- target,
143
- transactionId,
144
- taskId,
145
- operation,
146
- lock,
147
- async readText(relativePath) {
148
- await assertSafePath(target, relativePath);
149
- if (manifest.writes.some((entry) => writePath(entry) === relativePath && entry.kind === "DELETE")) {
150
- return null;
151
- }
152
- const staged = ensureWithin(target, `${stageRoot}/${relativePath}`);
153
- if (await fileExists(staged)) return readFile(staged, "utf8");
154
- const appendStaged = ensureWithin(target, `${stageRoot}/${relativePath}.append`);
155
- const appendText = await fileExists(appendStaged) ? await readFile(appendStaged, "utf8") : "";
156
- const destination = ensureWithin(target, relativePath);
157
- if (!(await fileExists(destination))) return appendText || null;
158
- return `${await readFile(destination, "utf8")}${appendText}`;
159
- },
160
- async stageText(relativePath, text) {
161
- await assertSafePath(target, relativePath);
162
- const conflicting = manifest.writes.find((entry) => writePath(entry) === relativePath);
163
- if (conflicting?.kind === "APPEND" || conflicting?.kind === "DELETE") {
164
- throw new Error(`cannot replace ${conflicting.kind.toLowerCase()}-staged path: ${relativePath}`);
165
- }
166
- const staged = `${stageRoot}/${relativePath}`;
167
- await assertSafePath(target, staged);
168
- await writeFileAtomic(ensureWithin(target, staged), text);
169
- if (!manifest.writes.some((entry) => writePath(entry) === relativePath)) {
170
- manifest.writes.push({ path: relativePath, hadPrevious: false, published: false });
171
- }
172
- await writeManifest(target, manifestPath, manifest);
173
- },
174
- async appendText(relativePath, text) {
175
- await assertSafePath(target, relativePath);
176
- if (typeof text !== "string" || text.length === 0) throw new Error("transaction append text must be a non-empty string");
177
- if (manifest.writes.some((entry) => writePath(entry) === relativePath && entry.kind !== "APPEND")) {
178
- throw new Error(`cannot append to replace-staged path: ${relativePath}`);
179
- }
180
- const destination = ensureWithin(target, relativePath);
181
- const staged = ensureWithin(target, `${stageRoot}/${relativePath}.append`);
182
- const existing = await fileExists(staged) ? await readFile(staged, "utf8") : "";
183
- await writeFileAtomic(staged, `${existing}${text}`);
184
- let entry = manifest.writes.find((candidate) => writePath(candidate) === relativePath);
185
- if (!entry) {
186
- entry = {
187
- path: relativePath,
188
- kind: "APPEND",
189
- originalSize: (await fileExists(destination)) ? (await stat(destination)).size : 0,
190
- appendStarted: false,
191
- published: false,
192
- };
193
- manifest.writes.push(entry);
194
- }
195
- await writeManifest(target, manifestPath, manifest);
196
- },
197
- async stageDelete(relativePath) {
198
- await assertSafePath(target, relativePath);
199
- if (manifest.writes.some((entry) => writePath(entry) === relativePath)) {
200
- throw new Error(`cannot delete write-staged path: ${relativePath}`);
201
- }
202
- const destination = ensureWithin(target, relativePath);
203
- if (!(await fileExists(destination))) {
204
- throw new Error(`transaction delete target is missing: ${relativePath}`);
205
- }
206
- manifest.writes.push({
207
- path: relativePath,
208
- kind: "DELETE",
209
- hadPrevious: false,
210
- published: false,
211
- });
212
- await writeManifest(target, manifestPath, manifest);
213
- },
214
- };
215
- try {
216
- const result = await transactionContext.run(tx, async () => {
217
- const callbackResult = await callback(tx);
218
- if (recordCommitEvent) {
219
- // Import lazily to keep the transaction/event dependency directional
220
- // at module initialization. The event is staged while this task's
221
- // transaction context is still active and is published last below.
222
- const { appendProtocolEvent } = await import("./events.js");
223
- await appendProtocolEvent(target, {
224
- taskId,
225
- event: "TRANSACTION_COMMITTED",
226
- details: { transactionId, operation },
227
- }, packageRoot, { taskId });
228
- }
229
- return callbackResult;
230
- });
231
- manifest.status = "COMMITTING";
232
- await writeManifest(target, manifestPath, manifest);
233
- // The ledger is the commit witness. Publishing it last means that a
234
- // visible TRANSACTION_COMMITTED event cannot precede a required staged
235
- // artifact in the same transaction.
236
- const writes = [...manifest.writes].sort((left, right) => {
237
- const leftIsLedger = writePath(left).endsWith("events.ndjson");
238
- const rightIsLedger = writePath(right).endsWith("events.ndjson");
239
- return Number(leftIsLedger) - Number(rightIsLedger);
240
- });
241
- for (const entry of writes) {
242
- const relativePath = writePath(entry);
243
- const staged = ensureWithin(target, `${stageRoot}/${relativePath}`);
244
- const destination = ensureWithin(target, relativePath);
245
- if (entry.kind === "APPEND") {
246
- const appendStaged = ensureWithin(target, `${stageRoot}/${relativePath}.append`);
247
- const appendText = await readFile(appendStaged, "utf8");
248
- await mkdir(path.dirname(destination), { recursive: true });
249
- entry.appendStarted = true;
250
- await writeManifest(target, manifestPath, manifest);
251
- const handle = await open(destination, "a", 0o644);
252
- try {
253
- await handle.writeFile(appendText);
254
- await handle.sync();
255
- } finally {
256
- await handle.close();
257
- }
258
- entry.published = true;
259
- await writeManifest(target, manifestPath, manifest);
260
- continue;
261
- }
262
- const backup = ensureWithin(target, `${root}/backup/${relativePath}`);
263
- await mkdir(path.dirname(destination), { recursive: true });
264
- entry.hadPrevious = await fileExists(destination);
265
- if (entry.hadPrevious) {
266
- entry.backupPending = true;
267
- await writeManifest(target, manifestPath, manifest);
268
- await mkdir(path.dirname(backup), { recursive: true });
269
- await rename(destination, backup);
270
- entry.backupCreated = true;
271
- }
272
- await writeManifest(target, manifestPath, manifest);
273
- if (entry.kind === "DELETE") {
274
- entry.published = true;
275
- await writeManifest(target, manifestPath, manifest);
276
- continue;
277
- }
278
- await rename(staged, destination);
279
- entry.published = true;
280
- await writeManifest(target, manifestPath, manifest);
281
- }
282
- manifest.status = "COMMITTED";
283
- manifest.committedAt = new Date().toISOString();
284
- await writeManifest(target, manifestPath, manifest);
285
- return result;
286
- } catch (error) {
287
- if (manifest.status === "COMMITTING") {
288
- try {
289
- await rollbackPublishedWrites(target, root, manifest);
290
- manifest.status = "ROLLED_BACK";
291
- manifest.recoveredAt = new Date().toISOString();
292
- } catch (rollbackError) {
293
- manifest.status = "ABANDONED";
294
- manifest.rollbackError = { message: rollbackError.message };
295
- }
296
- } else {
297
- // Staging has not published any writes; retain the diagnostic record
298
- // without requiring recovery of a project that was never modified.
299
- manifest.status = "ABORTED";
300
- }
301
- manifest.failedAt = new Date().toISOString();
302
- manifest.error = { message: error.message };
303
- await writeManifest(target, manifestPath, manifest);
304
- throw error;
305
- }
306
- });
307
- while (true) {
308
- try {
309
- return await runWithLock();
310
- } catch (error) {
311
- if (error.code !== E_TASK_LOCKED || Date.now() - started >= LOCK_WAIT_TIMEOUT_MS) throw error;
312
- await delay(20);
313
- }
37
+ if (!getOperationalStore(target)) {
38
+ const { withProjectStorage } = await import("../storage/project-boundary.js");
39
+ return withProjectStorage(target, () => withTaskTransaction({ target, taskId, operation, packageRoot, recordCommitEvent }, callback));
314
40
  }
41
+ const { withOperationalTransaction } = await import("../storage/unit-of-work.js");
42
+ return withOperationalTransaction({ target, taskId, operation, packageRoot, recordCommitEvent }, callback);
315
43
  }
@@ -1,3 +1,4 @@
1
+ import { operationalArtifactExists } from "../storage/operational-context.js";
1
2
  import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
2
3
  import { assertSchema, readSchema } from "./schema-validation.js";
3
4
  import { getPackageRoot } from "./templates.js";
@@ -5,6 +6,7 @@ import { currentRepositoryFingerprint } from "./repository.js";
5
6
  import { resolveRevisionProvider } from "./revision/provider.js";
6
7
  import { REVISION_PROVIDERS } from "./revision/registry.js";
7
8
  import { readScopedCheckerCapabilities } from "./verification-scope-capability.js";
9
+ import { withNativeReadScope } from "./native-storage.js";
8
10
  import { readContract } from "./contract.js";
9
11
  import { readPersistedRoute } from "./route-artifact.js";
10
12
  import { readWorkState } from "./work-state.js";
@@ -123,17 +125,19 @@ export async function validateVerificationScope(scope, packageRoot = getPackageR
123
125
  }
124
126
 
125
127
  export async function readVerificationScope(target, { taskId, packageRoot = getPackageRoot(), scopePath = null } = {}) {
126
- const relativePath = scopePath ?? taskVerificationScopePath(taskId);
127
- if (!(await fileExists(ensureWithin(target, relativePath)))) {
128
- throw scopeError("E_VERIFICATION_SCOPE_INVALID", `Verification scope is missing: ${relativePath}`, [relativePath]);
129
- }
130
- try {
131
- const artifact = await readJsonArtifact(target, relativePath, "verification-scope", packageRoot);
132
- return { ...artifact, value: await validateVerificationScope(artifact.value, packageRoot) };
133
- } catch (error) {
134
- if (error.code === "E_VERIFICATION_SCOPE_INVALID") throw error;
135
- throw scopeError("E_VERIFICATION_SCOPE_INVALID", error.message, [relativePath]);
136
- }
128
+ return withNativeReadScope(target, async () => {
129
+ const relativePath = scopePath ?? taskVerificationScopePath(taskId);
130
+ if (!(operationalArtifactExists(target, relativePath) ?? await fileExists(ensureWithin(target, relativePath)))) {
131
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", `Verification scope is missing: ${relativePath}`, [relativePath]);
132
+ }
133
+ try {
134
+ const artifact = await readJsonArtifact(target, relativePath, "verification-scope", packageRoot);
135
+ return { ...artifact, value: await validateVerificationScope(artifact.value, packageRoot) };
136
+ } catch (error) {
137
+ if (error.code === "E_VERIFICATION_SCOPE_INVALID") throw error;
138
+ throw scopeError("E_VERIFICATION_SCOPE_INVALID", error.message, [relativePath]);
139
+ }
140
+ });
137
141
  }
138
142
 
139
143
  export async function validateVerificationScopeFreshness(target, {
@@ -249,7 +253,11 @@ export async function resolveVerificationScope(target, {
249
253
  return { scope, inputs };
250
254
  }
251
255
 
252
- export async function persistVerificationScope(target, scope, { taskId, packageRoot = getPackageRoot() } = {}) {
256
+ export async function persistVerificationScope(target, scope, options = {}) {
257
+ return withTaskTransaction({ target, taskId: options.taskId, packageRoot: options.packageRoot, operation: "verify-scope" }, () => persistVerificationScopeInTransaction(target, scope, options));
258
+ }
259
+
260
+ async function persistVerificationScopeInTransaction(target, scope, { taskId, packageRoot = getPackageRoot() } = {}) {
253
261
  const relativePath = taskVerificationScopePath(taskId);
254
262
  const artifact = await writeJsonArtifact(target, relativePath, scope, "verification-scope", packageRoot, { taskId, operation: "verify-scope" });
255
263
  await appendProtocolEvent(target, {
@@ -1,5 +1,6 @@
1
+ import { withProjectReadSnapshot } from "../storage/project-read-snapshot.js";
2
+ import { withExistingProjectScope } from "../storage/existing-project-scope.js";
1
3
  import { createHash } from "node:crypto";
2
- import { unlink } from "node:fs/promises";
3
4
  import path from "node:path";
4
5
 
5
6
  import {
@@ -7,7 +8,6 @@ import {
7
8
  ensureWithin,
8
9
  fileExists,
9
10
  readBytes,
10
- writeFileAtomic,
11
11
  } from "./filesystem.js";
12
12
  import { GUIDE_IDS, PROTOCOL_VERSION, assertFailureClass, assertWorkPhase, isValidTransition } from "./protocol.js";
13
13
  import { assertSchema, readSchema } from "./schema-validation.js";
@@ -20,6 +20,8 @@ import { assertCoverageList } from "./coverage.js";
20
20
  import { canonicalFingerprint } from "./artifacts.js";
21
21
  import { taskArtifactPath } from "./task-paths.js";
22
22
  import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
23
+ import { readOperationalText, getOperationalStore } from "../storage/operational-context.js";
24
+ import { withNativeReadScope } from "./native-storage.js";
23
25
 
24
26
  export const WORK_STATE_PATH = ".forgeloop/work-state.json";
25
27
 
@@ -236,67 +238,83 @@ async function validateStoredState(state, packageRoot = getPackageRoot()) {
236
238
  }
237
239
 
238
240
  export async function readWorkState(target, options = {}) {
239
- const packageRoot = typeof options === "string" ? options : (options?.packageRoot ?? getPackageRoot());
240
- const relPath = typeof options === "object" && options !== null
241
- ? (options.statePath ?? options.relativePath ?? (options.taskId ? taskArtifactPath(options.taskId, "state") : (options.taskContext ? options.taskContext.paths.state : WORK_STATE_PATH)))
242
- : WORK_STATE_PATH;
243
-
244
- await assertSafePath(target, relPath);
245
- const statePath = ensureWithin(target, relPath);
246
- let state;
247
- try {
248
- const transaction = (await getTaskTransaction(target));
249
- const staged = transaction ? await transaction.readText(relPath) : null;
250
- if (staged === null) {
251
- if (!(await fileExists(statePath))) return null;
252
- const bytes = await readBytes(statePath);
253
- assertJsonBytes(bytes, relPath);
254
- state = JSON.parse(bytes.toString("utf8"));
255
- } else {
256
- const bytes = Buffer.from(staged, "utf8");
257
- assertJsonBytes(bytes, relPath);
258
- state = JSON.parse(staged);
241
+ return withNativeReadScope(target, async () => {
242
+ const packageRoot = typeof options === "string" ? options : (options?.packageRoot ?? getPackageRoot());
243
+ const relPath = typeof options === "object" && options !== null
244
+ ? (options.statePath ?? options.relativePath ?? (options.taskId ? taskArtifactPath(options.taskId, "state") : (options.taskContext ? options.taskContext.paths.state : WORK_STATE_PATH)))
245
+ : WORK_STATE_PATH;
246
+
247
+ const operational = readOperationalText(target, relPath);
248
+ if (operational.selected) {
249
+ if (operational.text === null) return null;
250
+ assertJsonBytes(operational.text, relPath);
251
+ return validateStoredState(JSON.parse(operational.text), packageRoot);
259
252
  }
260
- } catch (error) {
261
- throw new WorkStateError(`Unable to parse ${relPath}: ${error.message}`);
262
- }
263
- try {
264
- return await validateStoredState(state, packageRoot);
265
- } catch (error) {
266
- if (error instanceof WorkStateError) throw error;
267
- throw new WorkStateError(error.message);
268
- }
253
+ await assertSafePath(target, relPath);
254
+ const statePath = ensureWithin(target, relPath);
255
+ let state;
256
+ try {
257
+ const transaction = (await getTaskTransaction(target));
258
+ const staged = transaction ? await transaction.readText(relPath) : null;
259
+ if (staged === null) {
260
+ if (!(await fileExists(statePath))) return null;
261
+ const bytes = await readBytes(statePath);
262
+ assertJsonBytes(bytes, relPath);
263
+ state = JSON.parse(bytes.toString("utf8"));
264
+ } else {
265
+ const bytes = Buffer.from(staged, "utf8");
266
+ assertJsonBytes(bytes, relPath);
267
+ state = JSON.parse(staged);
268
+ }
269
+ } catch (error) {
270
+ throw new WorkStateError(`Unable to parse ${relPath}: ${error.message}`);
271
+ }
272
+ try {
273
+ return await validateStoredState(state, packageRoot);
274
+ } catch (error) {
275
+ if (error instanceof WorkStateError) throw error;
276
+ throw new WorkStateError(error.message);
277
+ }
278
+ });
269
279
  }
270
280
 
271
281
  export async function readContractFingerprint(target, contractFile) {
272
- await assertSafePath(target, contractFile);
273
- const contractPath = ensureWithin(target, contractFile);
274
- let contract;
275
- try {
276
- const bytes = await readBytes(contractPath);
277
- assertJsonBytes(bytes, contractFile);
278
- contract = JSON.parse(bytes.toString("utf8"));
279
- } catch (error) {
280
- throw new WorkStateError(`Unable to parse contract ${contractFile}: ${error.message}`);
281
- }
282
- return { path: contractFile, fingerprint: contractFingerprint(contract) };
282
+ return withNativeReadScope(target, async () => {
283
+ const operational = readOperationalText(target, contractFile);
284
+ if (operational.selected) {
285
+ if (operational.text === null) throw new WorkStateError(`Unable to parse contract ${contractFile}: artifact missing`);
286
+ assertJsonBytes(operational.text, contractFile);
287
+ return { path: contractFile, fingerprint: contractFingerprint(JSON.parse(operational.text)) };
288
+ }
289
+ await assertSafePath(target, contractFile);
290
+ const contractPath = ensureWithin(target, contractFile);
291
+ let contract;
292
+ try {
293
+ const bytes = await readBytes(contractPath);
294
+ assertJsonBytes(bytes, contractFile);
295
+ contract = JSON.parse(bytes.toString("utf8"));
296
+ } catch (error) {
297
+ throw new WorkStateError(`Unable to parse contract ${contractFile}: ${error.message}`);
298
+ }
299
+ return { path: contractFile, fingerprint: contractFingerprint(contract) };
300
+ });
283
301
  }
284
302
 
285
303
  export async function writeWorkState(target, state, options = {}) {
286
304
  const packageRoot = options?.packageRoot ?? getPackageRoot();
287
305
  const dryRun = options?.dryRun ?? false;
288
- const relPath = options?.statePath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "state") : WORK_STATE_PATH);
289
-
290
306
  await validateStoredState(state, packageRoot);
291
- await assertSafePath(target, relPath);
292
- const serialized = `${JSON.stringify(state, null, 2)}\n`;
293
- const transaction = (await getTaskTransaction(target));
294
- if (!dryRun && transaction) {
295
- await transaction.stageText(relPath, serialized);
296
- } else {
297
- const statePath = ensureWithin(target, relPath);
298
- await writeFileAtomic(statePath, serialized, { dryRun });
307
+ const canonicalPath = taskArtifactPath(state.taskId, "state");
308
+ const relativePath = options?.statePath ?? options?.relativePath ?? canonicalPath;
309
+ if ((options?.taskId && options.taskId !== state.taskId) || relativePath !== canonicalPath) {
310
+ throw Object.assign(new Error("Work-state writes require their canonical task-scoped SQLite identity; use explicit export for portable files"), { code: "E_STORAGE_OPERATION_UNSUPPORTED", artifacts: [relativePath] });
311
+ }
312
+ if (dryRun) return state;
313
+ if (!getOperationalStore(target)) {
314
+ const { withProjectStorage } = await import("../storage/project-boundary.js");
315
+ return withProjectStorage(target, () => writeWorkState(target, state, options));
299
316
  }
317
+ await withTaskTransaction({ target, taskId: state.taskId, operation: "write-state", packageRoot }, tx => tx.stageText(canonicalPath, `${JSON.stringify(state, null, 2)}\n`));
300
318
  return state;
301
319
  }
302
320
 
@@ -355,18 +373,34 @@ export async function initializeWorkState(target, state, { packageRoot = getPack
355
373
  }
356
374
 
357
375
  export async function clearWorkState(target, options = {}) {
358
- const relPath = options?.statePath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "state") : WORK_STATE_PATH);
359
- await assertSafePath(target, relPath);
360
- const statePath = ensureWithin(target, relPath);
361
- if (!(await fileExists(statePath))) return { removed: false, path: relPath };
362
- await unlink(statePath);
363
- return { removed: true, path: relPath };
376
+ if (!options.taskId) throw Object.assign(new Error("Canonical artifact clearing requires a task identity"), { code: "E_STORAGE_OPERATION_UNSUPPORTED" });
377
+ const relPath = taskArtifactPath(options.taskId, "state");
378
+ const requested = options.statePath ?? options.relativePath ?? relPath;
379
+ if (requested !== relPath) throw Object.assign(new Error("Artifact clearing requires the canonical task path; portable exports are independent files"), { code: "E_STORAGE_OPERATION_UNSUPPORTED", artifacts: [requested] });
380
+ if (!getOperationalStore(target)) {
381
+ return withExistingProjectScope(target, () => clearWorkState(target, options));
382
+ }
383
+ const operational = readOperationalText(target, relPath);
384
+ if (operational.text === null) return { removed: false, path: relPath };
385
+ return withTaskTransaction({ target, taskId: options.taskId, operation: "clear-work-state", packageRoot: options.packageRoot }, async transaction => {
386
+ transaction.stageDelete(relPath);
387
+ return { removed: true, path: relPath };
388
+ });
364
389
  }
365
390
 
366
391
  export async function readRequiredArtifactFingerprints(target, artifacts) {
392
+ return withProjectReadSnapshot(target, () => readSelectedRequiredArtifactFingerprints(target, artifacts));
393
+ }
394
+
395
+ async function readSelectedRequiredArtifactFingerprints(target, artifacts) {
367
396
  const normalized = normalizeRequiredArtifacts(artifacts);
368
397
  const current = [];
369
398
  for (const artifact of normalized) {
399
+ const operational = readOperationalText(target, artifact.path);
400
+ if (operational.selected) {
401
+ current.push({ path: artifact.path, sha256: operational.text === null ? null : createHash("sha256").update(operational.text).digest("hex"), status: operational.text === null ? "missing" : "present" });
402
+ continue;
403
+ }
370
404
  await assertSafePath(target, artifact.path);
371
405
  const artifactPath = ensureWithin(target, artifact.path);
372
406
  if (!(await fileExists(artifactPath))) {
@@ -485,6 +519,7 @@ export async function classifyLoadedWorkState({ target, state, contractFile = nu
485
519
  try {
486
520
  contract = await readContractFingerprint(target, contractFile);
487
521
  } catch (error) {
522
+ if (error?.code === "E_STATE_REVISION_CONFLICT") throw error;
488
523
  contractError = error.message;
489
524
  }
490
525
  }
@@ -495,6 +530,7 @@ export async function classifyLoadedWorkState({ target, state, contractFile = nu
495
530
  try {
496
531
  currentArtifacts = await readRequiredArtifactFingerprints(target, state.requiredArtifacts);
497
532
  } catch (error) {
533
+ if (error?.code === "E_STATE_REVISION_CONFLICT") throw error;
498
534
  artifactError = error.message;
499
535
  }
500
536
  } else {
@@ -547,7 +583,11 @@ export async function classifyLoadedWorkState({ target, state, contractFile = nu
547
583
  };
548
584
  }
549
585
 
550
- export async function readAndClassifyWorkState({ target, packageRoot = getPackageRoot(), contractFile = null, maxAgeMs, taskId = null, stateFile = null, statePath = null } = {}) {
586
+ export async function readAndClassifyWorkState(options = {}) {
587
+ return withProjectReadSnapshot(options.target, () => readAndClassifySelectedWorkState(options));
588
+ }
589
+
590
+ async function readAndClassifySelectedWorkState({ target, packageRoot = getPackageRoot(), contractFile = null, maxAgeMs, taskId = null, stateFile = null, statePath = null } = {}) {
551
591
  const effectiveStateRel = stateFile ?? statePath ?? (taskId ? taskArtifactPath(taskId, "state") : WORK_STATE_PATH);
552
592
  let state = null;
553
593
  try {
@@ -557,6 +597,7 @@ export async function readAndClassifyWorkState({ target, packageRoot = getPackag
557
597
  ...(stateFile || statePath ? { statePath: effectiveStateRel } : {}),
558
598
  });
559
599
  } catch (error) {
600
+ if (error?.code === "E_STATE_REVISION_CONFLICT") throw error;
560
601
  return {
561
602
  path: effectiveStateRel,
562
603
  present: true,