@cassiomc1/forgeloop 0.1.15 → 1.0.0
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/DOCS_INDEX.md +60 -0
- package/EXECUTION_STATE.md +9 -0
- package/LOOP_ENGINEERING.md +15 -0
- package/LOOP_SYSTEM_DESIGN.md +8 -0
- package/PROTOCOL_INTEGRATION.md +8 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +183 -654
- package/TERMINOLOGY.md +4 -0
- package/THREAT_MODEL.md +9 -0
- package/docs/assets/forgeloop-flow.svg +1 -0
- package/docs/forgeloop-flow.mmd +51 -0
- package/package.json +16 -3
- package/schemas/continuity.schema.json +57 -0
- package/scripts/CI_VALIDATORS.md +32 -0
- package/src/cli.js +307 -204
- package/src/commands/clear-continuity.js +9 -0
- package/src/commands/continuity.js +25 -0
- package/src/commands/doctor.js +17 -2
- package/src/commands/reconcile-continuity.js +23 -0
- package/src/commands/record-continuity.js +63 -0
- package/src/commands/status.js +14 -1
- package/src/commands/update.js +12 -13
- package/src/commands/validate-protocol.js +19 -0
- package/src/core/artifacts.js +1 -0
- package/src/core/bundles.js +6 -0
- package/src/core/command-resolution.js +295 -0
- package/src/core/command-tokenizer.js +122 -0
- package/src/core/conformance.js +8 -2
- package/src/core/continuity-cli-options.js +58 -0
- package/src/core/continuity-conformance.js +46 -0
- package/src/core/continuity-observability.js +20 -0
- package/src/core/continuity-reconciliation.js +224 -0
- package/src/core/continuity.js +245 -0
- package/src/core/inspect.js +9 -1
- package/src/core/installation-authority.js +178 -0
- package/src/core/json-safety.js +17 -13
- package/src/core/next-action-artifacts.js +118 -0
- package/src/core/next-action-continuity.js +65 -0
- package/src/core/next-action-model.js +127 -0
- package/src/core/next-action-phases.js +12 -0
- package/src/core/next-action.js +22 -249
- package/src/core/npm-classifier.js +343 -0
- package/src/core/package-manager-classifiers.js +37 -0
- package/src/core/preflight-consistency.js +221 -0
- package/src/core/preflight-loaders.js +112 -0
- package/src/core/preflight-model.js +83 -0
- package/src/core/preflight.js +34 -444
- package/src/core/protocol.js +7 -0
- package/src/core/schema-validation.js +15 -1
- package/src/core/templates.js +2 -0
- package/src/core/verification-capability.js +31 -1106
- package/src/core/verification-constants.js +61 -0
package/src/commands/doctor.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertSafePath, fileExists, ensureWithin, readBytes } from "../core/filesystem.js";
|
|
1
|
+
import { assertSafePath, fileExists, ensureWithin, readBytes, writeFileAtomic } from "../core/filesystem.js";
|
|
2
2
|
import { readManifest, sha256, writeManifest } from "../core/manifest.js";
|
|
3
3
|
import { readTemplateEntries } from "../core/templates.js";
|
|
4
4
|
import { createEvidence } from "../core/evidence.js";
|
|
@@ -113,9 +113,10 @@ async function adoptAdapters({ target, manifest, adoptPaths, findings }) {
|
|
|
113
113
|
return nextManifest;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export async function runDoctor({ target, packageRoot, adoptPaths = [], strict = false }) {
|
|
116
|
+
export async function runDoctor({ target, packageRoot, adoptPaths = [], strict = false, fix = false }) {
|
|
117
117
|
const findings = [];
|
|
118
118
|
let manifest = null;
|
|
119
|
+
let manifestChanged = false;
|
|
119
120
|
try {
|
|
120
121
|
manifest = await readManifest(target);
|
|
121
122
|
if (!manifest) {
|
|
@@ -186,6 +187,16 @@ export async function runDoctor({ target, packageRoot, adoptPaths = [], strict =
|
|
|
186
187
|
));
|
|
187
188
|
continue;
|
|
188
189
|
}
|
|
190
|
+
if (fix && manifest && (layoutVersion >= LAYOUT_VERSION || managedPath.startsWith(".forgeloop/"))) {
|
|
191
|
+
await writeFileAtomic(destination, entry.bytes);
|
|
192
|
+
manifest.files[managedPath] = {
|
|
193
|
+
sha256: sha256(entry.bytes),
|
|
194
|
+
preserve: entry.sourcePath === "PROJECT_PROFILE.md",
|
|
195
|
+
};
|
|
196
|
+
manifestChanged = true;
|
|
197
|
+
findings.push(finding("file-restored", "info", managedPath, "Restored missing managed template file.", "No action required."));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
189
200
|
findings.push(finding("file-missing", "error", managedPath, "Managed file is missing."));
|
|
190
201
|
continue;
|
|
191
202
|
}
|
|
@@ -280,6 +291,10 @@ export async function runDoctor({ target, packageRoot, adoptPaths = [], strict =
|
|
|
280
291
|
}
|
|
281
292
|
}
|
|
282
293
|
|
|
294
|
+
if (manifestChanged && manifest) {
|
|
295
|
+
await writeManifest(target, manifest);
|
|
296
|
+
}
|
|
297
|
+
|
|
283
298
|
const ok = findings.every((item) => item.severity !== "error")
|
|
284
299
|
&& (!strict || findings.every((item) => item.severity !== "warning"));
|
|
285
300
|
return {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { reconcileContinuity } from "../core/continuity-reconciliation.js";
|
|
2
|
+
|
|
3
|
+
export async function runReconcileContinuity({ target, packageRoot } = {}) {
|
|
4
|
+
return reconcileContinuity({ target, packageRoot });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatReconcileContinuityResult(result) {
|
|
8
|
+
const lines = [
|
|
9
|
+
`Continuity: ${result.path ?? ".forgeloop/continuity.json"}`,
|
|
10
|
+
`Classification: ${result.classification}`,
|
|
11
|
+
`Task matches: ${result.taskMatches ?? "not-verified"}`,
|
|
12
|
+
`Work state matches: ${result.workStateMatches ?? "not-verified"}`,
|
|
13
|
+
`Contract matches: ${result.contractMatches ?? "not-verified"}`,
|
|
14
|
+
`Phase matches: ${result.phaseMatches ?? "not-verified"}`,
|
|
15
|
+
`Repository: ${result.repositoryComparison}`,
|
|
16
|
+
`Changed paths: ${result.changedPathComparison}`,
|
|
17
|
+
"Authority: OPERATIONAL_CONTEXT_ONLY",
|
|
18
|
+
"Evidence: NONE",
|
|
19
|
+
];
|
|
20
|
+
if (result.reasonCodes?.length) lines.push(`Reason codes: ${result.reasonCodes.join(", ")}`);
|
|
21
|
+
if (result.reasons?.length) lines.push(`Reasons: ${result.reasons.join(", ")}`);
|
|
22
|
+
return `${lines.join("\n")}\n`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { writeContinuity } from "../core/continuity.js";
|
|
2
|
+
|
|
3
|
+
function parseWorkItem(value, label) {
|
|
4
|
+
if (typeof value !== "string") throw new Error(`${label} must use <id>:<summary>`);
|
|
5
|
+
const separator = value.indexOf(":");
|
|
6
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
7
|
+
throw new Error(`${label} must use <id>:<summary>`);
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
id: value.slice(0, separator).trim(),
|
|
11
|
+
summary: value.slice(separator + 1).trim(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runRecordContinuity({
|
|
16
|
+
target,
|
|
17
|
+
packageRoot,
|
|
18
|
+
focusId,
|
|
19
|
+
focusSummary,
|
|
20
|
+
remaining = [],
|
|
21
|
+
knownIssues = [],
|
|
22
|
+
changedAreas = [],
|
|
23
|
+
inspectFirst = [],
|
|
24
|
+
resumeNote,
|
|
25
|
+
state,
|
|
26
|
+
contract,
|
|
27
|
+
repositoryFingerprint,
|
|
28
|
+
now,
|
|
29
|
+
dryRun = false,
|
|
30
|
+
} = {}) {
|
|
31
|
+
if ((focusId && !focusSummary) || (!focusId && focusSummary)) {
|
|
32
|
+
throw new Error("record-continuity requires --focus-id and --focus-summary together");
|
|
33
|
+
}
|
|
34
|
+
return writeContinuity(target, {
|
|
35
|
+
...(focusId ? { currentFocus: { id: focusId, summary: focusSummary } } : {}),
|
|
36
|
+
remainingWork: remaining.map((item) => parseWorkItem(item, "--remaining")),
|
|
37
|
+
knownIssues: knownIssues.map((item) => parseWorkItem(item, "--known-issue")),
|
|
38
|
+
changedAreas,
|
|
39
|
+
inspectFirst,
|
|
40
|
+
...(resumeNote ? { resumeNote } : {}),
|
|
41
|
+
}, {
|
|
42
|
+
packageRoot,
|
|
43
|
+
...(state ? { state } : {}),
|
|
44
|
+
...(contract ? { contract } : {}),
|
|
45
|
+
...(repositoryFingerprint ? { repositoryFingerprint } : {}),
|
|
46
|
+
...(now ? { now } : {}),
|
|
47
|
+
dryRun,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatRecordContinuityResult(result) {
|
|
52
|
+
const value = result.value;
|
|
53
|
+
return [
|
|
54
|
+
`Continuity: ${result.path}`,
|
|
55
|
+
`Task: ${value.taskId}`,
|
|
56
|
+
`Phase: ${value.phase}`,
|
|
57
|
+
`Focus: ${value.currentFocus?.id ?? "none"}`,
|
|
58
|
+
`Remaining: ${value.remainingWork.length}`,
|
|
59
|
+
`Known issues: ${value.knownIssues.length}`,
|
|
60
|
+
"Authority: OPERATIONAL_CONTEXT_ONLY",
|
|
61
|
+
"Evidence: NONE",
|
|
62
|
+
].join("\n") + "\n";
|
|
63
|
+
}
|
package/src/commands/status.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { readAndClassifyWorkState } from "../core/work-state.js";
|
|
2
2
|
import { inspectSchemaHealth } from "../core/schema-validation.js";
|
|
3
|
+
import { reconcileContinuity } from "../core/continuity-reconciliation.js";
|
|
3
4
|
|
|
4
5
|
export async function runStatus({ target, packageRoot, contractFile = null }) {
|
|
5
6
|
const state = await readAndClassifyWorkState({ target, packageRoot, contractFile });
|
|
6
|
-
const protocol = await
|
|
7
|
+
const [protocol, continuity] = await Promise.all([
|
|
8
|
+
inspectSchemaHealth(target),
|
|
9
|
+
reconcileContinuity({ target, packageRoot }),
|
|
10
|
+
]);
|
|
7
11
|
return {
|
|
8
12
|
...state,
|
|
9
13
|
protocol,
|
|
14
|
+
continuity,
|
|
10
15
|
evidence: [...(state.evidence ?? []), ...(protocol.evidence ?? [])],
|
|
11
16
|
};
|
|
12
17
|
}
|
|
@@ -24,6 +29,14 @@ export function formatStatusResult(result) {
|
|
|
24
29
|
if (result.contractComparison) lines.push(`Contract: ${result.contractComparison}`);
|
|
25
30
|
if (result.artifactComparison) lines.push(`Artifacts: ${result.artifactComparison}`);
|
|
26
31
|
if (result.protocol) lines.push(`Schemas: ${result.protocol.status}`);
|
|
32
|
+
if (result.continuity) {
|
|
33
|
+
const continuity = result.continuity.continuity;
|
|
34
|
+
lines.push(`Continuity: ${result.continuity.classification}`);
|
|
35
|
+
lines.push(`Continuity focus: ${continuity?.currentFocus?.id ?? "none"}`);
|
|
36
|
+
lines.push(`Continuity remaining: ${continuity?.remainingWork?.length ?? 0}`);
|
|
37
|
+
lines.push(`Continuity known issues: ${continuity?.knownIssues?.length ?? 0}`);
|
|
38
|
+
lines.push(`Continuity authority: ${result.continuity.authority ?? "OPERATIONAL_CONTEXT_ONLY"}`);
|
|
39
|
+
}
|
|
27
40
|
if (result.error) lines.push(`Error: ${result.error}`);
|
|
28
41
|
return `${lines.join("\n")}\n`;
|
|
29
42
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -9,9 +9,8 @@ import {
|
|
|
9
9
|
writeManifest,
|
|
10
10
|
} from "../core/manifest.js";
|
|
11
11
|
import { readTemplateEntries } from "../core/templates.js";
|
|
12
|
-
import { isNativeAdapterPath, LAYOUT_VERSION } from "../core/target-layout.js";
|
|
12
|
+
import { isNativeAdapterPath, LAYOUT_VERSION, LEGACY_PROFILE_PATH } from "../core/target-layout.js";
|
|
13
13
|
|
|
14
|
-
const PROFILE_PATH = "PROJECT_PROFILE.md";
|
|
15
14
|
const LEGACY_CLEANUP_DIRECTORIES = Object.freeze(["ENG", "schemas"]);
|
|
16
15
|
|
|
17
16
|
function migrationConflict(code, path, message) {
|
|
@@ -169,7 +168,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
169
168
|
const legacyHash = legacyBytes ? sha256(legacyBytes) : null;
|
|
170
169
|
const unchangedManagedLegacy = Boolean(legacyRecord) && legacyHash === legacyRecord.sha256;
|
|
171
170
|
|
|
172
|
-
if (entry.sourcePath ===
|
|
171
|
+
if (entry.sourcePath === LEGACY_PROFILE_PATH
|
|
173
172
|
&& legacyExists
|
|
174
173
|
&& legacyRecord
|
|
175
174
|
&& currentHash === legacyHash) {
|
|
@@ -179,7 +178,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
179
178
|
continue;
|
|
180
179
|
}
|
|
181
180
|
|
|
182
|
-
if (!hiddenMatchesSource && !destinationRecord && entry.sourcePath !==
|
|
181
|
+
if (!hiddenMatchesSource && !destinationRecord && entry.sourcePath !== LEGACY_PROFILE_PATH) {
|
|
183
182
|
conflicts.push(migrationConflict(
|
|
184
183
|
"E_HIDDEN_KIT_MIGRATION_CONFLICT",
|
|
185
184
|
entry.relativePath,
|
|
@@ -191,12 +190,12 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
191
190
|
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath, legacyHash);
|
|
192
191
|
nextManifest.files[entry.relativePath] = manifestRecord(
|
|
193
192
|
sourceHash,
|
|
194
|
-
entry.sourcePath ===
|
|
193
|
+
entry.sourcePath === LEGACY_PROFILE_PATH,
|
|
195
194
|
legacyHash,
|
|
196
195
|
);
|
|
197
196
|
continue;
|
|
198
197
|
} else if (hiddenMatchesSource && legacyExists) {
|
|
199
|
-
const conflict = entry.sourcePath ===
|
|
198
|
+
const conflict = entry.sourcePath === LEGACY_PROFILE_PATH
|
|
200
199
|
? migrationConflict(
|
|
201
200
|
"E_PROFILE_MIGRATION_CONFLICT",
|
|
202
201
|
entry.legacyRelativePath,
|
|
@@ -214,7 +213,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
214
213
|
}
|
|
215
214
|
nextManifest.files[entry.relativePath] = manifestRecord(
|
|
216
215
|
currentHash,
|
|
217
|
-
entry.sourcePath ===
|
|
216
|
+
entry.sourcePath === LEGACY_PROFILE_PATH
|
|
218
217
|
|| !destinationRecord
|
|
219
218
|
|| Boolean(destinationRecord.preserve),
|
|
220
219
|
);
|
|
@@ -224,7 +223,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
224
223
|
if (!legacyExists) {
|
|
225
224
|
addAction(actions, dryRun, "create", entry.relativePath);
|
|
226
225
|
writes.push({ destination, bytes: entry.bytes });
|
|
227
|
-
nextManifest.files[entry.relativePath] = manifestRecord(sourceHash, entry.sourcePath ===
|
|
226
|
+
nextManifest.files[entry.relativePath] = manifestRecord(sourceHash, entry.sourcePath === LEGACY_PROFILE_PATH);
|
|
228
227
|
continue;
|
|
229
228
|
}
|
|
230
229
|
|
|
@@ -232,7 +231,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
232
231
|
const legacyHash = sha256(legacyBytes);
|
|
233
232
|
const unchangedManaged = Boolean(legacyRecord) && legacyHash === legacyRecord.sha256;
|
|
234
233
|
|
|
235
|
-
if (entry.sourcePath ===
|
|
234
|
+
if (entry.sourcePath === LEGACY_PROFILE_PATH && legacyRecord) {
|
|
236
235
|
addAction(actions, dryRun, "move-profile", entry.legacyRelativePath, { to: entry.relativePath });
|
|
237
236
|
writes.push({ destination, bytes: legacyBytes });
|
|
238
237
|
addLegacyCleanup(cleanupFiles, cleanupDirectories, entry.legacyRelativePath, legacyHash);
|
|
@@ -256,7 +255,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
256
255
|
continue;
|
|
257
256
|
}
|
|
258
257
|
|
|
259
|
-
const conflict = entry.sourcePath ===
|
|
258
|
+
const conflict = entry.sourcePath === LEGACY_PROFILE_PATH
|
|
260
259
|
? migrationConflict(
|
|
261
260
|
"E_PROFILE_MIGRATION_CONFLICT",
|
|
262
261
|
entry.legacyRelativePath,
|
|
@@ -275,7 +274,7 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
|
|
|
275
274
|
reason: legacyRecord ? "managed-modified" : "unmanaged",
|
|
276
275
|
});
|
|
277
276
|
writes.push({ destination, bytes: entry.bytes });
|
|
278
|
-
nextManifest.files[entry.relativePath] = manifestRecord(sourceHash, entry.sourcePath ===
|
|
277
|
+
nextManifest.files[entry.relativePath] = manifestRecord(sourceHash, entry.sourcePath === LEGACY_PROFILE_PATH);
|
|
279
278
|
}
|
|
280
279
|
|
|
281
280
|
// Apply all hidden writes and verify their bytes before changing manifest authority.
|
|
@@ -364,7 +363,7 @@ export async function runUpdate({ target, dryRun, packageRoot, packageVersion, h
|
|
|
364
363
|
entry,
|
|
365
364
|
record: {
|
|
366
365
|
sha256: sourceHash,
|
|
367
|
-
preserve: entry.sourcePath ===
|
|
366
|
+
preserve: entry.sourcePath === LEGACY_PROFILE_PATH,
|
|
368
367
|
...(record?.legacySha256 ? { legacySha256: record.legacySha256 } : {}),
|
|
369
368
|
},
|
|
370
369
|
});
|
|
@@ -376,7 +375,7 @@ export async function runUpdate({ target, dryRun, packageRoot, packageVersion, h
|
|
|
376
375
|
continue;
|
|
377
376
|
}
|
|
378
377
|
|
|
379
|
-
if (record.preserve || entry.sourcePath ===
|
|
378
|
+
if (record.preserve || entry.sourcePath === LEGACY_PROFILE_PATH) {
|
|
380
379
|
actions.push({ action: "skip", path: entry.relativePath, reason: "preserved" });
|
|
381
380
|
continue;
|
|
382
381
|
}
|
|
@@ -10,6 +10,8 @@ import { ARTIFACT_PATHS, readJsonArtifact } from "../core/artifacts.js";
|
|
|
10
10
|
import { evaluatePreflight, validateReadyProtocolConsistency } from "../core/preflight.js";
|
|
11
11
|
import { validateEventLedger, validateStateLedgerCoherence } from "../core/events.js";
|
|
12
12
|
import { validateChecksExecutionProvenance } from "../core/completion-artifacts.js";
|
|
13
|
+
import { assertContinuitySemantics } from "../core/continuity.js";
|
|
14
|
+
import { currentChangedPaths, currentRepositoryFingerprint } from "../core/repository.js";
|
|
13
15
|
|
|
14
16
|
async function readArtifact(target, relativePath, label) {
|
|
15
17
|
if (!relativePath) return null;
|
|
@@ -48,6 +50,7 @@ export async function runValidateProtocol({
|
|
|
48
50
|
stateFile = null,
|
|
49
51
|
receiptFile = null,
|
|
50
52
|
contractFile = null,
|
|
53
|
+
continuityFile = null,
|
|
51
54
|
taskBriefFiles = [],
|
|
52
55
|
delegatedResultFiles = [],
|
|
53
56
|
}) {
|
|
@@ -55,6 +58,7 @@ export async function runValidateProtocol({
|
|
|
55
58
|
["route", routeFile],
|
|
56
59
|
["state", stateFile],
|
|
57
60
|
["receipt", receiptFile],
|
|
61
|
+
["continuity", continuityFile],
|
|
58
62
|
...taskBriefFiles.map((file) => [`task brief:${file}`, file]),
|
|
59
63
|
...delegatedResultFiles.map((file) => [`delegated result:${file}`, file]),
|
|
60
64
|
];
|
|
@@ -68,6 +72,7 @@ export async function runValidateProtocol({
|
|
|
68
72
|
const route = loaded.find((item) => item.label === "route")?.value ?? null;
|
|
69
73
|
const state = loaded.find((item) => item.label === "state")?.value ?? null;
|
|
70
74
|
const receipt = loaded.find((item) => item.label === "receipt")?.value ?? null;
|
|
75
|
+
const continuity = loaded.find((item) => item.label === "continuity")?.value ?? null;
|
|
71
76
|
const taskBriefs = loaded.filter((item) => item.label.startsWith("task brief:") && item.value).map((item) => item.value);
|
|
72
77
|
const delegatedResults = loaded.filter((item) => item.label.startsWith("delegated result:") && item.value).map((item) => item.value);
|
|
73
78
|
const schemaErrors = [];
|
|
@@ -88,6 +93,7 @@ export async function runValidateProtocol({
|
|
|
88
93
|
await validateLoaded(loaded.find((item) => item.label === "route"), "routing-result", async (value) => assertRouteInvariants(value));
|
|
89
94
|
await validateLoaded(loaded.find((item) => item.label === "state"), "work-state", async (value) => assertWorkStateSemantics(value));
|
|
90
95
|
await validateLoaded(loaded.find((item) => item.label === "receipt"), "execution-receipt", async (value) => validateReceipt(value, packageRoot));
|
|
96
|
+
await validateLoaded(loaded.find((item) => item.label === "continuity"), "continuity", async (value) => assertContinuitySemantics(value));
|
|
91
97
|
for (const [value, artifactPath] of [
|
|
92
98
|
[state, stateFile],
|
|
93
99
|
[receipt, receiptFile],
|
|
@@ -142,11 +148,20 @@ export async function runValidateProtocol({
|
|
|
142
148
|
})),
|
|
143
149
|
];
|
|
144
150
|
}
|
|
151
|
+
const continuityContext = continuity && state && !stateValidationError
|
|
152
|
+
? {
|
|
153
|
+
contractFingerprint: state.contractFingerprint,
|
|
154
|
+
repositoryFingerprint: await currentRepositoryFingerprint(target),
|
|
155
|
+
changedPaths: await currentChangedPaths(target),
|
|
156
|
+
}
|
|
157
|
+
: {};
|
|
145
158
|
const result = validateTaskArtifactSet({
|
|
146
159
|
route,
|
|
147
160
|
state,
|
|
148
161
|
stateClassification,
|
|
149
162
|
receipt,
|
|
163
|
+
continuity,
|
|
164
|
+
continuityContext,
|
|
150
165
|
taskBriefs,
|
|
151
166
|
delegatedResults,
|
|
152
167
|
events: ledgerEvents,
|
|
@@ -164,6 +179,10 @@ export async function runValidateProtocol({
|
|
|
164
179
|
|
|
165
180
|
export function formatValidateProtocolResult(result) {
|
|
166
181
|
const lines = [`Protocol: ${result.status}`];
|
|
182
|
+
if (result.continuity) {
|
|
183
|
+
lines.push(`Continuity: ${result.continuity.status}`);
|
|
184
|
+
lines.push(`Continuity authority: ${result.continuity.authority}`);
|
|
185
|
+
}
|
|
167
186
|
if (result.stale) {
|
|
168
187
|
lines.push(`Repository: ${result.stale.repositoryComparison}`);
|
|
169
188
|
lines.push(`Contract: ${result.stale.contractComparison}`);
|
package/src/core/artifacts.js
CHANGED
|
@@ -16,6 +16,7 @@ export const ARTIFACT_PATHS = Object.freeze({
|
|
|
16
16
|
config: ".forgeloop/config.json",
|
|
17
17
|
gates: ".forgeloop/gates",
|
|
18
18
|
state: ".forgeloop/work-state.json",
|
|
19
|
+
continuity: ".forgeloop/continuity.json",
|
|
19
20
|
receipt: ".forgeloop/execution-receipt.json",
|
|
20
21
|
executionDirectory: ".forgeloop/executions",
|
|
21
22
|
});
|
package/src/core/bundles.js
CHANGED
|
@@ -6,6 +6,7 @@ import { assertSafePath, ensureWithin, fileExists, readBytes, writeFileAtomic }
|
|
|
6
6
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
7
7
|
import { validateChecksExecutionProvenance } from "./completion-artifacts.js";
|
|
8
8
|
import { readExecutionArtifact } from "./execution.js";
|
|
9
|
+
import { assertContinuitySemantics } from "./continuity.js";
|
|
9
10
|
|
|
10
11
|
export const BUNDLE_SCHEMA_VERSION = 1;
|
|
11
12
|
const BUNDLE_ROOT = ".forgeloop/tasks";
|
|
@@ -89,6 +90,7 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
|
|
|
89
90
|
[ARTIFACT_PATHS.receipt, "receipt.json", "execution-receipt"],
|
|
90
91
|
[ARTIFACT_PATHS.sources, "sources.json", "source-registry"],
|
|
91
92
|
[ARTIFACT_PATHS.config, "config.json", "config"],
|
|
93
|
+
[ARTIFACT_PATHS.continuity, "continuity.json", "continuity"],
|
|
92
94
|
];
|
|
93
95
|
for (const [sourcePath, destinationName, schemaName] of optional) {
|
|
94
96
|
const copied = await copyJson(target, sourcePath, `${directory}/${destinationName}`, schemaName, packageRoot, artifacts, destinationName);
|
|
@@ -146,6 +148,7 @@ export async function readTaskBundle(target, taskId, packageRoot) {
|
|
|
146
148
|
"receipt.json": ["receipt", "execution-receipt"],
|
|
147
149
|
"sources.json": ["sources", "source-registry"],
|
|
148
150
|
"config.json": ["config", "config"],
|
|
151
|
+
"continuity.json": ["continuity", "continuity"],
|
|
149
152
|
};
|
|
150
153
|
const executions = {};
|
|
151
154
|
for (const artifact of manifest.value.artifacts) {
|
|
@@ -160,6 +163,9 @@ export async function readTaskBundle(target, taskId, packageRoot) {
|
|
|
160
163
|
if (mapping[1] === "current-contract") {
|
|
161
164
|
await validateContract(loadedArtifact.value, packageRoot);
|
|
162
165
|
}
|
|
166
|
+
if (mapping[1] === "continuity") {
|
|
167
|
+
assertContinuitySemantics(loadedArtifact.value);
|
|
168
|
+
}
|
|
163
169
|
loaded[mapping[0]] = loadedArtifact.value;
|
|
164
170
|
}
|
|
165
171
|
if (Object.keys(executions).length > 0) loaded.executions = executions;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
extractToolFromArgs,
|
|
6
|
+
normalizeExecutableName,
|
|
7
|
+
splitCommandPipeline,
|
|
8
|
+
tokenizeCommand,
|
|
9
|
+
} from "./command-tokenizer.js";
|
|
10
|
+
import {
|
|
11
|
+
classifyNpmInvocation,
|
|
12
|
+
getNpmLifecycleCandidates,
|
|
13
|
+
getNpmScriptName,
|
|
14
|
+
parseNpmInvocation,
|
|
15
|
+
parseNpmInvocationArgs,
|
|
16
|
+
} from "./npm-classifier.js";
|
|
17
|
+
import {
|
|
18
|
+
classifyPnpmInvocation,
|
|
19
|
+
classifyYarnInvocation,
|
|
20
|
+
} from "./package-manager-classifiers.js";
|
|
21
|
+
|
|
22
|
+
function classifySingleCommand(commandInput) {
|
|
23
|
+
const tokens = Array.isArray(commandInput) ? [...commandInput] : tokenizeCommand(commandInput);
|
|
24
|
+
if (tokens.length === 0) return { resolutionMode: "UNKNOWN", mayInstall: false, installer: null, tool: null };
|
|
25
|
+
|
|
26
|
+
let i = 0;
|
|
27
|
+
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) i++;
|
|
28
|
+
if (i >= tokens.length) return { resolutionMode: "UNKNOWN", mayInstall: false, installer: null, tool: null };
|
|
29
|
+
|
|
30
|
+
const binaryToken = tokens[i];
|
|
31
|
+
const binary = normalizeExecutableName(binaryToken);
|
|
32
|
+
const rest = tokens.slice(i + 1);
|
|
33
|
+
|
|
34
|
+
if (binary === "call") return classifyCommandResolution(rest);
|
|
35
|
+
|
|
36
|
+
if (binaryToken.includes("node_modules/.bin/") || binaryToken.startsWith("./node_modules/")) {
|
|
37
|
+
return { resolutionMode: "LOCAL_PACKAGE_BINARY", mayInstall: false, installer: null, tool: binary };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (binary === "npx") {
|
|
41
|
+
if (rest.some((arg) => arg === "--no-install" || arg === "--no")) {
|
|
42
|
+
const nonInstallArgs = rest.filter((arg) => arg !== "--no-install" && arg !== "--no");
|
|
43
|
+
return {
|
|
44
|
+
resolutionMode: "NON_INSTALLING_RESOLUTION",
|
|
45
|
+
mayInstall: false,
|
|
46
|
+
installer: "npx",
|
|
47
|
+
tool: extractToolFromArgs(nonInstallArgs),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
52
|
+
mayInstall: true,
|
|
53
|
+
installer: "npx",
|
|
54
|
+
tool: extractToolFromArgs(rest),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (binary === "pnpx" || binary === "bunx" || binary === "uvx") {
|
|
59
|
+
return {
|
|
60
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
61
|
+
mayInstall: true,
|
|
62
|
+
installer: binary,
|
|
63
|
+
tool: extractToolFromArgs(rest),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (binary === "pnpm") return classifyPnpmInvocation(rest);
|
|
68
|
+
if (binary === "yarn") return classifyYarnInvocation(rest);
|
|
69
|
+
|
|
70
|
+
if (binary === "bun") {
|
|
71
|
+
if (rest[0] === "x") {
|
|
72
|
+
return {
|
|
73
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
74
|
+
mayInstall: true,
|
|
75
|
+
installer: "bun x",
|
|
76
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (["add", "install", "i"].includes(rest[0])) {
|
|
80
|
+
return {
|
|
81
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
82
|
+
mayInstall: true,
|
|
83
|
+
installer: "bun",
|
|
84
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { resolutionMode: "LOCAL_PACKAGE_BINARY", mayInstall: false, installer: null, tool: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (binary === "uv") {
|
|
91
|
+
if (rest[0] === "tool" && rest[1] === "run") {
|
|
92
|
+
return {
|
|
93
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
94
|
+
mayInstall: true,
|
|
95
|
+
installer: "uv tool run",
|
|
96
|
+
tool: extractToolFromArgs(rest.slice(2)),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (rest[0] === "pip" && rest[1] === "install") {
|
|
100
|
+
return {
|
|
101
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
102
|
+
mayInstall: true,
|
|
103
|
+
installer: "uv pip install",
|
|
104
|
+
tool: extractToolFromArgs(rest.slice(2)),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (rest[0] === "add") {
|
|
108
|
+
return {
|
|
109
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
110
|
+
mayInstall: true,
|
|
111
|
+
installer: "uv add",
|
|
112
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return { resolutionMode: "LOCAL_EXECUTABLE", mayInstall: false, installer: null, tool: null };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (binary === "pipx") {
|
|
119
|
+
if (rest[0] === "run") {
|
|
120
|
+
return {
|
|
121
|
+
resolutionMode: "INSTALL_CAPABLE_RESOLUTION",
|
|
122
|
+
mayInstall: true,
|
|
123
|
+
installer: "pipx run",
|
|
124
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (rest[0] === "install") {
|
|
128
|
+
return {
|
|
129
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
130
|
+
mayInstall: true,
|
|
131
|
+
installer: "pipx install",
|
|
132
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { resolutionMode: "LOCAL_EXECUTABLE", mayInstall: false, installer: null, tool: null };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (binary === "npm") return classifyNpmInvocation(parseNpmInvocationArgs(rest));
|
|
139
|
+
|
|
140
|
+
if (binary === "pip" || binary === "pip3") {
|
|
141
|
+
if (rest[0] === "install") {
|
|
142
|
+
return {
|
|
143
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
144
|
+
mayInstall: true,
|
|
145
|
+
installer: binary,
|
|
146
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (binary === "python" || binary === "python3") {
|
|
151
|
+
if (rest[0] === "-m" && rest[1] === "pip" && rest[2] === "install") {
|
|
152
|
+
return {
|
|
153
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
154
|
+
mayInstall: true,
|
|
155
|
+
installer: `${binary} -m pip install`,
|
|
156
|
+
tool: extractToolFromArgs(rest.slice(3)),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (binary === "cargo") {
|
|
162
|
+
if (rest[0] === "install" || rest[0] === "binstall") {
|
|
163
|
+
return {
|
|
164
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
165
|
+
mayInstall: true,
|
|
166
|
+
installer: binary,
|
|
167
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (["brew", "apt", "apt-get", "apk", "dnf", "pacman"].includes(binary)) {
|
|
173
|
+
if (["install", "add", "-S"].includes(rest[0])) {
|
|
174
|
+
return {
|
|
175
|
+
resolutionMode: "EXPLICIT_INSTALLATION",
|
|
176
|
+
mayInstall: true,
|
|
177
|
+
installer: binary,
|
|
178
|
+
tool: extractToolFromArgs(rest.slice(1)),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { resolutionMode: "LOCAL_EXECUTABLE", mayInstall: false, installer: null, tool: null };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function classifyCommandResolution(commandInput) {
|
|
187
|
+
if (Array.isArray(commandInput)) {
|
|
188
|
+
if (commandInput.length === 0 || commandInput.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
189
|
+
return { resolutionMode: "UNKNOWN", mayInstall: false, installer: null, tool: null };
|
|
190
|
+
}
|
|
191
|
+
const binary = normalizeExecutableName(commandInput[0]);
|
|
192
|
+
if (["sh", "bash", "zsh", "dash", "ksh"].includes(binary)) {
|
|
193
|
+
const shellFlagIndex = commandInput.findIndex((item, index) => index > 0 && /^-.*c/.test(item));
|
|
194
|
+
const shellCommand = shellFlagIndex >= 0 ? commandInput[shellFlagIndex + 1] : null;
|
|
195
|
+
if (shellCommand) return classifyCommandResolution(shellCommand);
|
|
196
|
+
}
|
|
197
|
+
if (binary === "cmd") {
|
|
198
|
+
const shellFlagIndex = commandInput.findIndex((item, index) => index > 0 && /^\/c$/iu.test(item));
|
|
199
|
+
const shellCommand = shellFlagIndex >= 0 ? commandInput.slice(shellFlagIndex + 1).join(" ") : null;
|
|
200
|
+
if (shellCommand) return classifyCommandResolution(shellCommand);
|
|
201
|
+
}
|
|
202
|
+
return classifySingleCommand(commandInput);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (typeof commandInput !== "string" || commandInput.trim() === "") {
|
|
206
|
+
return { resolutionMode: "UNKNOWN", mayInstall: false, installer: null, tool: null };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const subcommands = splitCommandPipeline(commandInput);
|
|
210
|
+
for (const subcommand of subcommands) {
|
|
211
|
+
const result = classifySingleCommand(subcommand);
|
|
212
|
+
if (result.mayInstall) return result;
|
|
213
|
+
}
|
|
214
|
+
return classifySingleCommand(subcommands[0] || commandInput);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export const MAX_NPM_SCRIPT_DEPTH = 16;
|
|
218
|
+
|
|
219
|
+
async function resolveNpmScriptRisk({ scriptName, packageJson, visited = new Set(), depth = 0 } = {}) {
|
|
220
|
+
if (depth > MAX_NPM_SCRIPT_DEPTH) {
|
|
221
|
+
return { resolutionMode: "UNKNOWN", mayInstall: true, installer: "npm-script", tool: null, reason: "MAX_SCRIPT_DEPTH" };
|
|
222
|
+
}
|
|
223
|
+
if (visited.has(scriptName)) {
|
|
224
|
+
return { resolutionMode: "UNKNOWN", mayInstall: true, installer: "npm-script", tool: null, reason: "SCRIPT_CYCLE" };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const nextVisited = new Set(visited);
|
|
228
|
+
nextVisited.add(scriptName);
|
|
229
|
+
const candidates = getNpmLifecycleCandidates({ scriptName, scripts: packageJson?.scripts ?? {} });
|
|
230
|
+
for (const candidate of candidates) {
|
|
231
|
+
const scriptBody = packageJson?.scripts?.[candidate];
|
|
232
|
+
if (typeof scriptBody !== "string" || scriptBody.trim() === "") continue;
|
|
233
|
+
|
|
234
|
+
const direct = classifyCommandResolution(scriptBody);
|
|
235
|
+
if (direct.mayInstall) {
|
|
236
|
+
return {
|
|
237
|
+
resolutionMode: direct.resolutionMode,
|
|
238
|
+
mayInstall: true,
|
|
239
|
+
installer: direct.installer ?? "npm-script",
|
|
240
|
+
tool: direct.tool,
|
|
241
|
+
dispatch: { kind: "npm-script", scriptName: candidate },
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for (const subcommand of splitCommandPipeline(scriptBody)) {
|
|
246
|
+
const nestedScriptName = getNpmScriptName(subcommand);
|
|
247
|
+
if (!nestedScriptName) continue;
|
|
248
|
+
const nested = await resolveNpmScriptRisk({
|
|
249
|
+
scriptName: nestedScriptName,
|
|
250
|
+
packageJson,
|
|
251
|
+
visited: nextVisited,
|
|
252
|
+
depth: depth + 1,
|
|
253
|
+
});
|
|
254
|
+
if (nested?.mayInstall) return nested;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function readPackageJsonIfPresent(cwd) {
|
|
261
|
+
if (!cwd || typeof cwd !== "string") return null;
|
|
262
|
+
try {
|
|
263
|
+
const raw = await fs.readFile(path.resolve(cwd, "package.json"), "utf8");
|
|
264
|
+
return JSON.parse(raw);
|
|
265
|
+
} catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function resolveExecutionResolution({ argv, cwd } = {}) {
|
|
271
|
+
const direct = classifyCommandResolution(argv);
|
|
272
|
+
if (direct.mayInstall && direct.resolutionMode !== "UNKNOWN") return direct;
|
|
273
|
+
|
|
274
|
+
const npmInvocation = parseNpmInvocation(argv);
|
|
275
|
+
const scriptName = getNpmScriptName(argv);
|
|
276
|
+
if (scriptName && (npmInvocation.workspace || npmInvocation.workspaces)) {
|
|
277
|
+
return {
|
|
278
|
+
resolutionMode: "UNKNOWN",
|
|
279
|
+
mayInstall: true,
|
|
280
|
+
installer: "npm-workspace",
|
|
281
|
+
tool: null,
|
|
282
|
+
reason: "NPM_WORKSPACE_SCRIPT_UNRESOLVED",
|
|
283
|
+
dispatch: { kind: "npm-workspace-script", scriptName },
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (direct.mayInstall) return direct;
|
|
288
|
+
if (!scriptName) return direct;
|
|
289
|
+
|
|
290
|
+
const packageJson = await readPackageJsonIfPresent(cwd);
|
|
291
|
+
if (!packageJson || typeof packageJson.scripts !== "object" || packageJson.scripts === null) return direct;
|
|
292
|
+
|
|
293
|
+
const nestedRisk = await resolveNpmScriptRisk({ scriptName, packageJson });
|
|
294
|
+
return nestedRisk?.mayInstall ? nestedRisk : direct;
|
|
295
|
+
}
|