@cassiomc1/forgeloop 0.1.14 → 0.1.15

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.
@@ -4,6 +4,8 @@ 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";
7
9
 
8
10
  export const BUNDLE_SCHEMA_VERSION = 1;
9
11
  const BUNDLE_ROOT = ".forgeloop/tasks";
@@ -37,6 +39,34 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
37
39
  safeTaskId(taskId);
38
40
  const directory = bundleDirectory(taskId);
39
41
  const artifacts = [];
42
+ const stateSource = await readJsonArtifact(target, ARTIFACT_PATHS.state, "work-state", packageRoot);
43
+ let receiptSource = null;
44
+ try {
45
+ receiptSource = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
46
+ } catch (error) {
47
+ if (error.code !== "ARTIFACT_MISSING") throw error;
48
+ }
49
+ const provenanceErrors = [
50
+ ...(await validateChecksExecutionProvenance(stateSource.value.checks, {
51
+ target,
52
+ packageRoot,
53
+ taskId,
54
+ artifactPath: ARTIFACT_PATHS.state,
55
+ })),
56
+ ...(await validateChecksExecutionProvenance(receiptSource?.value?.checks, {
57
+ target,
58
+ packageRoot,
59
+ taskId,
60
+ artifactPath: ARTIFACT_PATHS.receipt,
61
+ })),
62
+ ];
63
+ if (provenanceErrors.length > 0) {
64
+ const first = provenanceErrors[0];
65
+ const error = new Error(first.message);
66
+ error.code = first.code;
67
+ error.artifacts = first.artifacts;
68
+ throw error;
69
+ }
40
70
  const required = [
41
71
  [ARTIFACT_PATHS.contract, "contract.json", "current-contract"],
42
72
  [ARTIFACT_PATHS.route, "route.json", "routing-result"],
@@ -64,6 +94,16 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
64
94
  const copied = await copyJson(target, sourcePath, `${directory}/${destinationName}`, schemaName, packageRoot, artifacts, destinationName);
65
95
  if (copied && !artifacts.includes(destinationName)) artifacts.push(destinationName);
66
96
  }
97
+ const executionRefs = [...new Set([
98
+ ...(stateSource.value.checks ?? []),
99
+ ...(receiptSource?.value?.checks ?? []),
100
+ ].map((check) => check?.executionRef).filter(Boolean))].sort();
101
+ for (const executionRef of executionRefs) {
102
+ const execution = await readExecutionArtifact({ target, executionRef, packageRoot });
103
+ const destination = `${directory}/executions/${execution.value.executionId}.json`;
104
+ await writeJsonArtifact(target, destination, execution.value, "execution", packageRoot);
105
+ artifacts.push(`executions/${execution.value.executionId}.json`);
106
+ }
67
107
  const eventsPath = ensureWithin(target, ARTIFACT_PATHS.events);
68
108
  if (await fileExists(eventsPath)) {
69
109
  await assertSafePath(target, `${directory}/events.ndjson`);
@@ -107,7 +147,13 @@ export async function readTaskBundle(target, taskId, packageRoot) {
107
147
  "sources.json": ["sources", "source-registry"],
108
148
  "config.json": ["config", "config"],
109
149
  };
150
+ const executions = {};
110
151
  for (const artifact of manifest.value.artifacts) {
152
+ if (artifact.startsWith("executions/") && artifact.endsWith(".json")) {
153
+ const execution = await readJsonArtifact(target, `${directory}/${artifact}`, "execution", packageRoot);
154
+ executions[execution.value.executionId] = execution.value;
155
+ continue;
156
+ }
111
157
  const mapping = mappings[artifact];
112
158
  if (!mapping) continue;
113
159
  const loadedArtifact = await readJsonArtifact(target, `${directory}/${artifact}`, mapping[1], packageRoot);
@@ -116,5 +162,31 @@ export async function readTaskBundle(target, taskId, packageRoot) {
116
162
  }
117
163
  loaded[mapping[0]] = loadedArtifact.value;
118
164
  }
165
+ if (Object.keys(executions).length > 0) loaded.executions = executions;
166
+ const provenanceErrors = [
167
+ ...(await validateChecksExecutionProvenance(loaded.state?.checks, {
168
+ target,
169
+ packageRoot,
170
+ taskId,
171
+ executionArtifacts: executions,
172
+ allowForeignCwd: true,
173
+ artifactPath: "state.json",
174
+ })),
175
+ ...(await validateChecksExecutionProvenance(loaded.receipt?.checks, {
176
+ target,
177
+ packageRoot,
178
+ taskId,
179
+ executionArtifacts: executions,
180
+ allowForeignCwd: true,
181
+ artifactPath: "receipt.json",
182
+ })),
183
+ ];
184
+ if (provenanceErrors.length > 0) {
185
+ const first = provenanceErrors[0];
186
+ const error = new Error(first.message);
187
+ error.code = first.code;
188
+ error.artifacts = first.artifacts;
189
+ throw error;
190
+ }
119
191
  return { manifest: manifest.value, artifacts: loaded };
120
192
  }
@@ -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
  }
@@ -8,7 +8,7 @@ import {
8
8
  import { readContract } from "./contract.js";
9
9
  import { appendProtocolEvent, validateEventLedger } from "./events.js";
10
10
  import { completionEvidenceForGuides } from "./guide-metadata.js";
11
- import { createCheck } from "./checks.js";
11
+ import { CHECK_PROVENANCE, createCheck } from "./checks.js";
12
12
  import { createEvidence } from "./evidence.js";
13
13
  import { coverageForRequirements } from "./coverage.js";
14
14
  import { assertCompletionRelationships, assertStateIdentity } from "./completion-relationships.js";
@@ -20,6 +20,7 @@ import { readWorkState, writeWorkState } from "./work-state.js";
20
20
  import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
21
21
  import { normalizeRequirements, classifyRequirement } from "./evidence-readiness.js";
22
22
  import { classifyCommandResolution, validateVerificationAuthority } from "./verification-capability.js";
23
+ import { readExecutionArtifact, validateExecutionBinding } from "./execution.js";
23
24
 
24
25
  function artifactError(code, message, artifacts = []) {
25
26
  const error = new Error(message);
@@ -59,6 +60,102 @@ async function readOptionalConfig(target, packageRoot) {
59
60
  }
60
61
  }
61
62
 
63
+ function formatArgv(argv) {
64
+ return argv.map((argument) => /[\s"']/u.test(argument)
65
+ ? JSON.stringify(argument)
66
+ : argument).join(" ");
67
+ }
68
+
69
+ function commandProvenanceError(message = "Observed command evidence requires a ForgeLoop execution artifact") {
70
+ return artifactError("E_COMMAND_PROVENANCE_UNATTESTED", message, [ARTIFACT_PATHS.receipt]);
71
+ }
72
+
73
+ /**
74
+ * Revalidate the ForgeLoop-owned execution artifact behind an observed command
75
+ * check. This is intentionally asynchronous so completion and audit can verify
76
+ * the artifact instead of trusting duplicated check metadata.
77
+ */
78
+ export async function validateCheckExecutionProvenance(check, {
79
+ target,
80
+ packageRoot,
81
+ taskId,
82
+ executionArtifacts,
83
+ allowForeignCwd = false,
84
+ } = {}) {
85
+ if (check?.kind !== "command" || check.evidenceKind !== "OBSERVED") return null;
86
+ if (check.provenance !== "FORGELOOP_EXECUTED" || !check.executionRef) {
87
+ throw commandProvenanceError();
88
+ }
89
+ const artifact = executionArtifacts
90
+ ? { value: executionArtifacts[check.executionRef] }
91
+ : await readExecutionArtifact({
92
+ target,
93
+ executionRef: check.executionRef,
94
+ packageRoot,
95
+ });
96
+ if (!artifact.value) {
97
+ throw artifactError(
98
+ "E_EXECUTION_REF_INVALID",
99
+ "Execution reference does not resolve to an execution artifact in this bundle",
100
+ [ARTIFACT_PATHS.executionDirectory],
101
+ );
102
+ }
103
+ const execution = validateExecutionBinding({
104
+ execution: artifact.value,
105
+ taskId,
106
+ checkId: check.id,
107
+ requirement: check.requirement,
108
+ verificationCycle: check.details?.verificationCycle ?? 1,
109
+ });
110
+ if (!allowForeignCwd && path.resolve(execution.cwd) !== path.resolve(target)) {
111
+ throw artifactError(
112
+ "E_EXECUTION_REF_INVALID",
113
+ "Execution artifact cwd does not match the current target",
114
+ [ARTIFACT_PATHS.executionDirectory],
115
+ );
116
+ }
117
+ if (check.status === "passed" && (execution.status !== "passed" || execution.exitCode !== 0)) {
118
+ throw artifactError(
119
+ "E_EXECUTION_REF_INVALID",
120
+ "A passed command check must reference a successful execution artifact",
121
+ [ARTIFACT_PATHS.executionDirectory],
122
+ );
123
+ }
124
+ if (check.status === "failed" && execution.status !== "failed") {
125
+ throw artifactError(
126
+ "E_EXECUTION_REF_INVALID",
127
+ "A failed command check must reference a failed execution artifact",
128
+ [ARTIFACT_PATHS.executionDirectory],
129
+ );
130
+ }
131
+ if (check.exitCode !== undefined && check.exitCode !== execution.exitCode) {
132
+ throw artifactError(
133
+ "E_EXECUTION_REF_INVALID",
134
+ "Check exitCode does not match its execution artifact",
135
+ [ARTIFACT_PATHS.executionDirectory],
136
+ );
137
+ }
138
+ return execution;
139
+ }
140
+
141
+ export async function validateChecksExecutionProvenance(checks, options = {}) {
142
+ const errors = [];
143
+ for (const check of Array.isArray(checks) ? checks : []) {
144
+ try {
145
+ await validateCheckExecutionProvenance(check, options);
146
+ } catch (error) {
147
+ errors.push({
148
+ code: error.code ?? "E_EXECUTION_REF_INVALID",
149
+ message: error.message,
150
+ artifacts: error.artifacts ?? [options.artifactPath ?? ARTIFACT_PATHS.receipt],
151
+ checkId: check?.id,
152
+ requirementId: check?.requirement,
153
+ });
154
+ }
155
+ }
156
+ return errors;
157
+ }
158
+
62
159
  export async function requiredEvidenceForTarget({ target, contract, route, packageRoot, additionalEvidence = [] }) {
63
160
  const config = await readOptionalConfig(target, packageRoot);
64
161
  const guideEvidence = await completionEvidenceForGuides(route.value.guides, packageRoot);
@@ -196,42 +293,20 @@ function appendUniqueEvidence(evidence, nextEvidence) {
196
293
  return exists ? [...evidence] : [...evidence, nextEvidence];
197
294
  }
198
295
 
199
- export async function recordCheck({
296
+ /**
297
+ * Read-only lifecycle checks shared by run-check and record-check. Keeping
298
+ * these checks before process launch prevents a command from running when the
299
+ * target is not ready to receive verification evidence.
300
+ */
301
+ export async function assertRecordCheckPrerequisites({
200
302
  target,
201
303
  packageRoot,
202
- id,
203
- kind = "command",
204
304
  requirement,
205
305
  status,
206
306
  evidenceKind,
207
- command,
208
- result,
209
- exitCode,
210
- details,
211
307
  authorityContext,
212
308
  runtimeContext,
213
- }) {
214
- requiredString(id, "check id");
215
- requiredString(kind, "check kind");
216
- requiredString(requirement, "check requirement");
217
- requiredString(status, "check status");
218
- requiredString(evidenceKind, "evidence kind");
219
- if (command !== undefined && typeof command !== "string") {
220
- throw artifactError("E_CHECK_INVALID", "command must be a string when supplied");
221
- }
222
- if (result !== undefined && typeof result !== "string") {
223
- throw artifactError("E_CHECK_INVALID", "result must be a string when supplied");
224
- }
225
- if (details !== undefined && (!details || typeof details !== "object" || Array.isArray(details))) {
226
- throw artifactError("E_CHECK_INVALID", "check details must be a JSON object");
227
- }
228
- if ((typeof command !== "string" || command.trim() === "")
229
- && (typeof result !== "string" || result.trim() === "")) {
230
- throw artifactError("E_CHECK_INVALID", "record-check requires --command or --result");
231
- }
232
-
233
- const commandSpec = typeof command === "string" && command.trim() !== "" ? command.trim() : undefined;
234
-
309
+ } = {}) {
235
310
  const state = await readWorkState(target, packageRoot);
236
311
  if (!state) throw artifactError("E_STATE_MISSING", "Work state is required before recording a check", [ARTIFACT_PATHS.state]);
237
312
  if (["COMPLETE", "BLOCKED"].includes(state.phase)) {
@@ -274,9 +349,114 @@ export async function recordCheck({
274
349
  authorityContext,
275
350
  runtimeContext,
276
351
  });
277
- const source = commandSpec || `check:${id}`;
278
- const recordedResult = result?.trim() || `recorded command: ${commandSpec || source}`;
279
- const classification = commandSpec !== undefined ? classifyCommandResolution(commandSpec) : null;
352
+ const ledger = await validateEventLedger(target, packageRoot);
353
+ if (!ledger.valid) {
354
+ const first = ledger.errors[0];
355
+ throw artifactError(first.code, first.message, [ARTIFACT_PATHS.events]);
356
+ }
357
+ if (!ledger.events.some((event) => event.taskId === state.taskId && event.event === "VERIFICATION_STARTED")) {
358
+ throw artifactError(
359
+ "E_PHASE_CHRONOLOGY_INVALID",
360
+ "record-check requires VERIFICATION_STARTED in the current task ledger",
361
+ [ARTIFACT_PATHS.events],
362
+ );
363
+ }
364
+ return {
365
+ state,
366
+ contract,
367
+ route,
368
+ preflight,
369
+ requiredEvidence,
370
+ existingReceipt,
371
+ ledger,
372
+ };
373
+ }
374
+
375
+ export async function recordCheck({
376
+ target,
377
+ packageRoot,
378
+ id,
379
+ kind = "command",
380
+ requirement,
381
+ status,
382
+ evidenceKind,
383
+ command,
384
+ result,
385
+ exitCode,
386
+ details,
387
+ executionRef,
388
+ provenance,
389
+ authorityContext,
390
+ runtimeContext,
391
+ }) {
392
+ requiredString(id, "check id");
393
+ requiredString(kind, "check kind");
394
+ requiredString(requirement, "check requirement");
395
+ requiredString(status, "check status");
396
+ requiredString(evidenceKind, "evidence kind");
397
+ if (command !== undefined && typeof command !== "string") {
398
+ throw artifactError("E_CHECK_INVALID", "command must be a string when supplied");
399
+ }
400
+ if (result !== undefined && typeof result !== "string") {
401
+ throw artifactError("E_CHECK_INVALID", "result must be a string when supplied");
402
+ }
403
+ if (details !== undefined && (!details || typeof details !== "object" || Array.isArray(details))) {
404
+ throw artifactError("E_CHECK_INVALID", "check details must be a JSON object");
405
+ }
406
+ if (executionRef !== undefined && (typeof executionRef !== "string" || executionRef.trim() === "")) {
407
+ throw artifactError("E_EXECUTION_REF_INVALID", "executionRef must be a non-empty string when supplied");
408
+ }
409
+ if (provenance !== undefined && !CHECK_PROVENANCE.includes(provenance)) {
410
+ throw artifactError("E_CHECK_INVALID", `provenance must be one of ${CHECK_PROVENANCE.join(", ")}`);
411
+ }
412
+ if ((typeof command !== "string" || command.trim() === "")
413
+ && (typeof result !== "string" || result.trim() === "")) {
414
+ throw artifactError("E_CHECK_INVALID", "record-check requires --command or --result");
415
+ }
416
+
417
+ const context = await assertRecordCheckPrerequisites({
418
+ target,
419
+ packageRoot,
420
+ requirement,
421
+ status,
422
+ evidenceKind,
423
+ authorityContext,
424
+ runtimeContext,
425
+ });
426
+ const {
427
+ state,
428
+ contract,
429
+ route,
430
+ requiredEvidence,
431
+ existingReceipt,
432
+ } = context;
433
+
434
+ const commandSpec = typeof command === "string" && command.trim() !== "" ? command.trim() : undefined;
435
+ const observedCommand = kind === "command" && evidenceKind === "OBSERVED";
436
+ if (observedCommand && (!executionRef || provenance !== "FORGELOOP_EXECUTED")) {
437
+ throw commandProvenanceError();
438
+ }
439
+ const execution = executionRef
440
+ ? await validateCheckExecutionProvenance({
441
+ kind,
442
+ evidenceKind,
443
+ provenance,
444
+ executionRef,
445
+ status,
446
+ exitCode,
447
+ id,
448
+ requirement,
449
+ details: { ...(details ?? {}), verificationCycle: state.verificationCycle ?? 1 },
450
+ }, {
451
+ target,
452
+ packageRoot,
453
+ taskId: contract.value.taskId,
454
+ })
455
+ : null;
456
+ const effectiveCommand = execution ? formatArgv(execution.argv) : commandSpec;
457
+ const source = effectiveCommand || `check:${id}`;
458
+ const recordedResult = result?.trim() || `recorded command: ${effectiveCommand || source}`;
459
+ const classification = execution?.resolution ?? (effectiveCommand !== undefined ? classifyCommandResolution(effectiveCommand) : null);
280
460
  const installationAuthorized = Boolean(
281
461
  details?.installationAuthorized
282
462
  || details?.authority?.softwareInstallation === "AUTHORIZED"
@@ -289,15 +469,28 @@ export async function recordCheck({
289
469
  status,
290
470
  evidenceKind,
291
471
  source,
472
+ ...(executionRef === undefined ? {} : { executionRef }),
473
+ ...(provenance === undefined ? {} : { provenance }),
292
474
  timestamp: new Date().toISOString(),
293
- ...(exitCode === undefined ? {} : { exitCode }),
475
+ ...(execution?.exitCode !== null && execution?.exitCode !== undefined
476
+ ? { exitCode: execution.exitCode }
477
+ : exitCode === undefined ? {} : { exitCode }),
294
478
  details: {
295
- ...(command === undefined ? {} : { command }),
479
+ ...(effectiveCommand === undefined ? {} : { command: effectiveCommand }),
296
480
  ...(result === undefined ? {} : { result }),
297
481
  ...(details === undefined ? {} : details),
298
482
  verificationCycle: state.verificationCycle ?? 1,
299
483
  ...(classification ? {
300
484
  execution: {
485
+ ...(details?.execution ?? {}),
486
+ ...(execution ? {
487
+ executionRef: execution.executionId,
488
+ argv: [...execution.argv],
489
+ cwd: execution.cwd,
490
+ resolution: execution.resolution,
491
+ status: execution.status,
492
+ exitCode: execution.exitCode,
493
+ } : {}),
301
494
  resolutionMode: classification.resolutionMode,
302
495
  mayInstall: classification.mayInstall,
303
496
  installationAuthorized,
@@ -310,6 +503,7 @@ export async function recordCheck({
310
503
  packageRoot,
311
504
  authorityContext,
312
505
  runtimeContext,
506
+ requireCommandProvenance: observedCommand,
313
507
  });
314
508
  const evidence = createEvidence({
315
509
  kind: evidenceKind,
@@ -349,18 +543,6 @@ export async function recordCheck({
349
543
  authorityContext,
350
544
  runtimeContext,
351
545
  });
352
- const ledger = await validateEventLedger(target, packageRoot);
353
- if (!ledger.valid) {
354
- const first = ledger.errors[0];
355
- throw artifactError(first.code, first.message, [ARTIFACT_PATHS.events]);
356
- }
357
- if (!ledger.events.some((event) => event.taskId === state.taskId && event.event === "VERIFICATION_STARTED")) {
358
- throw artifactError(
359
- "E_PHASE_CHRONOLOGY_INVALID",
360
- "record-check requires VERIFICATION_STARTED in the current task ledger",
361
- [ARTIFACT_PATHS.events],
362
- );
363
- }
364
546
  const coverage = coverageForRequirements(requiredEvidence, checks, {
365
547
  target,
366
548
  taskId: contract.value.taskId,
@@ -1,5 +1,5 @@
1
1
  import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
2
- import { requiredEvidenceForTarget } from "./completion-artifacts.js";
2
+ import { requiredEvidenceForTarget, validateChecksExecutionProvenance } from "./completion-artifacts.js";
3
3
  import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger, validateStateLedgerCoherence } from "./events.js";
4
4
  import { evaluatePreflight } from "./preflight.js";
5
5
  import { readContract } from "./contract.js";
@@ -42,6 +42,9 @@ function repairNext(error) {
42
42
  case "E_CHECK_INVALID":
43
43
  case "E_CHECK_STATUS_CONTRADICTION":
44
44
  return "Run forgeloop record-check with compatible observed evidence for the named requirement.";
45
+ case "E_COMMAND_PROVENANCE_UNATTESTED":
46
+ case "E_EXECUTION_REF_INVALID":
47
+ return "Run forgeloop run-check with the exact argv, or record the result as manual/NOT_VERIFIED evidence without claiming command execution.";
45
48
  case "E_EVIDENCE_PARTIAL":
46
49
  return "Run or finish the missing component checks and record observed evidence.";
47
50
  case "E_EVIDENCE_INVALID":
@@ -216,6 +219,21 @@ export async function evaluateCompletion({ target, packageRoot, strict = false,
216
219
  }
217
220
  }
218
221
 
222
+ if (contract) {
223
+ errors.push(...await validateChecksExecutionProvenance(state?.checks, {
224
+ target,
225
+ packageRoot,
226
+ taskId: contract.value.taskId,
227
+ artifactPath: ARTIFACT_PATHS.state,
228
+ }));
229
+ errors.push(...await validateChecksExecutionProvenance(receipt?.value?.checks, {
230
+ target,
231
+ packageRoot,
232
+ taskId: contract.value.taskId,
233
+ artifactPath: ARTIFACT_PATHS.receipt,
234
+ }));
235
+ }
236
+
219
237
  if (state && !["REVIEWING", "COMPLETE"].includes(state.phase)) {
220
238
  errors.push(issue("E_PHASE_PREREQUISITE_MISSING", `Completion requires REVIEWING or COMPLETE state, found ${state.phase}`, [ARTIFACT_PATHS.state]));
221
239
  }
@@ -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 });