@cassiomc1/forgeloop 0.1.6 → 0.1.8
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/AGENT_COMPATIBILITY.md +28 -16
- package/ENG/taste-frontend-eng.md +139 -0
- package/EXECUTION_STATE.md +12 -5
- package/GUIDE_ROUTER.md +24 -1
- package/LOOP_ENGINEERING.md +43 -0
- package/LOOP_SYSTEM_DESIGN.md +50 -49
- package/PROJECT_PROFILE.md +27 -12
- package/QUALITY_SCORECARD.md +7 -0
- package/README.md +141 -75
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/conformance/README.md +7 -1
- package/conformance/blind-premium-website/EXPECTED_ROUTE.json +1 -1
- package/conformance/complete-website/EXPECTED_ROUTE.json +1 -1
- package/conformance/runs/2026-08-13-codex-sixth-live.md +412 -0
- package/package.json +1 -1
- package/src/cli.js +2 -1
- package/src/commands/doctor.js +110 -13
- package/src/commands/init.js +12 -5
- package/src/commands/update.js +215 -2
- package/src/commands/validate-protocol.js +19 -2
- package/src/core/audit.js +16 -1
- package/src/core/events.js +8 -0
- package/src/core/guide-metadata.js +1 -0
- package/src/core/inspect.js +16 -7
- package/src/core/manifest.js +6 -0
- package/src/core/native-adapters.js +74 -0
- package/src/core/next-action.js +42 -1
- package/src/core/preflight.js +220 -46
- package/src/core/profile.js +12 -4
- package/src/core/protocol.js +8 -0
- package/src/core/report.js +4 -1
- package/src/core/resumability.js +61 -0
- package/src/core/route-artifact.js +9 -2
- package/src/core/router.js +2 -1
- package/src/core/target-layout.js +38 -0
- package/src/core/templates.js +14 -2
package/src/commands/update.js
CHANGED
|
@@ -1,13 +1,222 @@
|
|
|
1
|
+
import { rmdir, unlink } from "node:fs/promises";
|
|
2
|
+
|
|
1
3
|
import { assertSafePath, fileExists, ensureWithin, readBytes, writeFileAtomic } from "../core/filesystem.js";
|
|
2
4
|
import {
|
|
5
|
+
createManifest,
|
|
3
6
|
PACKAGE_NAME,
|
|
4
7
|
readManifest,
|
|
5
8
|
sha256,
|
|
6
9
|
writeManifest,
|
|
7
10
|
} from "../core/manifest.js";
|
|
8
11
|
import { readTemplateEntries } from "../core/templates.js";
|
|
12
|
+
import { isNativeAdapterPath, LAYOUT_VERSION } from "../core/target-layout.js";
|
|
9
13
|
|
|
10
14
|
const PROFILE_PATH = "PROJECT_PROFILE.md";
|
|
15
|
+
const LEGACY_CLEANUP_DIRECTORIES = Object.freeze(["ENG", "schemas"]);
|
|
16
|
+
|
|
17
|
+
function migrationConflict(code, path, message) {
|
|
18
|
+
return { code, path, message };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function addAction(actions, dryRun, action, path, details = {}) {
|
|
22
|
+
actions.push({ action: dryRun ? `would-${action}` : action, path, ...details });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function verifyWrite(filePath, expectedBytes) {
|
|
26
|
+
const actualBytes = await readBytes(filePath);
|
|
27
|
+
if (!actualBytes.equals(expectedBytes)) {
|
|
28
|
+
const error = new Error(`Migration write verification failed for ${filePath}`);
|
|
29
|
+
error.code = "E_MIGRATION_WRITE_VERIFY";
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function removeEmptyLegacyDirectory(target, relativePath, dryRun) {
|
|
35
|
+
await assertSafePath(target, relativePath);
|
|
36
|
+
if (dryRun) return;
|
|
37
|
+
try {
|
|
38
|
+
await rmdir(ensureWithin(target, relativePath));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code)) throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function addLegacyCleanup(cleanupFiles, cleanupDirectories, relativePath) {
|
|
45
|
+
cleanupFiles.add(relativePath);
|
|
46
|
+
for (const directory of LEGACY_CLEANUP_DIRECTORIES) {
|
|
47
|
+
if (relativePath === directory || relativePath.startsWith(`${directory}/`)) {
|
|
48
|
+
cleanupDirectories.add(directory);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function migrateLegacyLayout({ target, dryRun, packageVersion, currentManifest, entries }) {
|
|
54
|
+
const nextManifest = createManifest(packageVersion);
|
|
55
|
+
const actions = [];
|
|
56
|
+
const conflicts = [];
|
|
57
|
+
const writes = [];
|
|
58
|
+
const cleanupFiles = new Set();
|
|
59
|
+
const cleanupDirectories = new Set();
|
|
60
|
+
|
|
61
|
+
// Validate every path before creating the migration plan or touching data.
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
await assertSafePath(target, entry.relativePath);
|
|
64
|
+
if (entry.legacyRelativePath !== entry.relativePath) {
|
|
65
|
+
await assertSafePath(target, entry.legacyRelativePath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const destination = ensureWithin(target, entry.relativePath);
|
|
71
|
+
const legacyDestination = ensureWithin(target, entry.legacyRelativePath);
|
|
72
|
+
const sourceHash = sha256(entry.bytes);
|
|
73
|
+
const destinationExists = await fileExists(destination);
|
|
74
|
+
const legacyExists = entry.legacyRelativePath !== entry.relativePath
|
|
75
|
+
&& await fileExists(legacyDestination);
|
|
76
|
+
const legacyRecord = currentManifest.files[entry.legacyRelativePath];
|
|
77
|
+
const destinationRecord = currentManifest.files[entry.relativePath];
|
|
78
|
+
|
|
79
|
+
if (isNativeAdapterPath(entry.relativePath)) {
|
|
80
|
+
if (!destinationExists) {
|
|
81
|
+
addAction(actions, dryRun, "create", entry.relativePath);
|
|
82
|
+
writes.push({ destination, bytes: entry.bytes });
|
|
83
|
+
nextManifest.files[entry.relativePath] = { sha256: sourceHash, preserve: false };
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const currentBytes = await readBytes(destination);
|
|
88
|
+
const currentHash = sha256(currentBytes);
|
|
89
|
+
if (legacyRecord && currentHash === legacyRecord.sha256) {
|
|
90
|
+
if (currentHash === sourceHash) {
|
|
91
|
+
actions.push({ action: "skip", path: entry.relativePath, reason: "current-shim" });
|
|
92
|
+
} else {
|
|
93
|
+
addAction(actions, dryRun, "update-adapter", entry.relativePath);
|
|
94
|
+
writes.push({ destination, bytes: entry.bytes });
|
|
95
|
+
}
|
|
96
|
+
nextManifest.files[entry.relativePath] = { sha256: sourceHash, preserve: false };
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
conflicts.push(migrationConflict(
|
|
101
|
+
"E_NATIVE_ADAPTER_MIGRATION_CONFLICT",
|
|
102
|
+
entry.relativePath,
|
|
103
|
+
legacyRecord
|
|
104
|
+
? "Managed native adapter was modified; it was preserved and was not silently overwritten."
|
|
105
|
+
: "Native adapter is not owned by the legacy manifest; it was preserved and was not silently adopted.",
|
|
106
|
+
));
|
|
107
|
+
addAction(actions, dryRun, "preserve-conflict", entry.relativePath, {
|
|
108
|
+
reason: legacyRecord ? "managed-modified" : "unmanaged",
|
|
109
|
+
});
|
|
110
|
+
if (legacyRecord) nextManifest.files[entry.relativePath] = { ...legacyRecord };
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (destinationExists) {
|
|
115
|
+
const currentBytes = await readBytes(destination);
|
|
116
|
+
const currentHash = sha256(currentBytes);
|
|
117
|
+
if (!destinationRecord && currentHash !== sourceHash && entry.sourcePath !== PROFILE_PATH) {
|
|
118
|
+
conflicts.push(migrationConflict(
|
|
119
|
+
"E_HIDDEN_KIT_MIGRATION_CONFLICT",
|
|
120
|
+
entry.relativePath,
|
|
121
|
+
"Existing hidden kit file is unmanaged and was not overwritten.",
|
|
122
|
+
));
|
|
123
|
+
addAction(actions, dryRun, "preserve-conflict", entry.relativePath, { reason: "hidden-unmanaged" });
|
|
124
|
+
} else {
|
|
125
|
+
actions.push({ action: "skip", path: entry.relativePath, reason: "already-present" });
|
|
126
|
+
}
|
|
127
|
+
nextManifest.files[entry.relativePath] = {
|
|
128
|
+
sha256: currentHash,
|
|
129
|
+
preserve: entry.sourcePath === PROFILE_PATH
|
|
130
|
+
|| !destinationRecord
|
|
131
|
+
|| Boolean(destinationRecord.preserve),
|
|
132
|
+
};
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!legacyExists) {
|
|
137
|
+
addAction(actions, dryRun, "create", entry.relativePath);
|
|
138
|
+
writes.push({ destination, bytes: entry.bytes });
|
|
139
|
+
nextManifest.files[entry.relativePath] = {
|
|
140
|
+
sha256: sourceHash,
|
|
141
|
+
preserve: entry.sourcePath === PROFILE_PATH,
|
|
142
|
+
};
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const legacyBytes = await readBytes(legacyDestination);
|
|
147
|
+
const legacyHash = sha256(legacyBytes);
|
|
148
|
+
const unchangedManaged = Boolean(legacyRecord) && legacyHash === legacyRecord.sha256;
|
|
149
|
+
|
|
150
|
+
if (entry.sourcePath === PROFILE_PATH && legacyRecord) {
|
|
151
|
+
addAction(actions, dryRun, "move-profile", entry.legacyRelativePath, { to: entry.relativePath });
|
|
152
|
+
writes.push({ destination, bytes: legacyBytes });
|
|
153
|
+
if (!dryRun) addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath);
|
|
154
|
+
nextManifest.files[entry.relativePath] = { sha256: legacyHash, preserve: true };
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (legacyRecord?.preserve && unchangedManaged) {
|
|
159
|
+
addAction(actions, dryRun, "move-preserved", entry.legacyRelativePath, { to: entry.relativePath });
|
|
160
|
+
writes.push({ destination, bytes: legacyBytes });
|
|
161
|
+
if (!dryRun) addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath);
|
|
162
|
+
nextManifest.files[entry.relativePath] = { sha256: legacyHash, preserve: true };
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (unchangedManaged) {
|
|
167
|
+
addAction(actions, dryRun, "migrate", entry.legacyRelativePath, { to: entry.relativePath });
|
|
168
|
+
writes.push({ destination, bytes: entry.bytes });
|
|
169
|
+
if (!dryRun) addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath);
|
|
170
|
+
nextManifest.files[entry.relativePath] = { sha256: sourceHash, preserve: false };
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const conflict = entry.sourcePath === PROFILE_PATH
|
|
175
|
+
? migrationConflict(
|
|
176
|
+
"E_PROFILE_MIGRATION_CONFLICT",
|
|
177
|
+
entry.legacyRelativePath,
|
|
178
|
+
"Project profile is not owned by the legacy manifest; it was preserved and was not overwritten or deleted.",
|
|
179
|
+
)
|
|
180
|
+
: migrationConflict(
|
|
181
|
+
"E_LEGACY_FILE_MIGRATION_CONFLICT",
|
|
182
|
+
entry.legacyRelativePath,
|
|
183
|
+
legacyRecord
|
|
184
|
+
? "Managed legacy file was modified; it was preserved while the hidden canonical file was installed."
|
|
185
|
+
: "Unmanaged legacy file was preserved while the hidden canonical file was installed.",
|
|
186
|
+
);
|
|
187
|
+
conflicts.push(conflict);
|
|
188
|
+
addAction(actions, dryRun, "preserve-conflict", entry.legacyRelativePath, {
|
|
189
|
+
to: entry.relativePath,
|
|
190
|
+
reason: legacyRecord ? "managed-modified" : "unmanaged",
|
|
191
|
+
});
|
|
192
|
+
writes.push({ destination, bytes: entry.bytes });
|
|
193
|
+
nextManifest.files[entry.relativePath] = {
|
|
194
|
+
sha256: sourceHash,
|
|
195
|
+
preserve: entry.sourcePath === PROFILE_PATH,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Apply all hidden writes and verify their bytes before removing any legacy file.
|
|
200
|
+
for (const plan of writes) {
|
|
201
|
+
await writeFileAtomic(plan.destination, plan.bytes, { dryRun });
|
|
202
|
+
if (!dryRun) await verifyWrite(plan.destination, plan.bytes);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const relativePath of cleanupFiles) {
|
|
206
|
+
await assertSafePath(target, relativePath);
|
|
207
|
+
const legacyPath = ensureWithin(target, relativePath);
|
|
208
|
+
if (!dryRun && await fileExists(legacyPath)) await unlink(legacyPath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const relativePath of cleanupDirectories) {
|
|
212
|
+
await removeEmptyLegacyDirectory(target, relativePath, dryRun);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// The manifest is written last so layoutVersion 2 is never authoritative before
|
|
216
|
+
// hidden destinations and adapter decisions have been applied.
|
|
217
|
+
await writeManifest(target, nextManifest, { dryRun });
|
|
218
|
+
return { actions, conflicts, manifest: nextManifest };
|
|
219
|
+
}
|
|
11
220
|
|
|
12
221
|
export async function runUpdate({ target, dryRun, packageRoot, packageVersion }) {
|
|
13
222
|
const currentManifest = await readManifest(target);
|
|
@@ -16,6 +225,10 @@ export async function runUpdate({ target, dryRun, packageRoot, packageVersion })
|
|
|
16
225
|
}
|
|
17
226
|
|
|
18
227
|
const entries = await readTemplateEntries(packageRoot);
|
|
228
|
+
if ((currentManifest.layoutVersion ?? 1) < LAYOUT_VERSION) {
|
|
229
|
+
return migrateLegacyLayout({ target, dryRun, packageVersion, currentManifest, entries });
|
|
230
|
+
}
|
|
231
|
+
|
|
19
232
|
const nextManifest = structuredClone(currentManifest);
|
|
20
233
|
const actions = [];
|
|
21
234
|
const conflicts = [];
|
|
@@ -47,7 +260,7 @@ export async function runUpdate({ target, dryRun, packageRoot, packageVersion })
|
|
|
47
260
|
entry,
|
|
48
261
|
record: {
|
|
49
262
|
sha256: sourceHash,
|
|
50
|
-
preserve: entry.
|
|
263
|
+
preserve: entry.sourcePath === PROFILE_PATH,
|
|
51
264
|
},
|
|
52
265
|
});
|
|
53
266
|
continue;
|
|
@@ -58,7 +271,7 @@ export async function runUpdate({ target, dryRun, packageRoot, packageVersion })
|
|
|
58
271
|
continue;
|
|
59
272
|
}
|
|
60
273
|
|
|
61
|
-
if (record.preserve || entry.
|
|
274
|
+
if (record.preserve || entry.sourcePath === PROFILE_PATH) {
|
|
62
275
|
actions.push({ action: "skip", path: entry.relativePath, reason: "preserved" });
|
|
63
276
|
continue;
|
|
64
277
|
}
|
|
@@ -6,6 +6,8 @@ import { assertRouteInvariants } from "../core/router.js";
|
|
|
6
6
|
import { assertWorkStateSemantics, classifyLoadedWorkState } from "../core/work-state.js";
|
|
7
7
|
import { validateReceipt } from "../core/receipt.js";
|
|
8
8
|
import { validateTaskBrief, validateDelegatedResult } from "../core/delegation.js";
|
|
9
|
+
import { ARTIFACT_PATHS, readJsonArtifact } from "../core/artifacts.js";
|
|
10
|
+
import { evaluatePreflight, validateReadyProtocolConsistency } from "../core/preflight.js";
|
|
9
11
|
|
|
10
12
|
async function readArtifact(target, relativePath, label) {
|
|
11
13
|
if (!relativePath) return null;
|
|
@@ -102,11 +104,26 @@ export async function runValidateProtocol({
|
|
|
102
104
|
taskBriefs,
|
|
103
105
|
delegatedResults,
|
|
104
106
|
});
|
|
105
|
-
|
|
107
|
+
let readyConsistencyErrors = [];
|
|
108
|
+
try {
|
|
109
|
+
const persistedPreflight = await readJsonArtifact(target, ARTIFACT_PATHS.preflight, "preflight", packageRoot);
|
|
110
|
+
if (persistedPreflight.value.status === "READY") {
|
|
111
|
+
readyConsistencyErrors = await validateReadyProtocolConsistency({
|
|
112
|
+
target,
|
|
113
|
+
packageRoot,
|
|
114
|
+
persisted: persistedPreflight.value,
|
|
115
|
+
current: await evaluatePreflight({ target, packageRoot }),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// A missing or invalid preflight is already outside the optional protocol set.
|
|
120
|
+
}
|
|
121
|
+
if (readErrors.length > 0 || schemaErrors.length > 0 || readyConsistencyErrors.length > 0) {
|
|
106
122
|
return {
|
|
107
123
|
...result,
|
|
108
124
|
status: "INVALID",
|
|
109
|
-
errors: [...result.errors, ...readErrors, ...schemaErrors
|
|
125
|
+
errors: [...result.errors, ...readErrors, ...schemaErrors, ...readyConsistencyErrors]
|
|
126
|
+
.sort((left, right) => left.code.localeCompare(right.code) || left.message.localeCompare(right.message)),
|
|
110
127
|
};
|
|
111
128
|
}
|
|
112
129
|
return result;
|
package/src/core/audit.js
CHANGED
|
@@ -4,6 +4,7 @@ import { readManifest } from "./manifest.js";
|
|
|
4
4
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
5
5
|
import { readJsonArtifact } from "./artifacts.js";
|
|
6
6
|
import { currentChangedPaths } from "./repository.js";
|
|
7
|
+
import { validateReadyProtocolConsistency } from "./preflight.js";
|
|
7
8
|
|
|
8
9
|
function sortErrors(errors) {
|
|
9
10
|
return [...errors].sort((left, right) => left.code.localeCompare(right.code)
|
|
@@ -42,7 +43,21 @@ export async function evaluateAudit({ target, packageRoot, strict = false } = {}
|
|
|
42
43
|
} catch (error) {
|
|
43
44
|
manifestError = error.message;
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
let readyConsistencyErrors = [];
|
|
47
|
+
try {
|
|
48
|
+
const persistedPreflight = await readJsonArtifact(target, ARTIFACT_PATHS.preflight, "preflight", packageRoot);
|
|
49
|
+
if (persistedPreflight.value.status === "READY") {
|
|
50
|
+
readyConsistencyErrors = await validateReadyProtocolConsistency({
|
|
51
|
+
target,
|
|
52
|
+
packageRoot,
|
|
53
|
+
persisted: persistedPreflight.value,
|
|
54
|
+
current: completion.preflight,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Completion already reports missing or invalid preflight artifacts.
|
|
59
|
+
}
|
|
60
|
+
const errors = sortErrors([...completion.errors, ...readyConsistencyErrors]);
|
|
46
61
|
const changedPaths = await compareChangedPaths(target, packageRoot);
|
|
47
62
|
if (changedPaths.status === "MISMATCH") {
|
|
48
63
|
errors.push({
|
package/src/core/events.js
CHANGED
|
@@ -19,6 +19,14 @@ export const LIFECYCLE_MILESTONES = Object.freeze([
|
|
|
19
19
|
"VERIFICATION_RECORDED",
|
|
20
20
|
"COMPLETION_VALIDATED",
|
|
21
21
|
]);
|
|
22
|
+
export const ACTIVATION_EVENT_MATRIX = Object.freeze([
|
|
23
|
+
Object.freeze({ stage: "task received", event: "TASK_RECEIVED", requiredFor: "new activation" }),
|
|
24
|
+
Object.freeze({ stage: "contract validated", event: "CONTRACT_VALIDATED", requiredFor: "preflight readiness" }),
|
|
25
|
+
Object.freeze({ stage: "route validated", event: "ROUTE_VALIDATED", requiredFor: "preflight readiness" }),
|
|
26
|
+
Object.freeze({ stage: "gate satisfied", event: "GATE_SATISFIED", requiredFor: "each satisfied gate" }),
|
|
27
|
+
Object.freeze({ stage: "preflight blocked", event: "PREFLIGHT_BLOCKED", requiredFor: "blocked activation" }),
|
|
28
|
+
Object.freeze({ stage: "preflight ready", event: "PREFLIGHT_READY", requiredFor: "resumable readiness" }),
|
|
29
|
+
]);
|
|
22
30
|
const REPEATABLE_MILESTONES = new Set(["VERIFICATION_RECORDED"]);
|
|
23
31
|
|
|
24
32
|
function eventHash(event) {
|
package/src/core/inspect.js
CHANGED
|
@@ -6,8 +6,9 @@ import { inspectSchemaHealth } from "./schema-validation.js";
|
|
|
6
6
|
import { readAndClassifyWorkState, WORK_STATE_PATH } from "./work-state.js";
|
|
7
7
|
import { createEvidence } from "./evidence.js";
|
|
8
8
|
import { runDoctor } from "../commands/doctor.js";
|
|
9
|
+
import { findProfilePath } from "./profile.js";
|
|
10
|
+
import { FORGELOOP_KIT_DIR } from "./target-layout.js";
|
|
9
11
|
|
|
10
|
-
const PROFILE_PATH = "PROJECT_PROFILE.md";
|
|
11
12
|
function profileMetadata(bytes) {
|
|
12
13
|
const text = bytes.toString("utf8");
|
|
13
14
|
return {
|
|
@@ -25,14 +26,21 @@ export async function inspectTarget({ target, packageRoot, contractFile = null }
|
|
|
25
26
|
manifestError = error.message;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
const profileRelativePath = await findProfilePath(target);
|
|
30
|
+
const profilePath = profileRelativePath ? ensureWithin(target, profileRelativePath) : null;
|
|
31
|
+
const profile = (profilePath && await fileExists(profilePath))
|
|
32
|
+
? { ...profileMetadata(await readBytes(profilePath)), path: profileRelativePath }
|
|
31
33
|
: { mode: null, status: null };
|
|
32
34
|
const statePath = ensureWithin(target, WORK_STATE_PATH);
|
|
33
35
|
const statePresent = await fileExists(statePath);
|
|
34
36
|
const state = await readAndClassifyWorkState({ target, packageRoot, contractFile });
|
|
35
|
-
const
|
|
37
|
+
const schemaRoot = manifest?.layoutVersion >= 2
|
|
38
|
+
? ensureWithin(target, FORGELOOP_KIT_DIR)
|
|
39
|
+
: target;
|
|
40
|
+
const schemaHealth = await inspectSchemaHealth(schemaRoot);
|
|
41
|
+
const schemaPathPrefix = manifest?.layoutVersion >= 2
|
|
42
|
+
? `${FORGELOOP_KIT_DIR}/schemas`
|
|
43
|
+
: "schemas";
|
|
36
44
|
const doctor = await runDoctor({ target, packageRoot });
|
|
37
45
|
const agents = await Promise.all(AGENT_SUPPORT.map(async (record) => ({
|
|
38
46
|
id: record.id,
|
|
@@ -50,12 +58,12 @@ export async function inspectTarget({ target, packageRoot, contractFile = null }
|
|
|
50
58
|
findings.push({
|
|
51
59
|
code: `schema-${schema.status}`,
|
|
52
60
|
severity: "error",
|
|
53
|
-
path:
|
|
61
|
+
path: `${schemaPathPrefix}/${schema.name}.schema.json`,
|
|
54
62
|
message: schema.error ?? `Schema is ${schema.status}.`,
|
|
55
63
|
remediation: "Restore the shipped schema and rerun inspect.",
|
|
56
64
|
evidence: createEvidence({
|
|
57
65
|
kind: schema.status === "missing" ? "NOT_VERIFIED" : "OBSERVED",
|
|
58
|
-
source:
|
|
66
|
+
source: `${schemaPathPrefix}/${schema.name}.schema.json`,
|
|
59
67
|
result: schema.status,
|
|
60
68
|
}),
|
|
61
69
|
});
|
|
@@ -88,6 +96,7 @@ export async function inspectTarget({ target, packageRoot, contractFile = null }
|
|
|
88
96
|
present: manifest !== null,
|
|
89
97
|
status: manifestError ? "invalid" : manifest ? "ready" : "missing",
|
|
90
98
|
packageVersion: manifest?.packageVersion ?? null,
|
|
99
|
+
layoutVersion: manifest?.layoutVersion ?? 1,
|
|
91
100
|
error: manifestError,
|
|
92
101
|
},
|
|
93
102
|
profile,
|
package/src/core/manifest.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
|
|
4
4
|
import { assertSafePath, ensureWithin, fileExists, writeFileAtomic } from "./filesystem.js";
|
|
5
5
|
import { assertJsonBytes, assertJsonLimits } from "./json-safety.js";
|
|
6
|
+
import { LAYOUT_VERSION, LEGACY_LAYOUT_VERSION } from "./target-layout.js";
|
|
6
7
|
|
|
7
8
|
export const MANIFEST_SCHEMA_VERSION = 1;
|
|
8
9
|
export const MANIFEST_PATH = ".forgeloop/manifest.json";
|
|
@@ -15,6 +16,7 @@ export function sha256(bytes) {
|
|
|
15
16
|
export function createManifest(packageVersion) {
|
|
16
17
|
return {
|
|
17
18
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
19
|
+
layoutVersion: LAYOUT_VERSION,
|
|
18
20
|
packageName: PACKAGE_NAME,
|
|
19
21
|
packageVersion,
|
|
20
22
|
files: {},
|
|
@@ -28,6 +30,10 @@ function validateManifest(manifest) {
|
|
|
28
30
|
if (manifest.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
|
|
29
31
|
throw new Error(`Unsupported manifest schema: ${manifest.schemaVersion}`);
|
|
30
32
|
}
|
|
33
|
+
if (manifest.layoutVersion !== undefined
|
|
34
|
+
&& ![LEGACY_LAYOUT_VERSION, LAYOUT_VERSION].includes(manifest.layoutVersion)) {
|
|
35
|
+
throw new Error(`Unsupported manifest layout: ${manifest.layoutVersion}`);
|
|
36
|
+
}
|
|
31
37
|
if (typeof manifest.packageVersion !== "string" || !manifest.packageVersion) {
|
|
32
38
|
throw new Error("Manifest packageVersion is required");
|
|
33
39
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
|
|
4
|
+
import { isKitPath } from "./target-layout.js";
|
|
5
|
+
|
|
6
|
+
const LEGACY_REFERENCE_PATTERN = /(?:\.\.\/|\.\/)+(?:LOOP_ENGINEERING|PROJECT_PROFILE|GUIDE_ROUTER)\.md\b/g;
|
|
7
|
+
|
|
8
|
+
export function nativeShimPrefix(relativePath) {
|
|
9
|
+
if (relativePath.startsWith(".cursor/")) return "../../.forgeloop/kit";
|
|
10
|
+
if (relativePath.startsWith(".github/")) return "../.forgeloop/kit";
|
|
11
|
+
return ".forgeloop/kit";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function nativeShimReferences(relativePath) {
|
|
15
|
+
const prefix = nativeShimPrefix(relativePath);
|
|
16
|
+
return [
|
|
17
|
+
`${prefix}/LOOP_ENGINEERING.md`,
|
|
18
|
+
`${prefix}/AGENT_COMPATIBILITY.md`,
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function nativeShim(relativePath) {
|
|
23
|
+
const kitPrefix = nativeShimPrefix(relativePath);
|
|
24
|
+
return `# ForgeLoop native adapter\n\nRead and follow the canonical ForgeLoop protocol in ${kitPrefix}/LOOP_ENGINEERING.md and ${kitPrefix}/AGENT_COMPATIBILITY.md.\nThe canonical guides and schemas are under ${kitPrefix}/; keep this adapter concise and preserve any host-specific instructions.\n`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveNativeReference(relativePath, reference) {
|
|
28
|
+
return path.posix.normalize(path.posix.join(path.posix.dirname(relativePath), reference));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function inspectNativeAdapter(relativePath, bytes) {
|
|
32
|
+
const text = Buffer.from(bytes).toString("utf8");
|
|
33
|
+
const expected = nativeShimReferences(relativePath);
|
|
34
|
+
const legacyReferences = text.match(LEGACY_REFERENCE_PATTERN) ?? [];
|
|
35
|
+
const missingReferences = expected.filter((reference) => !text.includes(reference));
|
|
36
|
+
const resolvedReferences = expected.map((reference) => ({
|
|
37
|
+
reference,
|
|
38
|
+
path: resolveNativeReference(relativePath, reference),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
text,
|
|
43
|
+
expected,
|
|
44
|
+
legacyReferences,
|
|
45
|
+
missingReferences,
|
|
46
|
+
resolvedReferences,
|
|
47
|
+
hasForgeLoopMarker: /forgeloop/i.test(text),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function validateNativeAdapterTargets({ target, relativePath, bytes }) {
|
|
52
|
+
const inspection = inspectNativeAdapter(relativePath, bytes);
|
|
53
|
+
const invalidReferences = inspection.resolvedReferences.filter(({ path: resolvedPath }) => !isKitPath(resolvedPath));
|
|
54
|
+
const missingTargets = [];
|
|
55
|
+
|
|
56
|
+
for (const { path: resolvedPath } of inspection.resolvedReferences) {
|
|
57
|
+
if (!isKitPath(resolvedPath)) continue;
|
|
58
|
+
try {
|
|
59
|
+
await assertSafePath(target, resolvedPath);
|
|
60
|
+
if (!(await fileExists(ensureWithin(target, resolvedPath)))) missingTargets.push(resolvedPath);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
invalidReferences.push({ path: resolvedPath, error: error.message });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
...inspection,
|
|
68
|
+
invalidReferences,
|
|
69
|
+
missingTargets,
|
|
70
|
+
stale: inspection.legacyReferences.length > 0
|
|
71
|
+
|| inspection.missingReferences.length > 0
|
|
72
|
+
|| invalidReferences.length > 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
package/src/core/next-action.js
CHANGED
|
@@ -3,7 +3,7 @@ import { completionIdentityErrors, evaluateCompletion } from "./completion.js";
|
|
|
3
3
|
import { requiredEvidenceForTarget } from "./completion-artifacts.js";
|
|
4
4
|
import { coverageForRequirements } from "./coverage.js";
|
|
5
5
|
import { readContract } from "./contract.js";
|
|
6
|
-
import { evaluatePreflight, validatePersistedPreflight } from "./preflight.js";
|
|
6
|
+
import { evaluatePreflight, validatePersistedPreflight, validateReadyProtocolConsistency } from "./preflight.js";
|
|
7
7
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
8
8
|
import { validateReceipt } from "./receipt.js";
|
|
9
9
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
@@ -245,6 +245,47 @@ export async function getNextAction({ target, packageRoot } = {}) {
|
|
|
245
245
|
);
|
|
246
246
|
}
|
|
247
247
|
if (!workState.value) {
|
|
248
|
+
const persistedPreflight = await loadArtifact(
|
|
249
|
+
() => readJsonArtifact(target, ARTIFACT_PATHS.preflight, "preflight", packageRoot),
|
|
250
|
+
ARTIFACT_PATHS.preflight,
|
|
251
|
+
);
|
|
252
|
+
if (!persistedPreflight.error && persistedPreflight.value?.value?.status === "READY") {
|
|
253
|
+
try {
|
|
254
|
+
const consistencyErrors = await validateReadyProtocolConsistency({
|
|
255
|
+
target,
|
|
256
|
+
packageRoot,
|
|
257
|
+
persisted: persistedPreflight.value.value,
|
|
258
|
+
});
|
|
259
|
+
if (consistencyErrors.length > 0) {
|
|
260
|
+
return result({
|
|
261
|
+
taskId: persistedPreflight.value.value.taskId ?? "unknown",
|
|
262
|
+
currentPhase: "ROUTED",
|
|
263
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
264
|
+
reasons: consistencyErrors,
|
|
265
|
+
requiredArtifacts: [
|
|
266
|
+
ARTIFACT_PATHS.state,
|
|
267
|
+
ARTIFACT_PATHS.contract,
|
|
268
|
+
ARTIFACT_PATHS.route,
|
|
269
|
+
ARTIFACT_PATHS.preflight,
|
|
270
|
+
ARTIFACT_PATHS.events,
|
|
271
|
+
],
|
|
272
|
+
missingArtifacts: [ARTIFACT_PATHS.state],
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
return result({
|
|
277
|
+
taskId: persistedPreflight.value.value.taskId ?? "unknown",
|
|
278
|
+
currentPhase: "ROUTED",
|
|
279
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
280
|
+
reasons: [artifactError(
|
|
281
|
+
error.code ?? "E_PREFLIGHT_READY_INCONSISTENT",
|
|
282
|
+
error.message,
|
|
283
|
+
error.artifacts ?? [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events],
|
|
284
|
+
)],
|
|
285
|
+
requiredArtifacts: [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events],
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
248
289
|
return decision(
|
|
249
290
|
{},
|
|
250
291
|
NEXT_ACTIONS.DISCOVER,
|