@cassiomc1/forgeloop 1.6.2 → 1.6.4
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 +26 -4
- package/EXECUTION_STATE.md +7 -0
- package/LOOP_ENGINEERING.md +58 -0
- package/LOOP_SYSTEM_DESIGN.md +54 -0
- package/ORCHESTRATOR_INTEGRATION.md +23 -0
- package/PROTOCOL_INTEGRATION.md +36 -0
- package/QUALITY_SCORECARD.md +21 -0
- package/README.md +57 -2
- package/TERMINOLOGY.md +11 -0
- package/THREAT_MODEL.md +7 -0
- package/completions/_forgeloop +85 -0
- package/completions/forgeloop.bash +173 -0
- package/completions/forgeloop.fish +544 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +281 -0
- package/docs/ARTIFACT_REFERENCE.md +226 -0
- package/docs/CLI_REFERENCE.md +308 -2
- package/docs/CODE_ATTESTATION.md +141 -0
- package/docs/CROSS_HARNESS_CONTINUITY.md +22 -2
- package/docs/DOCUMENTATION_GUIDE.md +13 -11
- package/docs/GETTING_STARTED.md +98 -1
- package/docs/MCP.md +8 -0
- package/docs/PLATFORM_ADAPTERS.md +69 -0
- package/docs/RECIPES.md +138 -0
- package/docs/RELEASE_CHECKLIST.md +54 -0
- package/docs/REVISION_PROVIDERS.md +136 -0
- package/docs/SIGNING_PROVIDERS.md +83 -0
- package/docs/TROUBLESHOOTING.md +184 -0
- package/docs/UNIVERSAL_INTEGRATION.md +11 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +13841 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +5046 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +46 -43
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +41 -41
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +13783 -0
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +4988 -0
- package/docs/diagrams/README.md +30 -22
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +125 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +11 -8
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +116 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +20 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +3 -3
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +20 -0
- package/integrations/generic-ci/verify.sh +14 -0
- package/package.json +31 -9
- package/schemas/attestation-verification-result.schema.json +34 -0
- package/schemas/code-attestation.schema.json +61 -0
- package/schemas/code-manifest.schema.json +57 -0
- package/schemas/config.schema.json +57 -1
- package/schemas/handoff-envelope.schema.json +57 -0
- package/schemas/in-toto-statement.schema.json +32 -0
- package/schemas/responsibility.schema.json +37 -0
- package/schemas/verification-scope.schema.json +26 -0
- package/schemas/workspace-binding.schema.json +20 -0
- package/scripts/CI_VALIDATORS.md +3 -0
- package/scripts/benchmark-cli-startup.mjs +60 -0
- package/scripts/check-changelog-freshness.mjs +88 -0
- package/scripts/check-critical-coverage.mjs +58 -0
- package/scripts/generate-agent-protocol-summary.mjs +141 -0
- package/scripts/generate-shell-completions.mjs +132 -0
- package/scripts/write-forgeloop-attestation-summary.mjs +93 -0
- package/src/cli.js +114 -6
- package/src/commands/attestation-create.js +30 -0
- package/src/commands/attestation-status.js +12 -0
- package/src/commands/attestation-verify-range.js +32 -0
- package/src/commands/attestation-verify.js +23 -0
- package/src/commands/handoff-create.js +9 -0
- package/src/commands/handoff-list.js +12 -0
- package/src/commands/handoff-show.js +10 -0
- package/src/commands/responsibility-set.js +20 -0
- package/src/commands/responsibility-status.js +12 -0
- package/src/commands/run-check.js +43 -3
- package/src/commands/task-repair-legacy-recovery.js +1 -2
- package/src/commands/verify-scope.js +9 -0
- package/src/commands/workspace-bind.js +17 -0
- package/src/commands/workspace-status.js +19 -0
- package/src/core/artifact-registry.js +84 -0
- package/src/core/attestation-coverage.js +134 -0
- package/src/core/attestation-verifier.js +227 -0
- package/src/core/attestation.js +263 -0
- package/src/core/audit.js +67 -1
- package/src/core/bundles.js +210 -9
- package/src/core/cli-command-definitions.js +196 -0
- package/src/core/code-manifest.js +293 -0
- package/src/core/command-executors.js +79 -0
- package/src/core/command-input.js +69 -18
- package/src/core/command-runtime.js +2 -1
- package/src/core/completion.js +170 -8
- package/src/core/config.js +60 -2
- package/src/core/continuity-cli-options.js +9 -3
- package/src/core/continuity-reconciliation.js +20 -0
- package/src/core/diagnostic-projection.js +3 -1
- package/src/core/error-codes.js +146 -0
- package/src/core/events.js +109 -13
- package/src/core/exit-codes.js +22 -0
- package/src/core/handoff.js +208 -0
- package/src/core/information-gain-projection.js +3 -3
- package/src/core/integration-invocation-policy.js +40 -0
- package/src/core/integration-resources.js +54 -0
- package/src/core/next-action-phases.js +968 -0
- package/src/core/next-action.js +32 -951
- package/src/core/phase.js +29 -1
- package/src/core/protocol-info.js +41 -1
- package/src/core/repository.js +256 -11
- package/src/core/responsibility.js +265 -0
- package/src/core/revision/git.js +207 -0
- package/src/core/revision/provider.js +90 -0
- package/src/core/revision/registry.js +5 -0
- package/src/core/route-artifact.js +1 -1
- package/src/core/schema-validation.js +8 -0
- package/src/core/signing/none.js +23 -0
- package/src/core/signing/provider.js +21 -0
- package/src/core/signing/registry.js +7 -0
- package/src/core/signing/sigstore.js +158 -0
- package/src/core/task-command.js +9 -1
- package/src/core/task-paths.js +72 -0
- package/src/core/templates.js +8 -0
- package/src/core/verification-scope-capability.js +179 -0
- package/src/core/verification-scope.js +271 -0
- package/src/core/workspace-binding.js +211 -0
- package/src/integration.d.ts +91 -0
- package/AGENT_COMPATIBILITY.md +0 -11
- package/docs/RELEASE_CHECKLIST_1_4.md +0 -38
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +0 -78
- package/docs/RELEASE_CHECKLIST_1_6_1.md +0 -121
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { continuityOptionDefaults, validateContinuityOptions } from "./continuity-cli-options.js";
|
|
2
|
+
import { E_CLI_INVOCATION_INVALID } from "./error-codes.js";
|
|
3
|
+
|
|
4
|
+
function inputError(message) {
|
|
5
|
+
const error = new Error(message);
|
|
6
|
+
error.code = E_CLI_INVOCATION_INVALID;
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
11
|
* Transport-neutral option defaults shared by the CLI parser and the
|
|
@@ -39,11 +46,35 @@ export function defaultCommandInputValues() {
|
|
|
39
46
|
checkDetails: null,
|
|
40
47
|
checkExecutionRef: null,
|
|
41
48
|
checkProvenance: null,
|
|
49
|
+
timeoutMs: null,
|
|
50
|
+
scopeRef: null,
|
|
42
51
|
commandArgv: [],
|
|
43
52
|
checkType: null,
|
|
44
53
|
checkSource: null,
|
|
45
54
|
policy: null,
|
|
46
55
|
taskId: null,
|
|
56
|
+
recipientHint: null,
|
|
57
|
+
handoffNote: null,
|
|
58
|
+
handoffId: null,
|
|
59
|
+
responsibilityLabel: null,
|
|
60
|
+
responsibilityAllowedPaths: [],
|
|
61
|
+
responsibilityReadOnlyPaths: [],
|
|
62
|
+
responsibilityRequiredChecks: [],
|
|
63
|
+
responsibilityFreezeContract: false,
|
|
64
|
+
responsibilityFreezeRoute: false,
|
|
65
|
+
responsibilityFreezeClaims: false,
|
|
66
|
+
verificationScopeMode: null,
|
|
67
|
+
revisionProvider: null,
|
|
68
|
+
signingProvider: null,
|
|
69
|
+
trustedRoot: null,
|
|
70
|
+
baseRevision: null,
|
|
71
|
+
headRevision: null,
|
|
72
|
+
requireCompleteCoverage: false,
|
|
73
|
+
requireSignature: false,
|
|
74
|
+
attestationRef: null,
|
|
75
|
+
attestationBundle: null,
|
|
76
|
+
attestationIdentity: null,
|
|
77
|
+
attestationIssuer: null,
|
|
47
78
|
help: false,
|
|
48
79
|
version: false,
|
|
49
80
|
...continuityOptionDefaults(),
|
|
@@ -60,48 +91,68 @@ export function validateForgeLoopCommandInput({ command, input, help = false } =
|
|
|
60
91
|
if (!command) return;
|
|
61
92
|
|
|
62
93
|
if (command === "policy" && !options.policy) {
|
|
63
|
-
throw
|
|
94
|
+
throw inputError("policy requires a name");
|
|
64
95
|
}
|
|
65
96
|
if (command !== "policy" && options.policy) {
|
|
66
|
-
throw
|
|
97
|
+
throw inputError(`Policy name is not valid for ${command}`);
|
|
67
98
|
}
|
|
68
99
|
if (command === "bundle" && !options.taskId) {
|
|
69
|
-
throw
|
|
100
|
+
throw inputError("bundle requires --task");
|
|
70
101
|
}
|
|
71
102
|
if (command === "task-create" && !options.taskId) {
|
|
72
|
-
throw
|
|
103
|
+
throw inputError("task-create requires --task");
|
|
104
|
+
}
|
|
105
|
+
if (["workspace-bind", "workspace-status", "handoff-create", "handoff-list", "handoff-show", "responsibility-set", "responsibility-status", "verify-scope", "attestation-create", "attestation-status", "attestation-verify"].includes(command)
|
|
106
|
+
&& !options.taskId) {
|
|
107
|
+
throw inputError(`${command} requires --task`);
|
|
108
|
+
}
|
|
109
|
+
if (command === "handoff-show" && !help && !options.handoffId) {
|
|
110
|
+
throw inputError("handoff-show requires --id");
|
|
111
|
+
}
|
|
112
|
+
if (command === "responsibility-set" && !help && !options.responsibilityLabel) {
|
|
113
|
+
throw inputError("responsibility-set requires --label");
|
|
114
|
+
}
|
|
115
|
+
if (command === "verify-scope" && !help) {
|
|
116
|
+
const mode = String(options.verificationScopeMode ?? "AUTO").toUpperCase();
|
|
117
|
+
if (!["AUTO", "CHANGED", "CLAIMED", "FULL"].includes(mode)) {
|
|
118
|
+
throw inputError("verify-scope --mode must be AUTO, CHANGED, CLAIMED, or FULL");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (command === "attestation-verify-range" && !help) {
|
|
122
|
+
if (!options.baseRevision) throw inputError("attestation-verify-range requires --base");
|
|
123
|
+
if (!options.headRevision) throw inputError("attestation-verify-range requires --head");
|
|
73
124
|
}
|
|
74
125
|
|
|
75
126
|
validateContinuityOptions(command, options);
|
|
76
127
|
|
|
77
128
|
if (command === "record-check" && !help) {
|
|
78
|
-
if (!options.checkId) throw
|
|
79
|
-
if (!options.checkRequirement) throw
|
|
80
|
-
if (!options.checkStatus) throw
|
|
81
|
-
if (!options.checkEvidenceKind) throw
|
|
82
|
-
if (!options.checkCommand && !options.checkResult) throw
|
|
129
|
+
if (!options.checkId) throw inputError("record-check requires --id");
|
|
130
|
+
if (!options.checkRequirement) throw inputError("record-check requires --requirement");
|
|
131
|
+
if (!options.checkStatus) throw inputError("record-check requires --status");
|
|
132
|
+
if (!options.checkEvidenceKind) throw inputError("record-check requires --evidence-kind");
|
|
133
|
+
if (!options.checkCommand && !options.checkResult) throw inputError("record-check requires --command or --result");
|
|
83
134
|
}
|
|
84
135
|
if (command === "run-check" && !help) {
|
|
85
|
-
if (!options.checkId) throw
|
|
86
|
-
if (!options.checkRequirement) throw
|
|
136
|
+
if (!options.checkId) throw inputError("run-check requires --id");
|
|
137
|
+
if (!options.checkRequirement) throw inputError("run-check requires --requirement");
|
|
87
138
|
if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
|
|
88
139
|
|| options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
|
|
89
|
-
throw
|
|
140
|
+
throw inputError("run-check accepts only --id, --requirement, --details, --timeout-ms, --scope-ref, and -- <argv>");
|
|
90
141
|
}
|
|
91
142
|
if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) {
|
|
92
|
-
throw
|
|
143
|
+
throw inputError("run-check requires -- followed by an exact command argv");
|
|
93
144
|
}
|
|
94
145
|
}
|
|
95
146
|
if (command === "reconcile-closure" && !help) {
|
|
96
|
-
if (!options.taskId) throw
|
|
97
|
-
if (!options.checkId) throw
|
|
98
|
-
if (!options.checkRequirement) throw
|
|
147
|
+
if (!options.taskId) throw inputError("reconcile-closure requires --task");
|
|
148
|
+
if (!options.checkId) throw inputError("reconcile-closure requires --id");
|
|
149
|
+
if (!options.checkRequirement) throw inputError("reconcile-closure requires --requirement");
|
|
99
150
|
if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
|
|
100
151
|
|| options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
|
|
101
|
-
throw
|
|
152
|
+
throw inputError("reconcile-closure accepts only --id, --requirement, --details, and -- <argv>");
|
|
102
153
|
}
|
|
103
154
|
if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) {
|
|
104
|
-
throw
|
|
155
|
+
throw inputError("reconcile-closure requires -- followed by an exact command argv");
|
|
105
156
|
}
|
|
106
157
|
}
|
|
107
158
|
}
|
|
@@ -4,6 +4,7 @@ import { CLI_COMMAND_DEFINITIONS } from "./cli-command-definitions.js";
|
|
|
4
4
|
import { COMMAND_EXECUTORS } from "./command-executors.js";
|
|
5
5
|
import { defaultCommandInputValues, validateForgeLoopCommandInput } from "./command-input.js";
|
|
6
6
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
7
|
+
import { exitCodeForError } from "./exit-codes.js";
|
|
7
8
|
|
|
8
9
|
export const FORGELOOP_INTEGRATION_RUNTIME_VERSION = 1;
|
|
9
10
|
|
|
@@ -105,7 +106,7 @@ export async function executeForgeLoopCommand({
|
|
|
105
106
|
return {
|
|
106
107
|
ok: false,
|
|
107
108
|
command,
|
|
108
|
-
exitCode:
|
|
109
|
+
exitCode: exitCodeForError(error),
|
|
109
110
|
result: null,
|
|
110
111
|
error: {
|
|
111
112
|
code: error.code ?? "E_COMMAND_EXECUTION_FAILED",
|
package/src/core/completion.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
2
2
|
import { requiredEvidenceForTarget, validateChecksExecutionProvenance } from "./completion-artifacts.js";
|
|
3
|
-
import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger, validateStateLedgerCoherence } from "./events.js";
|
|
3
|
+
import { appendProtocolEvent, previewProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger, validateStateLedgerCoherence } from "./events.js";
|
|
4
4
|
import { evaluatePreflight } from "./preflight.js";
|
|
5
5
|
import { readContract } from "./contract.js";
|
|
6
6
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
@@ -11,9 +11,29 @@ import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
|
|
|
11
11
|
import { evaluateStartExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
|
|
12
12
|
import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
|
|
13
13
|
import { evaluateTerminalRequirements } from "./evidence-readiness.js";
|
|
14
|
-
import { PROJECT_ARTIFACT_PATHS, taskArtifactPath } from "./task-paths.js";
|
|
14
|
+
import { PROJECT_ARTIFACT_PATHS, taskArtifactPath, taskCodeManifestPath } from "./task-paths.js";
|
|
15
15
|
import { detectPolicyCapability, evaluateTargetPolicy } from "./policy-engine.js";
|
|
16
16
|
import { listActions } from "./actions.js";
|
|
17
|
+
import { readConfig } from "./config.js";
|
|
18
|
+
import { resolveResponsibilityStatus } from "./responsibility.js";
|
|
19
|
+
import { createCodeManifest, readCodeManifest, validateCodeManifestBindings, writeCodeManifest } from "./code-manifest.js";
|
|
20
|
+
import { getActiveTaskTransaction, withTaskTransaction } from "./transaction.js";
|
|
21
|
+
|
|
22
|
+
async function attestationConfiguration(target, packageRoot, errors) {
|
|
23
|
+
try {
|
|
24
|
+
const config = await readConfig(target, packageRoot);
|
|
25
|
+
return {
|
|
26
|
+
mode: config.attestation?.mode ?? "off",
|
|
27
|
+
revisionProvider: config.attestation?.revisionProvider ?? "git",
|
|
28
|
+
requireCompleteCoverage: config.attestation?.requireCompleteCoverage === true,
|
|
29
|
+
signing: config.attestation?.signing ?? { provider: "none", required: false, policy: {} },
|
|
30
|
+
};
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code === "ARTIFACT_MISSING") return { mode: "off", revisionProvider: "git", requireCompleteCoverage: false, signing: { provider: "none", required: false, policy: {} } };
|
|
33
|
+
errors.push(issue(error.code ?? "E_ATTESTATION_INVALID", `Attestation configuration is invalid: ${error.message}`, [PROJECT_ARTIFACT_PATHS.config]));
|
|
34
|
+
return { mode: "off", revisionProvider: "git", requireCompleteCoverage: false, signing: { provider: "none", required: false, policy: {} } };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
17
37
|
|
|
18
38
|
async function actionReceiptSummary(target, packageRoot, taskId) {
|
|
19
39
|
const actions = await listActions(target, { packageRoot, taskId });
|
|
@@ -290,6 +310,59 @@ export async function evaluateCompletion({
|
|
|
290
310
|
[receiptRel],
|
|
291
311
|
errors,
|
|
292
312
|
);
|
|
313
|
+
const attestationConfig = await attestationConfiguration(target, packageRoot, errors);
|
|
314
|
+
let attestation = {
|
|
315
|
+
mode: attestationConfig.mode,
|
|
316
|
+
status: attestationConfig.mode === "off" ? "DISABLED" : "MISSING",
|
|
317
|
+
level: "PROCESSED",
|
|
318
|
+
manifest: null,
|
|
319
|
+
revisionProvider: attestationConfig.revisionProvider,
|
|
320
|
+
requireCompleteCoverage: attestationConfig.requireCompleteCoverage,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
if (state?.phase === "COMPLETE" && attestationConfig.mode !== "off") {
|
|
324
|
+
try {
|
|
325
|
+
const manifest = await readCodeManifest({ target, packageRoot, taskId: contract?.value?.taskId ?? taskId });
|
|
326
|
+
await validateCodeManifestBindings({
|
|
327
|
+
target,
|
|
328
|
+
packageRoot,
|
|
329
|
+
taskId: contract?.value?.taskId ?? taskId,
|
|
330
|
+
manifest: manifest.value,
|
|
331
|
+
revisionProvider: attestationConfig.revisionProvider,
|
|
332
|
+
});
|
|
333
|
+
attestation = {
|
|
334
|
+
...attestation,
|
|
335
|
+
status: "CAPTURED",
|
|
336
|
+
level: "VERIFIED",
|
|
337
|
+
manifest: { path: manifest.path, fingerprint: manifest.fingerprint, contentDigest: manifest.value.contentDigest },
|
|
338
|
+
};
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (attestationConfig.mode === "required") {
|
|
341
|
+
errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_ATTESTATION_MANIFEST_MISSING" : error.code ?? "E_ATTESTATION_MANIFEST_STALE", error.message, error.artifacts ?? [taskArtifactPath(contract?.value?.taskId ?? taskId, "attestations")]));
|
|
342
|
+
}
|
|
343
|
+
attestation = {
|
|
344
|
+
...attestation,
|
|
345
|
+
status: error.code === "ARTIFACT_MISSING" ? "MISSING" : "UNAVAILABLE",
|
|
346
|
+
error: { code: error.code ?? "E_ATTESTATION_MANIFEST_STALE", message: error.message },
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let responsibility = { status: "NOT_APPLICABLE", taskId: contract?.value?.taskId ?? taskId, errors: [] };
|
|
352
|
+
if (taskId || contract?.value?.taskId) {
|
|
353
|
+
try {
|
|
354
|
+
responsibility = await resolveResponsibilityStatus(target, {
|
|
355
|
+
packageRoot,
|
|
356
|
+
taskId: contract?.value?.taskId ?? taskId,
|
|
357
|
+
});
|
|
358
|
+
for (const error of responsibility.errors ?? []) {
|
|
359
|
+
errors.push(issue(error.code, error.message, [responsibility.path ?? taskArtifactPath(taskId, "responsibility")]));
|
|
360
|
+
}
|
|
361
|
+
} catch (error) {
|
|
362
|
+
errors.push(issue(error.code ?? "E_RESPONSIBILITY_INVALID", error.message, [taskArtifactPath(taskId, "responsibility")]));
|
|
363
|
+
responsibility = { status: "INVALID", taskId: contract?.value?.taskId ?? taskId, errors: [{ code: error.code ?? "E_RESPONSIBILITY_INVALID", message: error.message }] };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
293
366
|
|
|
294
367
|
if (state && hasExecutionStarted(state.phase)) {
|
|
295
368
|
const prerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot, taskId, statePath, contractPath, routePath });
|
|
@@ -508,10 +581,15 @@ export async function evaluateCompletion({
|
|
|
508
581
|
pending: durableActions.filter((action) => !["VERIFIED", "FAILED", "CANCELLED"].includes(action.state)).length,
|
|
509
582
|
actionRefs: durableActions.map((action) => action.actionId),
|
|
510
583
|
},
|
|
584
|
+
responsibility: {
|
|
585
|
+
status: responsibility.status,
|
|
586
|
+
...(responsibility.fingerprint ? { fingerprint: responsibility.fingerprint } : {}),
|
|
587
|
+
},
|
|
588
|
+
attestation,
|
|
511
589
|
};
|
|
512
590
|
}
|
|
513
591
|
|
|
514
|
-
|
|
592
|
+
async function runCompleteInternal({
|
|
515
593
|
target,
|
|
516
594
|
packageRoot,
|
|
517
595
|
strict = false,
|
|
@@ -631,6 +709,7 @@ export async function runComplete({
|
|
|
631
709
|
const state = await readWorkState(target, { packageRoot, taskId, statePath });
|
|
632
710
|
if (state && state.phase !== "COMPLETE") {
|
|
633
711
|
const receipt = await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
|
|
712
|
+
const contract = await readContract(target, packageRoot, { taskId, contractPath });
|
|
634
713
|
const next = {
|
|
635
714
|
...state,
|
|
636
715
|
previousPhase: state.phase,
|
|
@@ -647,6 +726,47 @@ export async function runComplete({
|
|
|
647
726
|
stateFingerprint: canonicalFingerprint(next),
|
|
648
727
|
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
649
728
|
}, packageRoot, { target, taskId: state.taskId, authorityContext, runtimeContext });
|
|
729
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId, eventsPath });
|
|
730
|
+
const existingCompletion = ledger.events.find((event) => event.taskId === contract.value.taskId && event.event === "COMPLETION_VALIDATED");
|
|
731
|
+
const completionEvent = existingCompletion ?? await previewProtocolEvent(target, {
|
|
732
|
+
taskId: contract.value.taskId,
|
|
733
|
+
event: "COMPLETION_VALIDATED",
|
|
734
|
+
}, packageRoot, { taskId, eventsPath });
|
|
735
|
+
let manifestCapture = null;
|
|
736
|
+
const attestationConfig = result.attestation ?? { mode: "off", revisionProvider: "git" };
|
|
737
|
+
if (attestationConfig.mode !== "off") {
|
|
738
|
+
try {
|
|
739
|
+
manifestCapture = await createCodeManifest({
|
|
740
|
+
target,
|
|
741
|
+
packageRoot,
|
|
742
|
+
taskId: contract.value.taskId,
|
|
743
|
+
revisionProvider: attestationConfig.revisionProvider,
|
|
744
|
+
baseRevision: state.repositoryFingerprint?.head ?? null,
|
|
745
|
+
headRevision: "WORKTREE",
|
|
746
|
+
captureMode: "WORKTREE",
|
|
747
|
+
candidateState: next,
|
|
748
|
+
candidateReceipt: nextReceipt,
|
|
749
|
+
candidateCompletionCheckpoint: { seq: completionEvent.seq, hash: completionEvent.hash },
|
|
750
|
+
});
|
|
751
|
+
result.attestation = {
|
|
752
|
+
...result.attestation,
|
|
753
|
+
status: "CAPTURED",
|
|
754
|
+
level: "VERIFIED",
|
|
755
|
+
manifest: {
|
|
756
|
+
path: taskCodeManifestPath(contract.value.taskId),
|
|
757
|
+
fingerprint: manifestCapture.fingerprint,
|
|
758
|
+
contentDigest: manifestCapture.manifest.contentDigest,
|
|
759
|
+
},
|
|
760
|
+
};
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (attestationConfig.mode === "required") throw error;
|
|
763
|
+
result.attestation = {
|
|
764
|
+
...result.attestation,
|
|
765
|
+
status: "UNAVAILABLE",
|
|
766
|
+
error: { code: error.code ?? "E_ATTESTATION_MANIFEST_INVALID", message: error.message },
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
}
|
|
650
770
|
await mutateWorkState(target, {
|
|
651
771
|
expectedRevision: state.revision ?? 0,
|
|
652
772
|
packageRoot,
|
|
@@ -654,12 +774,54 @@ export async function runComplete({
|
|
|
654
774
|
statePath,
|
|
655
775
|
}, () => next);
|
|
656
776
|
await writeJsonArtifact(target, receiptRel, nextReceipt, "execution-receipt", packageRoot);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
777
|
+
if (!existingCompletion) {
|
|
778
|
+
const persistedCompletion = await appendProtocolEvent(target, {
|
|
779
|
+
taskId: contract.value.taskId,
|
|
780
|
+
event: "COMPLETION_VALIDATED",
|
|
781
|
+
at: completionEvent.at,
|
|
782
|
+
}, packageRoot, { taskId, eventsPath });
|
|
783
|
+
if (persistedCompletion.hash !== completionEvent.hash) {
|
|
784
|
+
throw new Error("Precomputed completion event changed before persistence");
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (manifestCapture) {
|
|
788
|
+
const manifestArtifact = await writeCodeManifest({
|
|
789
|
+
target,
|
|
790
|
+
packageRoot,
|
|
791
|
+
taskId: contract.value.taskId,
|
|
792
|
+
manifest: manifestCapture.manifest,
|
|
793
|
+
});
|
|
794
|
+
await appendProtocolEvent(target, {
|
|
795
|
+
taskId: contract.value.taskId,
|
|
796
|
+
event: "CODE_MANIFEST_CAPTURED",
|
|
797
|
+
fingerprint: manifestArtifact.fingerprint,
|
|
798
|
+
details: {
|
|
799
|
+
manifestFingerprint: manifestArtifact.fingerprint,
|
|
800
|
+
contentDigest: manifestCapture.manifest.contentDigest,
|
|
801
|
+
coveredPaths: new Set(manifestCapture.manifest.entries.flatMap((entry) => [entry.path, entry.sourcePath].filter(Boolean))).size,
|
|
802
|
+
verificationCycle: manifestCapture.manifest.verificationCycle,
|
|
803
|
+
},
|
|
804
|
+
}, packageRoot, { taskId, eventsPath });
|
|
805
|
+
}
|
|
806
|
+
} else if (state?.phase === "COMPLETE" && result.attestation?.status === "MISSING" && result.attestation.mode === "required") {
|
|
807
|
+
const error = new Error("Required code manifest is missing from a COMPLETE task");
|
|
808
|
+
error.code = "E_ATTESTATION_MANIFEST_MISSING";
|
|
809
|
+
throw error;
|
|
662
810
|
}
|
|
663
811
|
}
|
|
664
812
|
return result;
|
|
665
813
|
}
|
|
814
|
+
|
|
815
|
+
export async function runComplete(options = {}) {
|
|
816
|
+
const taskId = options.taskId ?? null;
|
|
817
|
+
if (options.persist !== false && taskId && !getActiveTaskTransaction()) {
|
|
818
|
+
return withTaskTransaction({
|
|
819
|
+
target: options.target,
|
|
820
|
+
taskId,
|
|
821
|
+
packageRoot: options.packageRoot,
|
|
822
|
+
operation: "complete",
|
|
823
|
+
recordCommitEvent: true,
|
|
824
|
+
}, () => runCompleteInternal(options));
|
|
825
|
+
}
|
|
826
|
+
return runCompleteInternal(options);
|
|
827
|
+
}
|
package/src/core/config.js
CHANGED
|
@@ -1,20 +1,76 @@
|
|
|
1
1
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
2
2
|
import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
3
|
+
import { E_ATTESTATION_CONFIGURATION_INVALID } from "./error-codes.js";
|
|
4
|
+
import { normalizeVerificationConfiguration } from "./verification-scope-capability.js";
|
|
3
5
|
|
|
4
6
|
export const CONFIG_SCHEMA_VERSION = 1;
|
|
5
7
|
export const COMPLIANCE_MODES = Object.freeze(["advisory", "standard", "strict"]);
|
|
8
|
+
export const ATTESTATION_MODES = Object.freeze(["off", "optional", "required"]);
|
|
6
9
|
|
|
7
10
|
function stringArray(value, label) {
|
|
8
11
|
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
|
|
9
|
-
|
|
12
|
+
const error = new Error(`${label} must be an array of non-empty strings`);
|
|
13
|
+
error.code = E_ATTESTATION_CONFIGURATION_INVALID;
|
|
14
|
+
throw error;
|
|
10
15
|
}
|
|
11
16
|
return [...new Set(value)];
|
|
12
17
|
}
|
|
13
18
|
|
|
19
|
+
function configurationError(message) {
|
|
20
|
+
const error = new Error(message);
|
|
21
|
+
error.code = E_ATTESTATION_CONFIGURATION_INVALID;
|
|
22
|
+
return error;
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
export function createConfig(input = {}) {
|
|
15
26
|
const complianceMode = input.complianceMode ?? "standard";
|
|
16
27
|
if (!COMPLIANCE_MODES.includes(complianceMode)) {
|
|
17
|
-
throw
|
|
28
|
+
throw configurationError(`Unknown compliance mode: ${complianceMode}`);
|
|
29
|
+
}
|
|
30
|
+
let attestation;
|
|
31
|
+
if (input.attestation !== undefined) {
|
|
32
|
+
if (!input.attestation || typeof input.attestation !== "object" || Array.isArray(input.attestation)) {
|
|
33
|
+
throw configurationError("attestation must be an object");
|
|
34
|
+
}
|
|
35
|
+
const mode = input.attestation.mode ?? "optional";
|
|
36
|
+
if (!ATTESTATION_MODES.includes(mode)) throw configurationError(`Unknown attestation mode: ${mode}`);
|
|
37
|
+
const revisionProvider = input.attestation.revisionProvider ?? "git";
|
|
38
|
+
if (typeof revisionProvider !== "string" || !revisionProvider.trim()) throw configurationError("attestation.revisionProvider must be a non-empty string");
|
|
39
|
+
const signing = input.attestation.signing ?? {};
|
|
40
|
+
if (!signing || typeof signing !== "object" || Array.isArray(signing)) {
|
|
41
|
+
throw configurationError("attestation.signing must be an object");
|
|
42
|
+
}
|
|
43
|
+
const provider = signing.provider ?? "none";
|
|
44
|
+
if (!["none", "sigstore"].includes(provider)) throw configurationError(`Unknown attestation signing provider: ${provider}`);
|
|
45
|
+
const policy = signing.policy ?? {};
|
|
46
|
+
if (!policy || typeof policy !== "object" || Array.isArray(policy)) {
|
|
47
|
+
throw configurationError("attestation.signing.policy must be an object");
|
|
48
|
+
}
|
|
49
|
+
if (policy.issuer !== undefined && typeof policy.issuer !== "string") throw configurationError("attestation.signing.policy.issuer must be a string");
|
|
50
|
+
if (policy.identities !== undefined) stringArray(policy.identities, "attestation.signing.policy.identities");
|
|
51
|
+
attestation = {
|
|
52
|
+
mode,
|
|
53
|
+
revisionProvider,
|
|
54
|
+
requireCompleteCoverage: input.attestation.requireCompleteCoverage === true,
|
|
55
|
+
coverage: { exclude: stringArray(input.attestation.coverage?.exclude ?? [], "attestation.coverage.exclude") },
|
|
56
|
+
signing: {
|
|
57
|
+
provider,
|
|
58
|
+
required: signing.required === true,
|
|
59
|
+
policy: {
|
|
60
|
+
...(policy.issuer !== undefined ? { issuer: policy.issuer } : {}),
|
|
61
|
+
identities: [...(policy.identities ?? [])],
|
|
62
|
+
requireTransparencyLog: policy.requireTransparencyLog !== false,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
let verification;
|
|
68
|
+
if (input.verification !== undefined) {
|
|
69
|
+
try {
|
|
70
|
+
verification = normalizeVerificationConfiguration(input.verification);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw configurationError(error.message);
|
|
73
|
+
}
|
|
18
74
|
}
|
|
19
75
|
return {
|
|
20
76
|
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
@@ -23,6 +79,8 @@ export function createConfig(input = {}) {
|
|
|
23
79
|
...(input.policy !== undefined ? { policy: input.policy } : {}),
|
|
24
80
|
...(input.requiredGates !== undefined ? { requiredGates: stringArray(input.requiredGates, "requiredGates") } : {}),
|
|
25
81
|
...(input.requiredEvidence !== undefined ? { requiredEvidence: stringArray(input.requiredEvidence, "requiredEvidence") } : {}),
|
|
82
|
+
...(verification ? { verification } : {}),
|
|
83
|
+
...(attestation ? { attestation } : {}),
|
|
26
84
|
};
|
|
27
85
|
}
|
|
28
86
|
|
|
@@ -8,6 +8,12 @@ const OPTION_FIELDS = Object.freeze({
|
|
|
8
8
|
"--resume-note": ["continuityResumeNote", false],
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
+
function continuityInputError(message) {
|
|
12
|
+
const error = new Error(message);
|
|
13
|
+
error.code = "E_CLI_INVOCATION_INVALID";
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
export function continuityOptionDefaults() {
|
|
12
18
|
return {
|
|
13
19
|
continuityFocusId: null,
|
|
@@ -24,7 +30,7 @@ export function consumeContinuityOption({ argument, argv, index, options }) {
|
|
|
24
30
|
const definition = OPTION_FIELDS[argument];
|
|
25
31
|
if (!definition) return { handled: false, index };
|
|
26
32
|
const value = argv[index + 1];
|
|
27
|
-
if (!value || value.startsWith("-")) throw
|
|
33
|
+
if (!value || value.startsWith("-")) throw continuityInputError(`${argument} requires a value`);
|
|
28
34
|
const [field, repeatable] = definition;
|
|
29
35
|
if (repeatable) options[field].push(value);
|
|
30
36
|
else options[field] = value;
|
|
@@ -45,13 +51,13 @@ export function hasContinuityOptions(options = {}) {
|
|
|
45
51
|
|
|
46
52
|
export function validateContinuityOptions(command, options = {}) {
|
|
47
53
|
if (command !== "record-continuity" && hasContinuityOptions(options)) {
|
|
48
|
-
throw
|
|
54
|
+
throw continuityInputError(`Continuity recording options are not valid for ${command}`);
|
|
49
55
|
}
|
|
50
56
|
if (command === "record-continuity") {
|
|
51
57
|
const hasFocusId = Boolean(options.continuityFocusId);
|
|
52
58
|
const hasFocusSummary = Boolean(options.continuityFocusSummary);
|
|
53
59
|
if (hasFocusId !== hasFocusSummary) {
|
|
54
|
-
throw
|
|
60
|
+
throw continuityInputError("record-continuity requires --focus-id and --focus-summary together");
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
return options;
|
|
@@ -177,6 +177,7 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
|
|
|
177
177
|
]);
|
|
178
178
|
|
|
179
179
|
const state = await readWorkState(target, { packageRoot, taskId });
|
|
180
|
+
const latestHandoff = await resolveLatestHandoffSafe(target, { packageRoot, taskId: state?.taskId ?? taskId });
|
|
180
181
|
let continuityArtifact;
|
|
181
182
|
try {
|
|
182
183
|
continuityArtifact = await readContinuity(target, { packageRoot, taskId });
|
|
@@ -186,6 +187,7 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
|
|
|
186
187
|
...classifyContinuity({ continuity: null, state }),
|
|
187
188
|
path: ".forgeloop/continuity.json",
|
|
188
189
|
present: false,
|
|
190
|
+
latestHandoff,
|
|
189
191
|
diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
|
|
190
192
|
};
|
|
191
193
|
}
|
|
@@ -197,6 +199,7 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
|
|
|
197
199
|
path: ".forgeloop/continuity.json",
|
|
198
200
|
present: true,
|
|
199
201
|
error: error.message,
|
|
202
|
+
latestHandoff,
|
|
200
203
|
};
|
|
201
204
|
}
|
|
202
205
|
|
|
@@ -225,10 +228,27 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
|
|
|
225
228
|
present: true,
|
|
226
229
|
fingerprint: continuityArtifact.fingerprint,
|
|
227
230
|
continuity: continuityArtifact.value,
|
|
231
|
+
latestHandoff,
|
|
228
232
|
diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
|
|
229
233
|
};
|
|
230
234
|
}
|
|
231
235
|
|
|
236
|
+
async function resolveLatestHandoffSafe(target, { packageRoot, taskId } = {}) {
|
|
237
|
+
if (!taskId) return null;
|
|
238
|
+
try {
|
|
239
|
+
const { resolveLatestHandoff } = await import("./handoff.js");
|
|
240
|
+
return await resolveLatestHandoff(target, { packageRoot, taskId });
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
status: "INVALID",
|
|
244
|
+
error: {
|
|
245
|
+
code: error.code ?? "E_HANDOFF_INVALID",
|
|
246
|
+
message: error.message,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
232
252
|
async function deriveDiagnosticContextSafe({ target, packageRoot, state }) {
|
|
233
253
|
try {
|
|
234
254
|
const [{ readEvents }, { deriveDiagnosticContext }] = await Promise.all([
|
|
@@ -30,7 +30,9 @@ export function resolveCurrentCycleDiagnostic(events, taskId, verificationCycle)
|
|
|
30
30
|
const candidates = diagnosticEventsForTask(events, taskId)
|
|
31
31
|
.filter((event) => {
|
|
32
32
|
const cycle = event.details?.verificationCycle;
|
|
33
|
-
return verificationCycle
|
|
33
|
+
return (verificationCycle === null || verificationCycle === undefined)
|
|
34
|
+
|| (cycle === null || cycle === undefined)
|
|
35
|
+
|| cycle === verificationCycle;
|
|
34
36
|
})
|
|
35
37
|
.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
|
|
36
38
|
|