@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,16 +1,11 @@
1
- import { cp, mkdir, rename, rm } from "node:fs/promises";
2
- import path from "node:path";
3
- import { ensureWithin, fileExists, writeFileAtomic } from "./filesystem.js";
1
+ import { ensureWithin, fileExists } from "./filesystem.js";
4
2
  import { getPackageRoot } from "./templates.js";
5
3
  import {
6
4
  LEGACY_TASK_ARTIFACT_PATHS,
7
- TASK_STATE_ROOT,
8
- taskArtifactPath,
9
5
  taskDirectory,
10
6
  } from "./task-paths.js";
11
7
  import { assertTaskId, taskStorageKey } from "./task-identity.js";
12
- import { createTaskDescriptor, readTaskDescriptor, writeTaskDescriptor } from "./task-descriptor.js";
13
- import { readJsonArtifact } from "./artifacts.js";
8
+ import { readPortableJsonArtifact } from "./artifacts.js";
14
9
  import { validateMigrationSnapshot } from "./task-migration-validation.js";
15
10
  import {
16
11
  E_TASK_MIGRATION_IDENTITY_MISMATCH,
@@ -31,51 +26,11 @@ export async function detectLegacySingletonLayout(target) {
31
26
  };
32
27
  }
33
28
 
34
- async function removeLegacyArtifact(target, relativePath) {
35
- const fullPath = ensureWithin(target, relativePath);
36
- await rm(fullPath, { recursive: true, force: true });
37
- if (await fileExists(fullPath)) {
38
- const error = new Error(
39
- `Legacy artifact still exists after migration cleanup: ${relativePath}`,
40
- );
41
- error.code = E_TASK_MIGRATION_INVALID;
42
- throw error;
43
- }
44
- }
45
-
46
- function assertMigrationSnapshotsEqual(source, destination, label) {
47
- for (const [key, hash] of Object.entries(source.artifactFingerprints)) {
48
- if (destination.artifactFingerprints[key] !== hash) {
49
- const error = new Error(`Migration fingerprint mismatch in ${label}: ${key}`);
50
- error.code = E_TASK_MIGRATION_INVALID;
51
- throw error;
52
- }
53
- }
54
-
55
- for (const [key, hash] of Object.entries(source.directoryFingerprints)) {
56
- if (destination.directoryFingerprints[key] !== hash) {
57
- const error = new Error(`Migration directory fingerprint mismatch in ${label}: ${key}`);
58
- error.code = E_TASK_MIGRATION_INVALID;
59
- throw error;
60
- }
61
- }
62
-
63
- if (destination.eventCount !== source.eventCount) {
64
- const error = new Error(`Migration event count mismatch in ${label}`);
65
- error.code = E_TASK_MIGRATION_INVALID;
66
- throw error;
67
- }
68
- }
69
-
70
29
  export async function migrateLegacyLayout(
71
30
  target,
72
31
  {
73
32
  dryRun = false,
74
33
  packageRoot = getPackageRoot(),
75
- afterCopyForTest = null,
76
- afterPublishForTest = null,
77
- beforeLegacyCleanupForTest = null,
78
- removeLegacyArtifactForTest = null,
79
34
  } = {},
80
35
  ) {
81
36
  const detection = await detectLegacySingletonLayout(target);
@@ -90,60 +45,21 @@ export async function migrateLegacyLayout(
90
45
  let canonicalTaskId = null;
91
46
  const artifactIdentities = [];
92
47
 
93
- // Determine candidate canonicalTaskId from known legacy artifacts
48
+ const identitySchemas = { contract: "current-contract", state: "work-state", continuity: "continuity", receipt: "execution-receipt" };
94
49
  for (const item of detection.legacyFiles) {
95
- if (item.key === "contract") {
96
- try {
97
- const contract = await readJsonArtifact(target, item.path, "current-contract", packageRoot);
98
- if (contract.value?.taskId) {
99
- if (!canonicalTaskId) canonicalTaskId = contract.value.taskId;
100
- artifactIdentities.push({ artifact: "contract", taskId: contract.value.taskId });
101
- }
102
- } catch (err) {
103
- const error = new Error(`Legacy contract artifact is invalid: ${err.message}`);
104
- error.code = E_TASK_MIGRATION_INVALID;
105
- error.cause = err;
106
- throw error;
107
- }
108
- } else if (item.key === "state") {
109
- try {
110
- const state = await readJsonArtifact(target, item.path, "work-state", packageRoot);
111
- if (state.value?.taskId) {
112
- if (!canonicalTaskId) canonicalTaskId = state.value.taskId;
113
- artifactIdentities.push({ artifact: "state", taskId: state.value.taskId });
114
- }
115
- } catch (err) {
116
- const error = new Error(`Legacy state artifact is invalid: ${err.message}`);
117
- error.code = E_TASK_MIGRATION_INVALID;
118
- error.cause = err;
119
- throw error;
120
- }
121
- } else if (item.key === "continuity") {
122
- try {
123
- const continuity = await readJsonArtifact(target, item.path, "continuity", packageRoot);
124
- if (continuity.value?.taskId) {
125
- if (!canonicalTaskId) canonicalTaskId = continuity.value.taskId;
126
- artifactIdentities.push({ artifact: "continuity", taskId: continuity.value.taskId });
127
- }
128
- } catch (err) {
129
- const error = new Error(`Legacy continuity artifact is invalid: ${err.message}`);
130
- error.code = E_TASK_MIGRATION_INVALID;
131
- error.cause = err;
132
- throw error;
133
- }
134
- } else if (item.key === "receipt") {
135
- try {
136
- const receipt = await readJsonArtifact(target, item.path, "execution-receipt", packageRoot);
137
- if (receipt.value?.taskId) {
138
- if (!canonicalTaskId) canonicalTaskId = receipt.value.taskId;
139
- artifactIdentities.push({ artifact: "receipt", taskId: receipt.value.taskId });
140
- }
141
- } catch (err) {
142
- const error = new Error(`Legacy receipt artifact is invalid: ${err.message}`);
143
- error.code = E_TASK_MIGRATION_INVALID;
144
- error.cause = err;
145
- throw error;
50
+ const schema = identitySchemas[item.key];
51
+ if (!schema) continue;
52
+ try {
53
+ const artifact = await readPortableJsonArtifact(target, item.path, schema, packageRoot);
54
+ if (artifact.value?.taskId) {
55
+ canonicalTaskId ??= artifact.value.taskId;
56
+ artifactIdentities.push({ artifact: item.key, taskId: artifact.value.taskId });
146
57
  }
58
+ } catch (err) {
59
+ const error = new Error(`Legacy ${item.key} artifact is invalid: ${err.message}`);
60
+ error.code = E_TASK_MIGRATION_INVALID;
61
+ error.cause = err;
62
+ throw error;
147
63
  }
148
64
  }
149
65
 
@@ -167,7 +83,7 @@ export async function migrateLegacyLayout(
167
83
  }
168
84
 
169
85
  // 1. Validate complete legacy source snapshot fail-closed before creating any directories
170
- const sourceSnapshot = await validateMigrationSnapshot(target, {
86
+ await validateMigrationSnapshot(target, {
171
87
  taskId: canonicalTaskId,
172
88
  packageRoot,
173
89
  paths: LEGACY_TASK_ARTIFACT_PATHS,
@@ -176,8 +92,6 @@ export async function migrateLegacyLayout(
176
92
  const taskKey = taskStorageKey(canonicalTaskId);
177
93
  const finalDirRel = taskDirectory(canonicalTaskId);
178
94
  const finalDirAbs = ensureWithin(target, finalDirRel);
179
- const tempDirRel = `${TASK_STATE_ROOT}/.tmp-${taskKey}`;
180
- const tempDirAbs = ensureWithin(target, tempDirRel);
181
95
 
182
96
  if (await fileExists(finalDirAbs)) {
183
97
  const error = new Error(`Target task directory already exists: ${finalDirRel}`);
@@ -185,8 +99,6 @@ export async function migrateLegacyLayout(
185
99
  throw error;
186
100
  }
187
101
 
188
- const migratedArtifacts = [];
189
-
190
102
  if (dryRun) {
191
103
  return {
192
104
  migrated: false,
@@ -198,187 +110,5 @@ export async function migrateLegacyLayout(
198
110
  };
199
111
  }
200
112
 
201
- let published = false;
202
- let cleanupStarted = false;
203
-
204
- // 2. Create temporary migration directory
205
- await mkdir(tempDirAbs, { recursive: true });
206
-
207
- try {
208
- // 3. Create and write task.json descriptor
209
- const descriptor = createTaskDescriptor({
210
- taskId: canonicalTaskId,
211
- writeClaims: [],
212
- });
213
- await writeTaskDescriptor(target, descriptor, packageRoot, {
214
- relativePathOverride: `${tempDirRel}/task.json`,
215
- });
216
- migratedArtifacts.push("task.json");
217
-
218
- // 4. Copy and map files into temp namespace
219
- const fileMapping = [
220
- [LEGACY_TASK_ARTIFACT_PATHS.contract, "contract.json"],
221
- [LEGACY_TASK_ARTIFACT_PATHS.route, "routing-result.json"],
222
- [LEGACY_TASK_ARTIFACT_PATHS.preflight, "preflight.json"],
223
- [LEGACY_TASK_ARTIFACT_PATHS.state, "work-state.json"],
224
- [LEGACY_TASK_ARTIFACT_PATHS.continuity, "continuity.json"],
225
- [LEGACY_TASK_ARTIFACT_PATHS.receipt, "execution-receipt.json"],
226
- [LEGACY_TASK_ARTIFACT_PATHS.events, "events.ndjson"],
227
- ];
228
-
229
- for (const [legacyRel, destName] of fileMapping) {
230
- const srcAbs = ensureWithin(target, legacyRel);
231
- if (await fileExists(srcAbs)) {
232
- const destAbs = path.join(tempDirAbs, destName);
233
- await cp(srcAbs, destAbs);
234
- migratedArtifacts.push(destName);
235
- }
236
- }
237
-
238
- // 5. Copy directories: gates/ and executions/
239
- const dirMapping = [
240
- [LEGACY_TASK_ARTIFACT_PATHS.gates, "gates"],
241
- [LEGACY_TASK_ARTIFACT_PATHS.executions, "executions"],
242
- ];
243
-
244
- for (const [legacyRel, destDirName] of dirMapping) {
245
- const srcAbs = ensureWithin(target, legacyRel);
246
- if (await fileExists(srcAbs)) {
247
- const destAbs = path.join(tempDirAbs, destDirName);
248
- await cp(srcAbs, destAbs, { recursive: true });
249
- migratedArtifacts.push(`${destDirName}/`);
250
- }
251
- }
252
-
253
- // Test hook for corruption / failure testing
254
- if (typeof afterCopyForTest === "function") {
255
- await afterCopyForTest({ tempDirAbs, tempDirRel, target });
256
- }
257
-
258
- // 6. Validate temp snapshot
259
- const tempPaths = {
260
- contract: `${tempDirRel}/contract.json`,
261
- route: `${tempDirRel}/routing-result.json`,
262
- preflight: `${tempDirRel}/preflight.json`,
263
- state: `${tempDirRel}/work-state.json`,
264
- continuity: `${tempDirRel}/continuity.json`,
265
- receipt: `${tempDirRel}/execution-receipt.json`,
266
- events: `${tempDirRel}/events.ndjson`,
267
- gates: `${tempDirRel}/gates`,
268
- executions: `${tempDirRel}/executions`,
269
- };
270
-
271
- const tempSnapshot = await validateMigrationSnapshot(target, {
272
- taskId: canonicalTaskId,
273
- packageRoot,
274
- paths: tempPaths,
275
- });
276
-
277
- // 7. Compare copied fingerprints
278
- assertMigrationSnapshotsEqual(sourceSnapshot, tempSnapshot, "temporary namespace");
279
-
280
- // 8. Atomically publish
281
- await mkdir(ensureWithin(target, TASK_STATE_ROOT), { recursive: true });
282
- await rename(tempDirAbs, finalDirAbs);
283
- published = true;
284
-
285
- // Test hook for corruption / failure testing after publication
286
- if (typeof afterPublishForTest === "function") {
287
- await afterPublishForTest({ finalDirAbs, finalDirRel, target });
288
- }
289
-
290
- // 9. Post-publish validation: verify final namespace snapshot and task descriptor
291
- const finalPaths = {
292
- contract: taskArtifactPath(canonicalTaskId, "contract"),
293
- route: taskArtifactPath(canonicalTaskId, "route"),
294
- preflight: taskArtifactPath(canonicalTaskId, "preflight"),
295
- state: taskArtifactPath(canonicalTaskId, "state"),
296
- continuity: taskArtifactPath(canonicalTaskId, "continuity"),
297
- receipt: taskArtifactPath(canonicalTaskId, "receipt"),
298
- events: taskArtifactPath(canonicalTaskId, "events"),
299
- gates: taskArtifactPath(canonicalTaskId, "gates"),
300
- executions: taskArtifactPath(canonicalTaskId, "executions"),
301
- };
302
-
303
- const finalSnapshot = await validateMigrationSnapshot(target, {
304
- taskId: canonicalTaskId,
305
- packageRoot,
306
- paths: finalPaths,
307
- });
308
- assertMigrationSnapshotsEqual(sourceSnapshot, finalSnapshot, "published namespace");
309
- await readTaskDescriptor(target, canonicalTaskId, packageRoot);
310
-
311
- const migrationReceipt = {
312
- schemaVersion: 1,
313
- protocolVersion: 1,
314
- kind: "LEGACY_TASK_MIGRATION",
315
- taskId: canonicalTaskId,
316
- taskKey,
317
- migratedAt: new Date().toISOString(),
318
- source: sourceSnapshot,
319
- destination: finalSnapshot,
320
- cleanupStatus: "PENDING",
321
- };
322
- await writeFileAtomic(
323
- ensureWithin(target, `${finalDirRel}/migration-receipt.json`),
324
- `${JSON.stringify(migrationReceipt, null, 2)}\n`,
325
- );
326
- migratedArtifacts.push("migration-receipt.json");
327
-
328
- // Test hook for cleanup failure testing
329
- if (typeof beforeLegacyCleanupForTest === "function") {
330
- await beforeLegacyCleanupForTest({ finalDirAbs, finalDirRel, target });
331
- }
332
-
333
- // 10. Cleanup legacy task files (strictly after successful publish and validation)
334
- cleanupStarted = true;
335
- try {
336
- for (const item of detection.legacyFiles) {
337
- if (typeof removeLegacyArtifactForTest === "function") {
338
- await removeLegacyArtifactForTest(target, item.path);
339
- } else {
340
- await removeLegacyArtifact(target, item.path);
341
- }
342
- }
343
- migrationReceipt.cleanupStatus = "COMPLETED";
344
- migrationReceipt.cleanedAt = new Date().toISOString();
345
- await writeFileAtomic(
346
- ensureWithin(target, `${finalDirRel}/migration-receipt.json`),
347
- `${JSON.stringify(migrationReceipt, null, 2)}\n`,
348
- );
349
- } catch (cleanupErr) {
350
- const error = new Error(
351
- `Migration published successfully to ${finalDirRel}, but legacy cleanup failed: ${cleanupErr.message}. Manual cleanup required.`,
352
- );
353
- error.code = E_TASK_MIGRATION_INVALID;
354
- error.cause = cleanupErr;
355
- throw error;
356
- }
357
-
358
- return {
359
- migrated: true,
360
- taskId: canonicalTaskId,
361
- taskKey,
362
- targetDirectory: finalDirRel,
363
- migratedArtifacts,
364
- };
365
- } catch (error) {
366
- if (published && !cleanupStarted && (await fileExists(finalDirAbs))) {
367
- try {
368
- await rm(finalDirAbs, { recursive: true, force: true });
369
- } catch (rollbackErr) {
370
- error.rollbackError = rollbackErr;
371
- }
372
- }
373
-
374
- // Cleanup temp dir on failure if it still exists
375
- if (await fileExists(tempDirAbs)) {
376
- try {
377
- await rm(tempDirAbs, { recursive: true, force: true });
378
- } catch (tempErr) {
379
- error.tempCleanupError = tempErr;
380
- }
381
- }
382
- throw error;
383
- }
113
+ throw Object.assign(new Error("Filesystem task migration apply is retired; use explicit SQLite task migration with destination and excluded writers"), { code: "E_STORAGE_OPERATION_UNSUPPORTED" });
384
114
  }
@@ -1,16 +1,21 @@
1
- import { readEvents, validateEventLedger } from "./events.js";
1
+ import { taskArtifactPath } from "./task-paths.js";
2
+ import { createHash } from "node:crypto";
3
+ import { getOperationalStore } from "../storage/operational-context.js";
4
+ import { needsExistingProjectScope, withExistingProjectScope } from "../storage/existing-project-scope.js";
5
+ import { iterateEvents, validateEventLedger, withEventLedgerAudit } from "./events.js";
2
6
  import { readWorkState } from "./work-state.js";
3
7
  import { canonicalFingerprint } from "./artifacts.js";
4
8
 
5
- function ledgerTail(events) {
6
- const last = events.at(-1) ?? null;
9
+ async function ledgerTail(target, packageRoot, options) {
10
+ let last = null;
11
+ for await (const event of iterateEvents(target, packageRoot, options)) last = event;
7
12
  return {
8
13
  sequence: last?.seq ?? 0,
9
14
  hash: last?.hash ?? null,
10
15
  };
11
16
  }
12
17
 
13
- export async function buildTaskSnapshot({
18
+ async function buildPortableTaskSnapshot({
14
19
  target,
15
20
  packageRoot,
16
21
  taskId = null,
@@ -19,19 +24,17 @@ export async function buildTaskSnapshot({
19
24
  } = {}) {
20
25
  const options = { packageRoot, taskId, ...(eventsPath ? { eventsPath } : {}) };
21
26
  const state = await readWorkState(target, { ...options, ...(stateFile ? { statePath: stateFile } : {}) });
22
- const events = await readEvents(target, packageRoot, options);
23
27
  const before = {
24
28
  stateRevision: state?.revision ?? null,
25
- ...ledgerTail(events),
29
+ ...await ledgerTail(target, packageRoot, options),
26
30
  };
27
31
 
28
32
  const validation = await validateEventLedger(target, packageRoot, options);
29
33
 
30
34
  const rereadState = await readWorkState(target, { ...options, ...(stateFile ? { statePath: stateFile } : {}) });
31
- const rereadEvents = await readEvents(target, packageRoot, options);
32
35
  const after = {
33
36
  stateRevision: rereadState?.revision ?? null,
34
- ...ledgerTail(rereadEvents),
37
+ ...await ledgerTail(target, packageRoot, options),
35
38
  };
36
39
 
37
40
  const consistent = JSON.stringify(before) === JSON.stringify(after);
@@ -51,3 +54,43 @@ export async function buildTaskSnapshot({
51
54
  events: validation.events,
52
55
  };
53
56
  }
57
+
58
+
59
+ function eventSummaryFingerprint(events) {
60
+ const digest = createHash("sha256");
61
+ digest.update("[");
62
+ let first = true;
63
+ for (const { seq, event, at } of events) {
64
+ if (!first) digest.update(",");
65
+ first = false;
66
+ // Same sorted keys/undefined omission as canonicalFingerprint({seq,event,at}).
67
+ digest.update(JSON.stringify({ at, event, seq }));
68
+ }
69
+ return digest.update("]").digest("hex");
70
+ }
71
+
72
+ /** Internal callback keeps event source owned until the projection finishes. */
73
+ export async function withTaskSnapshot(options, callback) {
74
+ const { target, packageRoot, taskId = null, eventsPath = null, stateFile = null } = options;
75
+ if (await needsExistingProjectScope(target)) {
76
+ return withExistingProjectScope(target, () => withTaskSnapshot(options, callback), { readOnly: true });
77
+ }
78
+ if (!getOperationalStore(target) || (stateFile && (!taskId || stateFile !== taskArtifactPath(taskId, "state")))) {
79
+ return callback(await buildPortableTaskSnapshot(options));
80
+ }
81
+ return withEventLedgerAudit(target, packageRoot, { taskId, ...(eventsPath ? { eventsPath } : {}) }, async validation => {
82
+ if (Array.isArray(validation.events)) return callback(await buildPortableTaskSnapshot(options));
83
+ const state = await readWorkState(target, { packageRoot, taskId, ...(stateFile ? { statePath: stateFile } : {}) });
84
+ const tail = validation.events.at(-1);
85
+ return callback({ consistent: true, taskId: taskId ?? state?.taskId ?? null,
86
+ anchors: { stateRevision: state?.revision ?? null, sequence: tail?.seq ?? 0, hash: tail?.hash ?? null },
87
+ capturedAt: new Date().toISOString(),
88
+ integrity: { valid: validation.valid, errors: validation.errors, eventCount: validation.events.length,
89
+ fingerprint: eventSummaryFingerprint(validation.events) },
90
+ state, events: validation.events });
91
+ });
92
+ }
93
+
94
+ export async function buildTaskSnapshot(options = {}) {
95
+ return withTaskSnapshot(options, snapshot => ({ ...snapshot, events: Array.isArray(snapshot.events) ? snapshot.events : [...snapshot.events] }));
96
+ }
@@ -0,0 +1,18 @@
1
+ import { detectLegacySingletonLayout, migrateLegacyLayout } from "./task-migration.js";
2
+ import { migrateProjectStorage } from "../storage/migration.js";
3
+
4
+ /** Explicit legacy conversion now publishes canonical SQLite storage. */
5
+ export async function migrateLegacyTaskStorage(target, { packageRoot, dryRun = false, destination, writersQuiesced = false } = {}) {
6
+ const detected = await detectLegacySingletonLayout(target);
7
+ if (!detected.hasLegacy) return migrateLegacyLayout(target, { packageRoot, dryRun });
8
+ if (dryRun) return { ...await migrateLegacyLayout(target, { packageRoot, dryRun }), targetDirectory: ".forgeloop/state.sqlite" };
9
+ if (typeof destination !== "string" || !destination || writersQuiesced !== true) throw Object.assign(new Error("Legacy task migration requires --destination and explicit --writers-quiesced for SQLite cutover"), { code: "E_CLI_INVOCATION_INVALID" });
10
+ const planned = await migrateLegacyLayout(target, { packageRoot, dryRun: true });
11
+ const storage = await migrateProjectStorage(target, { destination, writersQuiesced, packageRoot });
12
+ return {
13
+ migrated: true, taskId: planned.taskId, taskKey: planned.taskKey,
14
+ targetDirectory: ".forgeloop/state.sqlite", storage,
15
+ migrationReceipt: `${destination}/publication/publication-journal.json`,
16
+ migratedArtifacts: detected.legacyFiles.map(item => item.path),
17
+ };
18
+ }
@@ -12,8 +12,17 @@ import { GUIDE_TEMPLATE_PATHS } from "./guide-registry.js";
12
12
 
13
13
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
14
14
 
15
- const TEMPLATE_SOURCE_PATHS = Object.freeze({
16
- ".forgeloop/.gitignore": ".forgeloop/forgeloop.gitignore",
15
+ const INLINE_TEMPLATE_CONTENT = Object.freeze({
16
+ ".forgeloop/.gitignore": [
17
+ "# Local resumable task state is untrusted, target-specific data.",
18
+ "work-state.json",
19
+ "state.sqlite",
20
+ "state.sqlite-*",
21
+ "storage-version.json",
22
+ "executions/",
23
+ "repository-index/",
24
+ "",
25
+ ].join("\n"),
17
26
  });
18
27
 
19
28
  export const TEMPLATE_PATHS = [
@@ -36,8 +45,6 @@ export const TEMPLATE_PATHS = [
36
45
  "PROTOCOL_INTEGRATION.md",
37
46
  "AGENT_COMPATIBILITY.md",
38
47
  "THIRD_PARTY_NOTICES.md",
39
- "LICENSE",
40
- "LICENSE-DOCS.md",
41
48
  ...GUIDE_TEMPLATE_PATHS,
42
49
  "schemas/routing-input.schema.json",
43
50
  "schemas/routing-result.schema.json",
@@ -100,14 +107,19 @@ export function getPackageRoot() {
100
107
  export async function readTemplateEntries(packageRoot = PACKAGE_ROOT) {
101
108
  return Promise.all(
102
109
  TEMPLATE_PATHS.map(async (relativePath) => {
103
- const sourcePath = TEMPLATE_SOURCE_PATHS[relativePath] ?? relativePath;
110
+ const sourcePath = relativePath;
111
+ const inlineContent = INLINE_TEMPLATE_CONTENT[relativePath];
104
112
  return {
105
113
  relativePath: targetPathForSource(relativePath),
106
114
  sourcePath,
107
115
  legacyRelativePath: legacyPathForSource(sourcePath),
108
- bytes: Buffer.from(isNativeAdapterPath(relativePath)
109
- ? nativeShim(relativePath)
110
- : await readFile(path.join(packageRoot, sourcePath))),
116
+ bytes: Buffer.from(
117
+ inlineContent !== undefined
118
+ ? inlineContent
119
+ : isNativeAdapterPath(relativePath)
120
+ ? nativeShim(relativePath)
121
+ : await readFile(path.join(packageRoot, sourcePath)),
122
+ ),
111
123
  };
112
124
  }),
113
125
  );
@@ -1,4 +1,5 @@
1
1
  import { readTestUtility } from "./service.js";
2
+ import { assertSafePath } from "../filesystem.js";
2
3
  import { cp, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
@@ -38,6 +39,9 @@ export async function runPruneProbe({ target, packageRoot, taskId, testId } = {}
38
39
  }
39
40
  const temporaryRoot = await mkdtemp(path.join(os.tmpdir(), "forgeloop-prune-probe-"));
40
41
  try {
42
+ // Persisted utility records do not grant access outside the project or
43
+ // through symlinks copied into the disposable workspace.
44
+ await assertSafePath(target, item.file);
41
45
  await cp(target, temporaryRoot, {
42
46
  recursive: true,
43
47
  dereference: false,
@@ -49,7 +53,7 @@ export async function runPruneProbe({ target, packageRoot, taskId, testId } = {}
49
53
  const dependencies = path.join(target, "node_modules");
50
54
  try { await symlink(dependencies, path.join(temporaryRoot, "node_modules"), "junction"); } catch { /* dependencies are optional for static probes */ }
51
55
  if (item.file.endsWith(".mjs") || item.file.endsWith(".js")) {
52
- const candidatePath = path.join(temporaryRoot, item.file);
56
+ const candidatePath = await assertSafePath(temporaryRoot, item.file);
53
57
  const source = await readFile(candidatePath, "utf8");
54
58
  const escapedName = item.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
55
59
  const pattern = new RegExp(`\\b(test|it)\\s*\\(\\s*([\\"'])${escapedName}\\2`);