@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,224 @@
1
+ import { canonicalFingerprint } from "./artifacts.js";
2
+ import { assertContinuitySemantics, readContinuity } from "./continuity.js";
3
+ import { WORK_TRANSITIONS } from "./protocol.js";
4
+
5
+ const RECONCILIATION_CODE = "E_CONTINUITY_RECONCILIATION_REQUIRED";
6
+
7
+ function uniqueSorted(values) {
8
+ return [...new Set(values.filter(Boolean))].sort();
9
+ }
10
+
11
+ function pathCoveredByHint(changedPath, hint) {
12
+ const normalizedPath = String(changedPath).replaceAll("\\", "/").replace(/^\.\//, "");
13
+ const normalizedHint = String(hint).replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
14
+ return normalizedPath === normalizedHint || normalizedPath.startsWith(`${normalizedHint}/`);
15
+ }
16
+
17
+ function compareChangedPaths(changedPaths, hints) {
18
+ if (!Array.isArray(changedPaths)) return "NOT_VERIFIED";
19
+ if (!Array.isArray(hints) || hints.length === 0) return changedPaths.length === 0 ? "MATCH" : "MISMATCH";
20
+ const matched = hints.filter((hint) => changedPaths.some((changedPath) => pathCoveredByHint(changedPath, hint))).length;
21
+ if (matched === hints.length) return "MATCH";
22
+ if (matched > 0) return "PARTIAL";
23
+ return "MISMATCH";
24
+ }
25
+
26
+ function compareRepository(saved, current) {
27
+ const savedUnavailable = !saved || (saved.branch === null && saved.head === null);
28
+ const currentUnavailable = !current || (current.branch === null && current.head === null);
29
+ if (savedUnavailable && currentUnavailable) return "NOT_VERIFIED";
30
+ if (!saved || !current) return "NOT_VERIFIED";
31
+ return saved.branch === current.branch && saved.head === current.head ? "MATCH" : "MISMATCH";
32
+ }
33
+
34
+ function phaseReachable(from, to) {
35
+ if (from === to) return true;
36
+ if (to === "BLOCKED" && from !== "COMPLETE") return true;
37
+ if (from === "BLOCKED") return false;
38
+ const queue = [from];
39
+ const visited = new Set(queue);
40
+ while (queue.length > 0) {
41
+ const phase = queue.shift();
42
+ for (const next of WORK_TRANSITIONS[phase] ?? []) {
43
+ if (next === to) return true;
44
+ if (!visited.has(next)) {
45
+ visited.add(next);
46
+ queue.push(next);
47
+ }
48
+ }
49
+ }
50
+ return false;
51
+ }
52
+
53
+ function baseResult(classification, overrides = {}) {
54
+ return {
55
+ classification,
56
+ taskMatches: overrides.taskMatches ?? null,
57
+ workStateMatches: overrides.workStateMatches ?? null,
58
+ contractMatches: overrides.contractMatches ?? null,
59
+ phaseMatches: overrides.phaseMatches ?? null,
60
+ repositoryComparison: overrides.repositoryComparison ?? "NOT_VERIFIED",
61
+ changedPathComparison: overrides.changedPathComparison ?? "NOT_VERIFIED",
62
+ reasonCodes: uniqueSorted(overrides.reasonCodes ?? []),
63
+ reasons: uniqueSorted(overrides.reasons ?? []),
64
+ authority: "OPERATIONAL_CONTEXT_ONLY",
65
+ evidenceAuthority: "NONE",
66
+ };
67
+ }
68
+
69
+ export function classifyContinuity({
70
+ continuity,
71
+ state,
72
+ contractFingerprint,
73
+ repositoryFingerprint,
74
+ changedPaths,
75
+ } = {}) {
76
+ if (!continuity) {
77
+ return baseResult("ABSENT", { reasons: ["CONTINUITY_ABSENT"] });
78
+ }
79
+
80
+ let value;
81
+ try {
82
+ value = assertContinuitySemantics(continuity);
83
+ } catch (error) {
84
+ return baseResult("INVALID", {
85
+ reasonCodes: [error.code ?? "E_CONTINUITY_INVALID"],
86
+ reasons: [error.message],
87
+ });
88
+ }
89
+
90
+ if (!state || typeof state !== "object") {
91
+ return baseResult("INCONSISTENT", {
92
+ reasonCodes: ["E_CONTINUITY_STATE_MISSING"],
93
+ reasons: ["CONTINUITY_STATE_MISSING"],
94
+ });
95
+ }
96
+
97
+ if (state.phase === "COMPLETE") {
98
+ return baseResult("NOT_APPLICABLE", {
99
+ taskMatches: value.taskId === state.taskId,
100
+ phaseMatches: value.phase === state.phase,
101
+ reasons: ["CONTINUITY_NOT_APPLICABLE_AFTER_COMPLETE"],
102
+ });
103
+ }
104
+
105
+ const taskMatches = value.taskId === state.taskId;
106
+ const workStateMatches = value.workStateFingerprint === canonicalFingerprint(state);
107
+ const expectedContract = contractFingerprint ?? state.contractFingerprint ?? null;
108
+ const contractMatches = value.contractFingerprint === state.contractFingerprint
109
+ && (expectedContract === null || value.contractFingerprint === expectedContract);
110
+ const phaseMatches = value.phase === state.phase;
111
+ const repositoryComparison = compareRepository(value.repositoryFingerprint, repositoryFingerprint);
112
+ const changedPathComparison = compareChangedPaths(changedPaths, value.changedAreas);
113
+
114
+ const inconsistentCodes = [];
115
+ const inconsistentReasons = [];
116
+ if (!taskMatches) {
117
+ inconsistentCodes.push("E_CONTINUITY_TASK_MISMATCH");
118
+ inconsistentReasons.push("CONTINUITY_TASK_MISMATCH");
119
+ }
120
+ if (!contractMatches) {
121
+ inconsistentCodes.push("E_CONTINUITY_CONTRACT_MISMATCH");
122
+ inconsistentReasons.push("CONTINUITY_CONTRACT_MISMATCH");
123
+ }
124
+ if (!phaseMatches && !phaseReachable(value.phase, state.phase)) {
125
+ inconsistentCodes.push("E_CONTINUITY_PHASE_MISMATCH");
126
+ inconsistentReasons.push("CONTINUITY_PHASE_MISMATCH");
127
+ }
128
+ if (inconsistentCodes.length > 0) {
129
+ return baseResult("INCONSISTENT", {
130
+ taskMatches,
131
+ workStateMatches,
132
+ contractMatches,
133
+ phaseMatches,
134
+ repositoryComparison,
135
+ changedPathComparison,
136
+ reasonCodes: inconsistentCodes,
137
+ reasons: inconsistentReasons,
138
+ });
139
+ }
140
+
141
+ const reconciliationReasons = [];
142
+ if (!workStateMatches) reconciliationReasons.push("CONTINUITY_WORK_STATE_CHANGED");
143
+ if (!phaseMatches) reconciliationReasons.push("CONTINUITY_PHASE_CHANGED");
144
+ if (repositoryComparison === "MISMATCH") reconciliationReasons.push("CONTINUITY_REPOSITORY_CHANGED");
145
+ if (["PARTIAL", "MISMATCH"].includes(changedPathComparison)) {
146
+ reconciliationReasons.push("CONTINUITY_CHANGED_PATHS_DIFFER");
147
+ }
148
+
149
+ if (reconciliationReasons.length > 0) {
150
+ return baseResult("RECONCILIATION_REQUIRED", {
151
+ taskMatches,
152
+ workStateMatches,
153
+ contractMatches,
154
+ phaseMatches,
155
+ repositoryComparison,
156
+ changedPathComparison,
157
+ reasonCodes: [RECONCILIATION_CODE],
158
+ reasons: reconciliationReasons,
159
+ });
160
+ }
161
+
162
+ return baseResult("FRESH", {
163
+ taskMatches,
164
+ workStateMatches,
165
+ contractMatches,
166
+ phaseMatches,
167
+ repositoryComparison,
168
+ changedPathComparison,
169
+ });
170
+ }
171
+
172
+ export async function reconcileContinuity({ target, packageRoot } = {}) {
173
+ const [{ readWorkState }, { readContract }, repository] = await Promise.all([
174
+ import("./work-state.js"),
175
+ import("./contract.js"),
176
+ import("./repository.js"),
177
+ ]);
178
+
179
+ const state = await readWorkState(target, packageRoot);
180
+ let continuityArtifact;
181
+ try {
182
+ continuityArtifact = await readContinuity(target, packageRoot);
183
+ } catch (error) {
184
+ if (error.code === "ARTIFACT_MISSING") {
185
+ return { ...classifyContinuity({ continuity: null, state }), path: ".forgeloop/continuity.json", present: false };
186
+ }
187
+ return {
188
+ ...baseResult("INVALID", {
189
+ reasonCodes: [error.code ?? "E_CONTINUITY_INVALID"],
190
+ reasons: [error.message],
191
+ }),
192
+ path: ".forgeloop/continuity.json",
193
+ present: true,
194
+ error: error.message,
195
+ };
196
+ }
197
+
198
+ let contractFingerprint = null;
199
+ try {
200
+ const contract = await readContract(target, packageRoot);
201
+ contractFingerprint = contract.fingerprint;
202
+ } catch {
203
+ contractFingerprint = null;
204
+ }
205
+
206
+ const [repositoryFingerprint, changedPaths] = await Promise.all([
207
+ repository.currentRepositoryFingerprint(target),
208
+ repository.currentChangedPaths(target),
209
+ ]);
210
+
211
+ return {
212
+ ...classifyContinuity({
213
+ continuity: continuityArtifact.value,
214
+ state,
215
+ contractFingerprint,
216
+ repositoryFingerprint,
217
+ changedPaths,
218
+ }),
219
+ path: continuityArtifact.path,
220
+ present: true,
221
+ fingerprint: continuityArtifact.fingerprint,
222
+ continuity: continuityArtifact.value,
223
+ };
224
+ }
@@ -0,0 +1,245 @@
1
+ import { unlink } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ ARTIFACT_PATHS,
6
+ canonicalFingerprint,
7
+ readJsonArtifact,
8
+ writeJsonArtifact,
9
+ } from "./artifacts.js";
10
+ import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
11
+ import { PROTOCOL_VERSION, WORK_PHASES } from "./protocol.js";
12
+ import { assertSecretFree } from "./receipt.js";
13
+ import { getPackageRoot } from "./templates.js";
14
+
15
+ export const CONTINUITY_PATH = ARTIFACT_PATHS.continuity;
16
+ export const CONTINUITY_SCHEMA_VERSION = 1;
17
+
18
+ const LIMITS = Object.freeze({
19
+ taskId: 128,
20
+ id: 128,
21
+ summary: 1000,
22
+ path: 1024,
23
+ resumeNote: 2000,
24
+ remainingWork: 40,
25
+ knownIssues: 40,
26
+ changedAreas: 80,
27
+ inspectFirst: 40,
28
+ });
29
+
30
+ function continuityError(code, message, artifacts = [CONTINUITY_PATH]) {
31
+ const error = new Error(message);
32
+ error.name = "ContinuityError";
33
+ error.code = code;
34
+ error.artifacts = artifacts;
35
+ return error;
36
+ }
37
+
38
+ function nonEmptyString(value, label, maxLength) {
39
+ if (typeof value !== "string" || value.trim() === "") {
40
+ throw continuityError("E_CONTINUITY_INVALID", `${label} must be a non-empty string`);
41
+ }
42
+ if (value.length > maxLength) {
43
+ throw continuityError("E_CONTINUITY_INVALID", `${label} exceeds the ${maxLength}-character limit`);
44
+ }
45
+ if (/\p{Cc}/u.test(value)) {
46
+ throw continuityError("E_CONTINUITY_INVALID", `${label} contains control characters`);
47
+ }
48
+ return value;
49
+ }
50
+
51
+ function fingerprint(value, label) {
52
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) {
53
+ throw continuityError("E_CONTINUITY_INVALID", `${label} must be a lowercase SHA-256 fingerprint`);
54
+ }
55
+ return value;
56
+ }
57
+
58
+ function normalizeProjectPath(value, label) {
59
+ nonEmptyString(value, label, LIMITS.path);
60
+ const portable = value.replaceAll("\\", "/");
61
+ if (portable.startsWith("/") || /^[A-Za-z]:\//.test(portable)) {
62
+ throw continuityError("E_CONTINUITY_INVALID", `${label} must remain a relative project path`);
63
+ }
64
+ const normalized = path.posix.normalize(portable);
65
+ if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
66
+ throw continuityError("E_CONTINUITY_INVALID", `${label} must not escape the task target`);
67
+ }
68
+ return normalized.replace(/^\.\//, "");
69
+ }
70
+
71
+ function normalizeWorkItem(value, label) {
72
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
73
+ throw continuityError("E_CONTINUITY_INVALID", `${label} must be an object`);
74
+ }
75
+ const keys = Object.keys(value).sort();
76
+ if (keys.some((key) => !["id", "summary"].includes(key))) {
77
+ throw continuityError("E_CONTINUITY_INVALID", `${label} contains an unsupported field`);
78
+ }
79
+ return {
80
+ id: nonEmptyString(value.id, `${label}.id`, LIMITS.id),
81
+ summary: nonEmptyString(value.summary, `${label}.summary`, LIMITS.summary),
82
+ };
83
+ }
84
+
85
+ function normalizeWorkItems(value, label, maxItems) {
86
+ if (!Array.isArray(value)) throw continuityError("E_CONTINUITY_INVALID", `${label} must be an array`);
87
+ if (value.length > maxItems) throw continuityError("E_CONTINUITY_INVALID", `${label} exceeds the ${maxItems}-item limit`);
88
+ const items = value.map((item, index) => normalizeWorkItem(item, `${label}[${index}]`));
89
+ if (new Set(items.map((item) => item.id)).size !== items.length) {
90
+ throw continuityError("E_CONTINUITY_INVALID", `${label} must not contain duplicate ids`);
91
+ }
92
+ return items;
93
+ }
94
+
95
+ function normalizePaths(value, label, maxItems) {
96
+ if (!Array.isArray(value)) throw continuityError("E_CONTINUITY_INVALID", `${label} must be an array`);
97
+ if (value.length > maxItems) throw continuityError("E_CONTINUITY_INVALID", `${label} exceeds the ${maxItems}-item limit`);
98
+ const paths = value.map((item, index) => normalizeProjectPath(item, `${label}[${index}]`));
99
+ if (new Set(paths).size !== paths.length) throw continuityError("E_CONTINUITY_INVALID", `${label} must not contain duplicate paths`);
100
+ return paths;
101
+ }
102
+
103
+ function normalizeRepositoryFingerprint(value) {
104
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
105
+ throw continuityError("E_CONTINUITY_INVALID", "repositoryFingerprint must be an object");
106
+ }
107
+ if (Object.keys(value).some((key) => !["branch", "head"].includes(key))) {
108
+ throw continuityError("E_CONTINUITY_INVALID", "repositoryFingerprint contains an unsupported field");
109
+ }
110
+ for (const key of ["branch", "head"]) {
111
+ if (value[key] !== null && typeof value[key] !== "string") {
112
+ throw continuityError("E_CONTINUITY_INVALID", `repositoryFingerprint.${key} must be a string or null`);
113
+ }
114
+ }
115
+ return { branch: value.branch ?? null, head: value.head ?? null };
116
+ }
117
+
118
+ const CONTINUITY_FIELDS = new Set([
119
+ "schemaVersion",
120
+ "protocolVersion",
121
+ "taskId",
122
+ "workStateFingerprint",
123
+ "contractFingerprint",
124
+ "phase",
125
+ "verificationCycle",
126
+ "repositoryFingerprint",
127
+ "updatedAt",
128
+ "currentFocus",
129
+ "remainingWork",
130
+ "knownIssues",
131
+ "changedAreas",
132
+ "inspectFirst",
133
+ "resumeNote",
134
+ ]);
135
+
136
+ export function assertContinuitySemantics(input) {
137
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
138
+ throw continuityError("E_CONTINUITY_INVALID", "Continuity must be a JSON object");
139
+ }
140
+ const unknownField = Object.keys(input).find((key) => !CONTINUITY_FIELDS.has(key));
141
+ if (unknownField) {
142
+ throw continuityError("E_CONTINUITY_INVALID", `Continuity contains unsupported field: ${unknownField}`);
143
+ }
144
+ if (input.schemaVersion !== CONTINUITY_SCHEMA_VERSION || input.protocolVersion !== PROTOCOL_VERSION) {
145
+ throw continuityError("E_CONTINUITY_SCHEMA_UNSUPPORTED", "Unsupported continuity schema or protocol version");
146
+ }
147
+ if (!WORK_PHASES.includes(input.phase)) {
148
+ throw continuityError("E_CONTINUITY_INVALID", `Unknown continuity phase: ${input.phase}`);
149
+ }
150
+ if (input.verificationCycle !== undefined
151
+ && (!Number.isInteger(input.verificationCycle) || input.verificationCycle < 1)) {
152
+ throw continuityError("E_CONTINUITY_INVALID", "verificationCycle must be a positive integer");
153
+ }
154
+ const normalized = {
155
+ schemaVersion: CONTINUITY_SCHEMA_VERSION,
156
+ protocolVersion: PROTOCOL_VERSION,
157
+ taskId: nonEmptyString(input.taskId, "taskId", LIMITS.taskId),
158
+ workStateFingerprint: fingerprint(input.workStateFingerprint, "workStateFingerprint"),
159
+ contractFingerprint: fingerprint(input.contractFingerprint, "contractFingerprint"),
160
+ phase: input.phase,
161
+ ...(input.verificationCycle !== undefined ? { verificationCycle: input.verificationCycle } : {}),
162
+ repositoryFingerprint: normalizeRepositoryFingerprint(input.repositoryFingerprint),
163
+ updatedAt: nonEmptyString(input.updatedAt, "updatedAt", 128),
164
+ ...(input.currentFocus !== undefined ? { currentFocus: normalizeWorkItem(input.currentFocus, "currentFocus") } : {}),
165
+ remainingWork: normalizeWorkItems(input.remainingWork, "remainingWork", LIMITS.remainingWork),
166
+ knownIssues: normalizeWorkItems(input.knownIssues, "knownIssues", LIMITS.knownIssues),
167
+ changedAreas: normalizePaths(input.changedAreas, "changedAreas", LIMITS.changedAreas),
168
+ inspectFirst: normalizePaths(input.inspectFirst, "inspectFirst", LIMITS.inspectFirst),
169
+ ...(input.resumeNote !== undefined
170
+ ? { resumeNote: nonEmptyString(input.resumeNote, "resumeNote", LIMITS.resumeNote) }
171
+ : {}),
172
+ };
173
+ assertSecretFree(normalized);
174
+ return normalized;
175
+ }
176
+
177
+ export function createContinuity(input) {
178
+ return assertContinuitySemantics({
179
+ schemaVersion: input.schemaVersion ?? CONTINUITY_SCHEMA_VERSION,
180
+ protocolVersion: input.protocolVersion ?? PROTOCOL_VERSION,
181
+ ...structuredClone(input),
182
+ });
183
+ }
184
+
185
+ export async function readContinuity(target, packageRoot = getPackageRoot()) {
186
+ const artifact = await readJsonArtifact(target, CONTINUITY_PATH, "continuity", packageRoot);
187
+ return { ...artifact, value: assertContinuitySemantics(artifact.value) };
188
+ }
189
+
190
+ export async function writeContinuity(target, operationalInput = {}, options = {}) {
191
+ const packageRoot = options.packageRoot ?? getPackageRoot();
192
+ let state = options.state;
193
+ if (!state) {
194
+ const { readWorkState } = await import("./work-state.js");
195
+ state = await readWorkState(target, packageRoot);
196
+ }
197
+ if (!state) throw continuityError("E_CONTINUITY_STATE_MISSING", "Cannot record continuity without work state", [ARTIFACT_PATHS.state]);
198
+
199
+ let contract = options.contract;
200
+ if (!contract) {
201
+ const { readContract } = await import("./contract.js");
202
+ contract = await readContract(target, packageRoot);
203
+ }
204
+ const currentContractFingerprint = contract?.fingerprint
205
+ ?? (contract ? canonicalFingerprint(contract.value ?? contract) : null);
206
+ if (!currentContractFingerprint) {
207
+ throw continuityError("E_CONTINUITY_CONTRACT_MISMATCH", "Cannot bind continuity to the current contract", [ARTIFACT_PATHS.contract]);
208
+ }
209
+ if (state.contractFingerprint !== currentContractFingerprint) {
210
+ throw continuityError(
211
+ "E_CONTINUITY_CONTRACT_MISMATCH",
212
+ "Work state contract fingerprint does not match the current contract",
213
+ [ARTIFACT_PATHS.state, ARTIFACT_PATHS.contract],
214
+ );
215
+ }
216
+ const value = createContinuity({
217
+ taskId: state.taskId,
218
+ workStateFingerprint: canonicalFingerprint(state),
219
+ contractFingerprint: state.contractFingerprint,
220
+ phase: state.phase,
221
+ ...(state.verificationCycle !== undefined ? { verificationCycle: state.verificationCycle } : {}),
222
+ repositoryFingerprint: options.repositoryFingerprint ?? await (async () => {
223
+ const { currentRepositoryFingerprint } = await import("./repository.js");
224
+ return currentRepositoryFingerprint(target);
225
+ })(),
226
+ updatedAt: options.now ?? new Date().toISOString(),
227
+ ...(operationalInput.currentFocus !== undefined ? { currentFocus: operationalInput.currentFocus } : {}),
228
+ remainingWork: operationalInput.remainingWork ?? [],
229
+ knownIssues: operationalInput.knownIssues ?? [],
230
+ changedAreas: operationalInput.changedAreas ?? [],
231
+ inspectFirst: operationalInput.inspectFirst ?? [],
232
+ ...(operationalInput.resumeNote !== undefined ? { resumeNote: operationalInput.resumeNote } : {}),
233
+ });
234
+ return writeJsonArtifact(target, CONTINUITY_PATH, value, "continuity", packageRoot, {
235
+ dryRun: options.dryRun ?? false,
236
+ });
237
+ }
238
+
239
+ export async function clearContinuity(target) {
240
+ await assertSafePath(target, CONTINUITY_PATH);
241
+ const artifactPath = ensureWithin(target, CONTINUITY_PATH);
242
+ if (!(await fileExists(artifactPath))) return { removed: false, path: CONTINUITY_PATH };
243
+ await unlink(artifactPath);
244
+ return { removed: true, path: CONTINUITY_PATH };
245
+ }
@@ -1,6 +1,24 @@
1
1
  import { sha256 } from "./manifest.js";
2
2
  import { validateVerificationAuthority } from "./verification-capability.js";
3
3
 
4
+ export const E_COMMAND_PROVENANCE_UNATTESTED = "E_COMMAND_PROVENANCE_UNATTESTED";
5
+
6
+ export function validateCommandProvenance(check) {
7
+ if (check?.kind !== "command" || check.evidenceKind !== "OBSERVED") {
8
+ return { valid: true, error: null };
9
+ }
10
+ if (check.provenance !== "FORGELOOP_EXECUTED" || typeof check.executionRef !== "string" || check.executionRef.trim() === "") {
11
+ return {
12
+ valid: false,
13
+ error: {
14
+ code: E_COMMAND_PROVENANCE_UNATTESTED,
15
+ message: "Observed command evidence requires a ForgeLoop execution artifact",
16
+ },
17
+ };
18
+ }
19
+ return { valid: true, error: null };
20
+ }
21
+
4
22
  export const REQUIREMENT_TYPES = Object.freeze([
5
23
  "PRODUCT",
6
24
  "VERIFICATION",
@@ -256,6 +274,8 @@ function componentStatus(check, requirement, allChecks = [], options = {}) {
256
274
  item?.requirementId === child.id || item?.requirement === child.text
257
275
  )).at(-1);
258
276
  if (matchingComp) {
277
+ const provenance = validateCommandProvenance(matchingComp);
278
+ if (!provenance.valid) return { ...matchingComp, status: "failed", reasonCode: provenance.error.code };
259
279
  const auth = validateVerificationAuthority(matchingComp, options);
260
280
  if (!auth.valid) return { ...matchingComp, status: "failed", reasonCode: auth.error.code };
261
281
  return matchingComp;
@@ -264,6 +284,8 @@ function componentStatus(check, requirement, allChecks = [], options = {}) {
264
284
  const childCandidates = allChecks.filter((candidate) => matchesRequirement(candidate, child));
265
285
  const childCheck = latestAuthoritativeCheck(childCandidates);
266
286
  if (childCheck) {
287
+ const provenance = validateCommandProvenance(childCheck);
288
+ if (!provenance.valid) return { ...childCheck, status: "failed", reasonCode: provenance.error.code };
267
289
  const auth = validateVerificationAuthority(childCheck, options);
268
290
  if (!auth.valid) return { ...childCheck, status: "failed", reasonCode: auth.error.code };
269
291
  }
@@ -314,7 +336,10 @@ export function evaluateRequiredEvidence({
314
336
  }
315
337
  const candidates = checks.filter((check) => matchesRequirement(check, requirement));
316
338
  const check = latestAuthoritativeCheck(candidates);
317
- const auth = check ? validateVerificationAuthority(check, authOptions) : { valid: true };
339
+ const provenance = check ? validateCommandProvenance(check) : { valid: true };
340
+ const auth = provenance.valid
341
+ ? (check ? validateVerificationAuthority(check, authOptions) : { valid: true })
342
+ : provenance;
318
343
  const compound = componentStatus(check, requirement, checks, authOptions);
319
344
  if (!auth.valid) {
320
345
  result.invalid.push({ ...requirement, reasonCode: auth.error.code });