@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.
- package/CONTRIBUTING.md +11 -7
- package/DOCS_INDEX.md +2 -1
- package/ENG/c-development-eng.md +2 -2
- package/ENG/cpp-development-eng.md +2 -2
- package/ENG/dotnet-aspnetcore-development-eng.md +7 -1
- package/ENG/flutter-development-eng.md +4 -2
- package/ENG/go-development-eng.md +2 -2
- package/ENG/java-development-eng.md +3 -3
- package/ENG/nodejs-backend-development-eng.md +8 -4
- package/ENG/php-development-eng.md +3 -3
- package/ENG/python-development-eng.md +205 -0
- package/ENG/rust-development-eng.md +42 -1
- package/ENG/sql-development-eng.md +4 -4
- package/ENG/swift-development-eng.md +5 -4
- package/ENG/test-code-eng.md +1 -1
- package/ENG/typescript-development-eng.md +4 -4
- package/GUIDE_ROUTER.md +6 -0
- package/LICENSE +133 -21
- package/LOOP_SYSTEM_DESIGN.md +10 -10
- package/README.md +28 -39
- package/THIRD_PARTY_NOTICES.md +3 -4
- package/completions/_forgeloop +11 -3
- package/completions/forgeloop.bash +19 -3
- package/completions/forgeloop.fish +61 -1
- package/docs/AGENT_PROTOCOL_SUMMARY.md +50 -40
- package/docs/ARTIFACT_REFERENCE.md +50 -37
- package/docs/CLI_REFERENCE.md +147 -3
- package/docs/DOCUMENTATION_GUIDE.md +6 -4
- package/docs/GETTING_STARTED.md +3 -3
- package/docs/PACKAGE_CONTENTS.md +14 -13
- package/docs/RELEASE_CHECKLIST.md +81 -10
- package/docs/SELF_HOSTED_TESTS.md +121 -0
- package/docs/documentation-manifest.json +25 -12
- package/package.json +15 -10
- package/schemas/task-bundle.schema.json +7 -2
- package/scripts/generate-agent-protocol-summary.mjs +2 -2
- package/scripts/run-mcp-clean-tests.mjs +30 -0
- package/src/adapters/agent-browser/provider.js +14 -11
- package/src/adapters/emulated-services/provider.js +11 -4
- package/src/adapters/temporary-paths.js +18 -0
- package/src/cli.js +146 -210
- package/src/commands/action-show.js +5 -2
- package/src/commands/advance.js +5 -1
- package/src/commands/baseline.js +13 -11
- package/src/commands/checkpoint-revalidate.js +24 -8
- package/src/commands/complete.js +19 -3
- package/src/commands/contract-create.js +3 -1
- package/src/commands/doctor.js +36 -13
- package/src/commands/gate-record.js +20 -31
- package/src/commands/gate-revalidate.js +11 -13
- package/src/commands/handoff-list.js +6 -1
- package/src/commands/handoff-show.js +6 -1
- package/src/commands/migrate-protocol.js +2 -2
- package/src/commands/model-route.js +9 -1
- package/src/commands/next.js +7 -1
- package/src/commands/policy-diff.js +25 -4
- package/src/commands/progress.js +17 -11
- package/src/commands/record-diagnosis.js +3 -0
- package/src/commands/semantic-plan.js +26 -2
- package/src/commands/status.js +6 -1
- package/src/commands/storage-backup.js +15 -0
- package/src/commands/storage-migrate.js +5 -0
- package/src/commands/storage-migration-resume.js +5 -0
- package/src/commands/storage-migration-status.js +44 -0
- package/src/commands/storage-restore-resume.js +7 -0
- package/src/commands/storage-restore.js +8 -0
- package/src/commands/task-abandon.js +1 -1
- package/src/commands/task-list.js +8 -43
- package/src/commands/task-migrate.js +3 -3
- package/src/commands/task-recover.js +10 -2
- package/src/commands/task-scope.js +18 -2
- package/src/commands/task-show.js +16 -2
- package/src/commands/validate-protocol.js +29 -14
- package/src/commands/validate-receipt.js +8 -1
- package/src/config/guides.json +4 -0
- package/src/core/action-ledger-projection.js +10 -3
- package/src/core/actions.js +38 -75
- package/src/core/activation.js +11 -3
- package/src/core/approvals.js +17 -51
- package/src/core/artifact-registry.js +59 -2
- package/src/core/artifacts.js +81 -27
- package/src/core/attachment-paths.js +45 -0
- package/src/core/attestation-coverage.js +6 -1
- package/src/core/attestation-verifier.js +21 -2
- package/src/core/attestation.js +17 -6
- package/src/core/audit.js +6 -1
- package/src/core/bundles.js +160 -66
- package/src/core/checkpoint-revalidation.js +12 -10
- package/src/core/cli-command-definitions.js +109 -7
- package/src/core/code-manifest.js +9 -3
- package/src/core/command-executors.js +182 -126
- package/src/core/command-input.js +21 -1
- package/src/core/completion-artifacts.js +71 -29
- package/src/core/completion-ownership.js +19 -8
- package/src/core/completion-recovery-rebind.js +11 -1
- package/src/core/completion.js +60 -9
- package/src/core/conformance.js +2 -1
- package/src/core/continuity-lint.js +13 -2
- package/src/core/continuity-reconciliation.js +16 -3
- package/src/core/continuity.js +16 -8
- package/src/core/contract-bootstrap-recovery.js +42 -41
- package/src/core/contract-revision.js +8 -9
- package/src/core/decision/artifact.js +2 -1
- package/src/core/decision/events.js +9 -6
- package/src/core/decision/resolver.js +19 -11
- package/src/core/decision/service.js +63 -38
- package/src/core/decision/task-bindings.js +6 -1
- package/src/core/diagnosis-model.js +11 -0
- package/src/core/diagnosis.js +10 -1
- package/src/core/diagnostic-projection.js +29 -12
- package/src/core/diagnostic-record.js +28 -3
- package/src/core/efficiency.js +6 -1
- package/src/core/events.js +316 -97
- package/src/core/execution-prerequisites.js +22 -13
- package/src/core/execution.js +26 -40
- package/src/core/filesystem.js +11 -0
- package/src/core/gate-artifact.js +105 -19
- package/src/core/gate-provenance.js +6 -5
- package/src/core/handoff.js +26 -14
- package/src/core/history.js +39 -35
- package/src/core/hypothesis-projection.js +32 -0
- package/src/core/information-gain-projection.js +104 -100
- package/src/core/inspect.js +12 -3
- package/src/core/integration-invocation-policy.js +10 -0
- package/src/core/integration-resources.js +41 -34
- package/src/core/ledger-event-collection.js +189 -0
- package/src/core/ledger-relations.js +23 -0
- package/src/core/native-storage.js +12 -0
- package/src/core/next-action-continuity.js +31 -9
- package/src/core/next-action-phases.js +1 -1
- package/src/core/next-action-review-phase.js +2 -2
- package/src/core/next-action-verification-phase.js +2 -1
- package/src/core/next-action.js +21 -2
- package/src/core/phase.js +138 -93
- package/src/core/policy-engine.js +12 -13
- package/src/core/preflight-consistency.js +72 -59
- package/src/core/preflight-loaders.js +19 -12
- package/src/core/preflight-model.js +6 -1
- package/src/core/preflight.js +29 -12
- package/src/core/progress.js +21 -10
- package/src/core/protocol-info.js +3 -0
- package/src/core/protocol-migration.js +7 -6
- package/src/core/reconcile-closure.js +162 -24
- package/src/core/recovery-history.js +66 -47
- package/src/core/reflection.js +72 -77
- package/src/core/responsibility.js +25 -13
- package/src/core/route-artifact.js +13 -3
- package/src/core/runtime-context.js +5 -0
- package/src/core/schema-validation.js +8 -3
- package/src/core/settlement.js +12 -1
- package/src/core/signing/bundle-file.js +53 -0
- package/src/core/signing/bundle-reference.js +15 -0
- package/src/core/signing/sigstore.js +60 -33
- package/src/core/signing/statement-file.js +27 -0
- package/src/core/storage-runtime-registry.js +41 -0
- package/src/core/structural-quality/artifacts.js +20 -14
- package/src/core/structural-quality/service.js +81 -56
- package/src/core/target-layout.js +1 -1
- package/src/core/task-claim-state.js +52 -18
- package/src/core/task-command.js +21 -1
- package/src/core/task-conflict-inspection.js +112 -107
- package/src/core/task-descriptor.js +29 -22
- package/src/core/task-discovery.js +288 -90
- package/src/core/task-lock.js +66 -306
- package/src/core/task-migration-validation.js +5 -5
- package/src/core/task-migration.js +17 -287
- package/src/core/task-snapshot.js +51 -8
- package/src/core/task-storage-migration.js +18 -0
- package/src/core/templates.js +20 -8
- package/src/core/test-intelligence/prune.js +5 -1
- package/src/core/trace.js +154 -99
- package/src/core/trajectory-evaluation.js +15 -3
- package/src/core/trajectory-metrics.js +47 -32
- package/src/core/transaction.js +16 -288
- package/src/core/verification-scope.js +20 -12
- package/src/core/work-state.js +100 -59
- package/src/repository-index/binary-manager.js +7 -5
- package/src/storage/artifact-bytes.js +22 -0
- package/src/storage/attachment-binding.js +11 -0
- package/src/storage/attachment-files.js +82 -0
- package/src/storage/attachment-references.js +36 -0
- package/src/storage/backup.js +53 -0
- package/src/storage/capabilities.js +31 -0
- package/src/storage/connection-owner.js +119 -0
- package/src/storage/connection.js +376 -0
- package/src/storage/existing-project-scope.js +45 -0
- package/src/storage/export-destination.js +44 -0
- package/src/storage/exporter.js +284 -0
- package/src/storage/file-durability.js +46 -0
- package/src/storage/importer.js +461 -0
- package/src/storage/index.js +74 -0
- package/src/storage/leases.js +63 -0
- package/src/storage/ledger-event-snapshot.js +150 -0
- package/src/storage/ledger-relations.js +220 -0
- package/src/storage/legacy-maintenance-boundary.js +18 -0
- package/src/storage/legacy-signature.js +115 -0
- package/src/storage/maintenance-handoff.js +129 -0
- package/src/storage/maintenance-owner.js +42 -0
- package/src/storage/maintenance.js +310 -0
- package/src/storage/metadata-json.js +35 -0
- package/src/storage/migration-activate.js +59 -0
- package/src/storage/migration-archive.js +57 -0
- package/src/storage/migration-attachment-root.js +61 -0
- package/src/storage/migration-candidate.js +398 -0
- package/src/storage/migration-publication.js +143 -0
- package/src/storage/migration-rollback-admission.js +54 -0
- package/src/storage/migration-rollback-preparation.js +132 -0
- package/src/storage/migration-rollback-publication.js +183 -0
- package/src/storage/migration-rollback-release.js +66 -0
- package/src/storage/migration-rollback-source-stage.js +103 -0
- package/src/storage/migration-rollback.js +43 -0
- package/src/storage/migration-source-partition.js +56 -0
- package/src/storage/migration-source.js +255 -0
- package/src/storage/migration-terminal.js +39 -0
- package/src/storage/migration-validation.js +75 -0
- package/src/storage/migration.js +43 -0
- package/src/storage/operational-attachments.js +49 -0
- package/src/storage/operational-context.js +50 -0
- package/src/storage/project-backup.js +98 -0
- package/src/storage/project-bootstrap.js +24 -0
- package/src/storage/project-boundary.js +79 -0
- package/src/storage/project-read-snapshot.js +18 -0
- package/src/storage/project-replacement.js +137 -0
- package/src/storage/project-restore.js +203 -0
- package/src/storage/record-definitions.js +45 -0
- package/src/storage/repository.js +507 -0
- package/src/storage/restore-journal.js +62 -0
- package/src/storage/restore-layout.js +30 -0
- package/src/storage/restore-preparation.js +84 -0
- package/src/storage/restore-replacement-archive.js +162 -0
- package/src/storage/restore-replacement-owner.js +31 -0
- package/src/storage/restore-replacement-preparation.js +128 -0
- package/src/storage/restore-replacement.js +142 -0
- package/src/storage/restore-terminal.js +32 -0
- package/src/storage/runtime.js +31 -0
- package/src/storage/schema.js +214 -0
- package/src/storage/singleton-import.js +84 -0
- package/src/storage/snapshot-continuity.js +16 -0
- package/src/storage/snapshot.js +36 -0
- package/src/storage/storage-marker.js +79 -0
- package/src/storage/task-guards.js +107 -0
- package/src/storage/topology.js +42 -0
- package/src/storage/transaction.js +179 -0
- package/src/storage/unit-of-work.js +663 -0
- package/src/storage/windows-process-incarnation.js +206 -0
- package/.forgeloop/forgeloop.gitignore +0 -4
- package/LICENSE-DOCS.md +0 -13
- package/src/core/transaction-maintenance.js +0 -70
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
48
|
+
const identitySchemas = { contract: "current-contract", state: "work-state", continuity: "continuity", receipt: "execution-receipt" };
|
|
94
49
|
for (const item of detection.legacyFiles) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
6
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
+
}
|
package/src/core/templates.js
CHANGED
|
@@ -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
|
|
16
|
-
".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 =
|
|
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(
|
|
109
|
-
|
|
110
|
-
|
|
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 =
|
|
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`);
|