@cassiomc1/forgeloop 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.forgeloop/forgeloop.gitignore +1 -0
  2. package/DOCS_INDEX.md +60 -0
  3. package/EXECUTION_STATE.md +9 -0
  4. package/LOOP_ENGINEERING.md +44 -6
  5. package/LOOP_SYSTEM_DESIGN.md +8 -0
  6. package/PROTOCOL_INTEGRATION.md +41 -0
  7. package/QUALITY_SCORECARD.md +2 -0
  8. package/README.md +183 -643
  9. package/TERMINOLOGY.md +4 -0
  10. package/THREAT_MODEL.md +14 -0
  11. package/docs/assets/forgeloop-flow.svg +1 -0
  12. package/docs/forgeloop-flow.mmd +51 -0
  13. package/package.json +16 -3
  14. package/schemas/check.schema.json +2 -0
  15. package/schemas/continuity.schema.json +57 -0
  16. package/schemas/execution.schema.json +63 -0
  17. package/scripts/CI_VALIDATORS.md +32 -0
  18. package/src/cli.js +351 -196
  19. package/src/commands/clear-continuity.js +9 -0
  20. package/src/commands/continuity.js +25 -0
  21. package/src/commands/doctor.js +17 -2
  22. package/src/commands/reconcile-continuity.js +23 -0
  23. package/src/commands/record-continuity.js +63 -0
  24. package/src/commands/run-check.js +83 -0
  25. package/src/commands/status.js +14 -1
  26. package/src/commands/update.js +12 -13
  27. package/src/commands/validate-protocol.js +37 -0
  28. package/src/core/artifacts.js +9 -0
  29. package/src/core/bundles.js +78 -0
  30. package/src/core/checks.js +16 -0
  31. package/src/core/command-resolution.js +295 -0
  32. package/src/core/command-tokenizer.js +122 -0
  33. package/src/core/completion-artifacts.js +229 -47
  34. package/src/core/completion.js +19 -1
  35. package/src/core/conformance.js +8 -2
  36. package/src/core/continuity-cli-options.js +58 -0
  37. package/src/core/continuity-conformance.js +46 -0
  38. package/src/core/continuity-observability.js +20 -0
  39. package/src/core/continuity-reconciliation.js +224 -0
  40. package/src/core/continuity.js +245 -0
  41. package/src/core/evidence-readiness.js +26 -1
  42. package/src/core/execution.js +185 -0
  43. package/src/core/inspect.js +9 -1
  44. package/src/core/installation-authority.js +178 -0
  45. package/src/core/json-safety.js +17 -13
  46. package/src/core/next-action-artifacts.js +118 -0
  47. package/src/core/next-action-continuity.js +65 -0
  48. package/src/core/next-action-model.js +127 -0
  49. package/src/core/next-action-phases.js +12 -0
  50. package/src/core/next-action.js +22 -249
  51. package/src/core/npm-classifier.js +343 -0
  52. package/src/core/package-manager-classifiers.js +37 -0
  53. package/src/core/preflight-consistency.js +221 -0
  54. package/src/core/preflight-loaders.js +112 -0
  55. package/src/core/preflight-model.js +83 -0
  56. package/src/core/preflight.js +34 -444
  57. package/src/core/protocol.js +7 -0
  58. package/src/core/schema-validation.js +16 -1
  59. package/src/core/templates.js +3 -0
  60. package/src/core/verification-capability.js +31 -495
  61. package/src/core/verification-constants.js +61 -0
@@ -0,0 +1,9 @@
1
+ import { clearContinuity } from "../core/continuity.js";
2
+
3
+ export async function runClearContinuity({ target } = {}) {
4
+ return clearContinuity(target);
5
+ }
6
+
7
+ export function formatClearContinuityResult(result) {
8
+ return `${result.removed ? "cleared" : "absent"}: ${result.path}\n`;
9
+ }
@@ -0,0 +1,25 @@
1
+ import { reconcileContinuity } from "../core/continuity-reconciliation.js";
2
+
3
+ export async function runContinuity({ target, packageRoot } = {}) {
4
+ return reconcileContinuity({ target, packageRoot });
5
+ }
6
+
7
+ export function formatContinuityResult(result) {
8
+ const value = result.continuity;
9
+ const lines = [
10
+ "FORGELOOP EXECUTION CONTINUITY",
11
+ "",
12
+ `classification: ${result.classification}`,
13
+ `task: ${value?.taskId ?? "none"}`,
14
+ `phase: ${value?.phase ?? "none"}`,
15
+ `current focus: ${value?.currentFocus?.id ?? "none"}`,
16
+ `remaining work: ${value?.remainingWork?.length ?? 0}`,
17
+ `known issues: ${value?.knownIssues?.length ?? 0}`,
18
+ `inspect first: ${value?.inspectFirst?.join(", ") || "none"}`,
19
+ "",
20
+ "Authority: OPERATIONAL_CONTEXT_ONLY",
21
+ "Evidence: NONE",
22
+ ];
23
+ if (result.reasons?.length) lines.push(`Reasons: ${result.reasons.join(", ")}`);
24
+ return `${lines.join("\n")}\n`;
25
+ }
@@ -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
+ }
@@ -0,0 +1,83 @@
1
+ import {
2
+ assertRecordCheckPrerequisites,
3
+ recordCheck as recordCheckArtifact,
4
+ } from "../core/completion-artifacts.js";
5
+ import { runCommandExecution } from "../core/execution.js";
6
+
7
+ export async function runCheck({
8
+ target,
9
+ packageRoot,
10
+ id,
11
+ requirement,
12
+ argv,
13
+ details,
14
+ authorityContext,
15
+ runtimeContext,
16
+ }) {
17
+ if (typeof id !== "string" || id.trim() === "" || typeof requirement !== "string" || requirement.trim() === "") {
18
+ const error = new Error("run-check requires non-empty id and requirement");
19
+ error.code = "E_CHECK_INVALID";
20
+ throw error;
21
+ }
22
+ const ready = await assertRecordCheckPrerequisites({
23
+ target,
24
+ packageRoot,
25
+ requirement,
26
+ status: "passed",
27
+ evidenceKind: "OBSERVED",
28
+ authorityContext,
29
+ runtimeContext,
30
+ });
31
+ const verificationCycle = ready.state.verificationCycle ?? 1;
32
+ const execution = await runCommandExecution({
33
+ target,
34
+ packageRoot,
35
+ taskId: ready.contract.value.taskId,
36
+ checkId: id,
37
+ requirement,
38
+ verificationCycle,
39
+ argv,
40
+ details,
41
+ authorityContext,
42
+ runtimeContext,
43
+ });
44
+ const status = execution.execution.status === "passed" ? "passed" : "failed";
45
+ const recorded = await recordCheckArtifact({
46
+ target,
47
+ packageRoot,
48
+ id,
49
+ kind: "command",
50
+ requirement,
51
+ status,
52
+ evidenceKind: "OBSERVED",
53
+ command: execution.execution.argv.join(" "),
54
+ result: execution.result,
55
+ ...(execution.execution.exitCode === null ? {} : { exitCode: execution.execution.exitCode }),
56
+ details,
57
+ executionRef: execution.execution.executionId,
58
+ provenance: "FORGELOOP_EXECUTED",
59
+ authorityContext,
60
+ runtimeContext,
61
+ });
62
+ return {
63
+ ...recorded,
64
+ execution: execution.execution,
65
+ executionPath: execution.path,
66
+ };
67
+ }
68
+
69
+ export function formatRunCheckResult(result) {
70
+ return [
71
+ "FORGELOOP CHECK EXECUTED",
72
+ `id: ${result.check.id}`,
73
+ `requirement: ${result.check.requirement}`,
74
+ `status: ${result.check.status}`,
75
+ `execution: ${result.execution.executionId}`,
76
+ `argv: ${result.execution.argv.join(" ")}`,
77
+ `exit code: ${result.execution.exitCode ?? "not-started"}`,
78
+ `artifact: ${result.executionPath}`,
79
+ `coverage: ${result.coverage.find((item) => item.requirement === result.check.requirement)?.status ?? "NOT_VERIFIED"}`,
80
+ `receipt: ${result.path}`,
81
+ "",
82
+ ].join("\n");
83
+ }
@@ -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 inspectSchemaHealth(target);
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
  }
@@ -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 === PROFILE_PATH
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 !== PROFILE_PATH) {
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 === PROFILE_PATH,
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 === PROFILE_PATH
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 === PROFILE_PATH
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 === PROFILE_PATH);
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 === PROFILE_PATH && legacyRecord) {
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 === PROFILE_PATH
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 === PROFILE_PATH);
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 === PROFILE_PATH,
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 === PROFILE_PATH) {
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
  }
@@ -9,6 +9,9 @@ import { validateTaskBrief, validateDelegatedResult } from "../core/delegation.j
9
9
  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
+ import { validateChecksExecutionProvenance } from "../core/completion-artifacts.js";
13
+ import { assertContinuitySemantics } from "../core/continuity.js";
14
+ import { currentChangedPaths, currentRepositoryFingerprint } from "../core/repository.js";
12
15
 
13
16
  async function readArtifact(target, relativePath, label) {
14
17
  if (!relativePath) return null;
@@ -47,6 +50,7 @@ export async function runValidateProtocol({
47
50
  stateFile = null,
48
51
  receiptFile = null,
49
52
  contractFile = null,
53
+ continuityFile = null,
50
54
  taskBriefFiles = [],
51
55
  delegatedResultFiles = [],
52
56
  }) {
@@ -54,6 +58,7 @@ export async function runValidateProtocol({
54
58
  ["route", routeFile],
55
59
  ["state", stateFile],
56
60
  ["receipt", receiptFile],
61
+ ["continuity", continuityFile],
57
62
  ...taskBriefFiles.map((file) => [`task brief:${file}`, file]),
58
63
  ...delegatedResultFiles.map((file) => [`delegated result:${file}`, file]),
59
64
  ];
@@ -67,6 +72,7 @@ export async function runValidateProtocol({
67
72
  const route = loaded.find((item) => item.label === "route")?.value ?? null;
68
73
  const state = loaded.find((item) => item.label === "state")?.value ?? null;
69
74
  const receipt = loaded.find((item) => item.label === "receipt")?.value ?? null;
75
+ const continuity = loaded.find((item) => item.label === "continuity")?.value ?? null;
70
76
  const taskBriefs = loaded.filter((item) => item.label.startsWith("task brief:") && item.value).map((item) => item.value);
71
77
  const delegatedResults = loaded.filter((item) => item.label.startsWith("delegated result:") && item.value).map((item) => item.value);
72
78
  const schemaErrors = [];
@@ -87,6 +93,24 @@ export async function runValidateProtocol({
87
93
  await validateLoaded(loaded.find((item) => item.label === "route"), "routing-result", async (value) => assertRouteInvariants(value));
88
94
  await validateLoaded(loaded.find((item) => item.label === "state"), "work-state", async (value) => assertWorkStateSemantics(value));
89
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));
97
+ for (const [value, artifactPath] of [
98
+ [state, stateFile],
99
+ [receipt, receiptFile],
100
+ ]) {
101
+ if (!value) continue;
102
+ const provenanceErrors = await validateChecksExecutionProvenance(value.checks, {
103
+ target,
104
+ packageRoot,
105
+ taskId: value.taskId,
106
+ artifactPath,
107
+ });
108
+ schemaErrors.push(...provenanceErrors.map((error) => ({
109
+ ...error,
110
+ message: `Command provenance validation failed: ${error.message}`,
111
+ artifacts: [artifactPath, ...(error.artifacts ?? [])],
112
+ })));
113
+ }
90
114
  for (const item of loaded.filter((candidate) => candidate.label.startsWith("task brief:"))) {
91
115
  await validateLoaded(item, "task-brief", async (value) => validateTaskBrief(value, packageRoot));
92
116
  }
@@ -124,11 +148,20 @@ export async function runValidateProtocol({
124
148
  })),
125
149
  ];
126
150
  }
151
+ const continuityContext = continuity && state && !stateValidationError
152
+ ? {
153
+ contractFingerprint: state.contractFingerprint,
154
+ repositoryFingerprint: await currentRepositoryFingerprint(target),
155
+ changedPaths: await currentChangedPaths(target),
156
+ }
157
+ : {};
127
158
  const result = validateTaskArtifactSet({
128
159
  route,
129
160
  state,
130
161
  stateClassification,
131
162
  receipt,
163
+ continuity,
164
+ continuityContext,
132
165
  taskBriefs,
133
166
  delegatedResults,
134
167
  events: ledgerEvents,
@@ -146,6 +179,10 @@ export async function runValidateProtocol({
146
179
 
147
180
  export function formatValidateProtocolResult(result) {
148
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
+ }
149
186
  if (result.stale) {
150
187
  lines.push(`Repository: ${result.stale.repositoryComparison}`);
151
188
  lines.push(`Contract: ${result.stale.contractComparison}`);
@@ -16,9 +16,18 @@ 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",
21
+ executionDirectory: ".forgeloop/executions",
20
22
  });
21
23
 
24
+ export function executionArtifactPath(executionId) {
25
+ if (typeof executionId !== "string" || !/^exec-[A-Za-z0-9_-]+$/.test(executionId)) {
26
+ throw new ArtifactError("E_EXECUTION_REF_INVALID", "Execution reference must be a simple execution ID");
27
+ }
28
+ return `${ARTIFACT_PATHS.executionDirectory}/${executionId}.json`;
29
+ }
30
+
22
31
  export class ArtifactError extends Error {
23
32
  constructor(code, message, artifacts = []) {
24
33
  super(message);
@@ -4,6 +4,9 @@ import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts
4
4
  import { readContract, validateContract } from "./contract.js";
5
5
  import { assertSafePath, ensureWithin, fileExists, readBytes, writeFileAtomic } from "./filesystem.js";
6
6
  import { PROTOCOL_VERSION } from "./protocol.js";
7
+ import { validateChecksExecutionProvenance } from "./completion-artifacts.js";
8
+ import { readExecutionArtifact } from "./execution.js";
9
+ import { assertContinuitySemantics } from "./continuity.js";
7
10
 
8
11
  export const BUNDLE_SCHEMA_VERSION = 1;
9
12
  const BUNDLE_ROOT = ".forgeloop/tasks";
@@ -37,6 +40,34 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
37
40
  safeTaskId(taskId);
38
41
  const directory = bundleDirectory(taskId);
39
42
  const artifacts = [];
43
+ const stateSource = await readJsonArtifact(target, ARTIFACT_PATHS.state, "work-state", packageRoot);
44
+ let receiptSource = null;
45
+ try {
46
+ receiptSource = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
47
+ } catch (error) {
48
+ if (error.code !== "ARTIFACT_MISSING") throw error;
49
+ }
50
+ const provenanceErrors = [
51
+ ...(await validateChecksExecutionProvenance(stateSource.value.checks, {
52
+ target,
53
+ packageRoot,
54
+ taskId,
55
+ artifactPath: ARTIFACT_PATHS.state,
56
+ })),
57
+ ...(await validateChecksExecutionProvenance(receiptSource?.value?.checks, {
58
+ target,
59
+ packageRoot,
60
+ taskId,
61
+ artifactPath: ARTIFACT_PATHS.receipt,
62
+ })),
63
+ ];
64
+ if (provenanceErrors.length > 0) {
65
+ const first = provenanceErrors[0];
66
+ const error = new Error(first.message);
67
+ error.code = first.code;
68
+ error.artifacts = first.artifacts;
69
+ throw error;
70
+ }
40
71
  const required = [
41
72
  [ARTIFACT_PATHS.contract, "contract.json", "current-contract"],
42
73
  [ARTIFACT_PATHS.route, "route.json", "routing-result"],
@@ -59,11 +90,22 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
59
90
  [ARTIFACT_PATHS.receipt, "receipt.json", "execution-receipt"],
60
91
  [ARTIFACT_PATHS.sources, "sources.json", "source-registry"],
61
92
  [ARTIFACT_PATHS.config, "config.json", "config"],
93
+ [ARTIFACT_PATHS.continuity, "continuity.json", "continuity"],
62
94
  ];
63
95
  for (const [sourcePath, destinationName, schemaName] of optional) {
64
96
  const copied = await copyJson(target, sourcePath, `${directory}/${destinationName}`, schemaName, packageRoot, artifacts, destinationName);
65
97
  if (copied && !artifacts.includes(destinationName)) artifacts.push(destinationName);
66
98
  }
99
+ const executionRefs = [...new Set([
100
+ ...(stateSource.value.checks ?? []),
101
+ ...(receiptSource?.value?.checks ?? []),
102
+ ].map((check) => check?.executionRef).filter(Boolean))].sort();
103
+ for (const executionRef of executionRefs) {
104
+ const execution = await readExecutionArtifact({ target, executionRef, packageRoot });
105
+ const destination = `${directory}/executions/${execution.value.executionId}.json`;
106
+ await writeJsonArtifact(target, destination, execution.value, "execution", packageRoot);
107
+ artifacts.push(`executions/${execution.value.executionId}.json`);
108
+ }
67
109
  const eventsPath = ensureWithin(target, ARTIFACT_PATHS.events);
68
110
  if (await fileExists(eventsPath)) {
69
111
  await assertSafePath(target, `${directory}/events.ndjson`);
@@ -106,15 +148,51 @@ export async function readTaskBundle(target, taskId, packageRoot) {
106
148
  "receipt.json": ["receipt", "execution-receipt"],
107
149
  "sources.json": ["sources", "source-registry"],
108
150
  "config.json": ["config", "config"],
151
+ "continuity.json": ["continuity", "continuity"],
109
152
  };
153
+ const executions = {};
110
154
  for (const artifact of manifest.value.artifacts) {
155
+ if (artifact.startsWith("executions/") && artifact.endsWith(".json")) {
156
+ const execution = await readJsonArtifact(target, `${directory}/${artifact}`, "execution", packageRoot);
157
+ executions[execution.value.executionId] = execution.value;
158
+ continue;
159
+ }
111
160
  const mapping = mappings[artifact];
112
161
  if (!mapping) continue;
113
162
  const loadedArtifact = await readJsonArtifact(target, `${directory}/${artifact}`, mapping[1], packageRoot);
114
163
  if (mapping[1] === "current-contract") {
115
164
  await validateContract(loadedArtifact.value, packageRoot);
116
165
  }
166
+ if (mapping[1] === "continuity") {
167
+ assertContinuitySemantics(loadedArtifact.value);
168
+ }
117
169
  loaded[mapping[0]] = loadedArtifact.value;
118
170
  }
171
+ if (Object.keys(executions).length > 0) loaded.executions = executions;
172
+ const provenanceErrors = [
173
+ ...(await validateChecksExecutionProvenance(loaded.state?.checks, {
174
+ target,
175
+ packageRoot,
176
+ taskId,
177
+ executionArtifacts: executions,
178
+ allowForeignCwd: true,
179
+ artifactPath: "state.json",
180
+ })),
181
+ ...(await validateChecksExecutionProvenance(loaded.receipt?.checks, {
182
+ target,
183
+ packageRoot,
184
+ taskId,
185
+ executionArtifacts: executions,
186
+ allowForeignCwd: true,
187
+ artifactPath: "receipt.json",
188
+ })),
189
+ ];
190
+ if (provenanceErrors.length > 0) {
191
+ const first = provenanceErrors[0];
192
+ const error = new Error(first.message);
193
+ error.code = first.code;
194
+ error.artifacts = first.artifacts;
195
+ throw error;
196
+ }
119
197
  return { manifest: manifest.value, artifacts: loaded };
120
198
  }
@@ -4,6 +4,7 @@ import { validateVerificationAuthority } from "./verification-capability.js";
4
4
  export const CHECK_SCHEMA_VERSION = 1;
5
5
  export const CHECK_STATUSES = Object.freeze(["passed", "failed", "blocked", "not-run"]);
6
6
  export const CHECK_EVIDENCE_KINDS = Object.freeze(["OBSERVED", "INFERRED", "NOT_VERIFIED", "BLOCKED"]);
7
+ export const CHECK_PROVENANCE = Object.freeze(["FORGELOOP_EXECUTED", "ACTOR_REPORTED", "MANUAL_OBSERVATION"]);
7
8
 
8
9
  function checkError(code, message, artifacts = []) {
9
10
  const error = new Error(message);
@@ -60,6 +61,8 @@ export function createCheck(input = {}, options = {}) {
60
61
  ...(input.timestamp !== undefined ? { timestamp: input.timestamp } : {}),
61
62
  ...(input.repositoryFingerprint !== undefined ? { repositoryFingerprint: input.repositoryFingerprint } : {}),
62
63
  ...(input.viewport !== undefined ? { viewport: structuredClone(input.viewport) } : {}),
64
+ ...(input.executionRef !== undefined ? { executionRef: input.executionRef } : {}),
65
+ ...(input.provenance !== undefined ? { provenance: input.provenance } : {}),
63
66
  ...(input.details !== undefined ? { details: structuredClone(input.details) } : {}),
64
67
  };
65
68
  return assertCheck(check, "check", options);
@@ -76,6 +79,19 @@ export function assertCheck(value, label = "check", options = {}) {
76
79
  string(value.kind, `${label}.kind`);
77
80
  string(value.requirement, `${label}.requirement`);
78
81
  string(value.source, `${label}.source`);
82
+ optionalString(value.executionRef, `${label}.executionRef`);
83
+ if (value.provenance !== undefined && !CHECK_PROVENANCE.includes(value.provenance)) {
84
+ throw checkError("E_CHECK_INVALID", `${label}.provenance must be one of ${CHECK_PROVENANCE.join(", ")}`);
85
+ }
86
+ if (options.requireCommandProvenance === true
87
+ && value.kind === "command"
88
+ && value.evidenceKind === "OBSERVED"
89
+ && (value.executionRef === undefined || value.provenance !== "FORGELOOP_EXECUTED")) {
90
+ throw checkError(
91
+ "E_COMMAND_PROVENANCE_UNATTESTED",
92
+ `${label} observed command evidence requires ForgeLoop execution provenance`,
93
+ );
94
+ }
79
95
  if (!CHECK_STATUSES.includes(value.status)) {
80
96
  throw checkError("E_CHECK_INVALID", `${label}.status must be one of ${CHECK_STATUSES.join(", ")}`);
81
97
  }