@cassiomc1/forgeloop 1.10.0 → 1.10.1
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 +9 -0
- package/LOOP_ENGINEERING.md +4 -4
- package/LOOP_SYSTEM_DESIGN.md +23 -0
- package/QUALITY_SCORECARD.md +2 -2
- package/README.md +13 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +1 -1
- package/docs/ARTIFACT_REFERENCE.md +15 -0
- package/docs/CODE_ATTESTATION.md +9 -0
- package/docs/DOCUMENTATION_GUIDE.md +4 -4
- package/docs/EXECUTION_PROFILE_BENCHMARKS.md +10 -0
- package/docs/MCP.md +13 -1
- package/docs/PACKAGE_CONTENTS.md +83 -0
- package/docs/RELEASE_CHECKLIST.md +8 -0
- package/docs/REVISION_PROVIDERS.md +9 -0
- package/docs/TROUBLESHOOTING.md +12 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +13 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +10 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +22 -11
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +17 -8
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +14 -3
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +10 -1
- package/docs/diagrams/README.md +17 -0
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +383 -57
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +374 -55
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +328 -47
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/package.json +13 -4
- package/scripts/CI_VALIDATORS.md +24 -0
- package/scripts/check-critical-coverage.mjs +9 -0
- package/src/commands/doctor.js +11 -10
- package/src/core/actions.js +2 -2
- package/src/core/approvals.js +2 -2
- package/src/core/artifacts.js +3 -3
- package/src/core/checks.js +0 -33
- package/src/core/completion.js +2 -2
- package/src/core/events.js +5 -5
- package/src/core/execution-profile.js +18 -5
- package/src/core/next-action-pending-actions.js +255 -0
- package/src/core/next-action-phases.js +26 -764
- package/src/core/next-action-planned-phase.js +51 -0
- package/src/core/next-action-quality-guidance.js +19 -0
- package/src/core/next-action-recovery-phases.js +97 -0
- package/src/core/next-action-refresh.js +20 -0
- package/src/core/next-action-review-phase.js +189 -0
- package/src/core/next-action-verification-phase.js +192 -0
- package/src/core/task-recovery.js +2 -2
- package/src/core/transaction-maintenance.js +70 -0
- package/src/core/transaction.js +31 -10
- package/src/core/work-state.js +5 -5
- package/src/integration.d.ts +19 -3
- package/src/integration.js +1 -0
- package/src/core/cli-metadata.js +0 -23
- package/src/core/decision-classification.js +0 -55
- package/src/core/gates.js +0 -57
- package/src/core/workflow-compatibility.js +0 -151
package/src/core/artifacts.js
CHANGED
|
@@ -5,7 +5,7 @@ import { assertSecretFree } from "./receipt.js";
|
|
|
5
5
|
import { assertJsonBytes, assertJsonLimits } from "./json-safety.js";
|
|
6
6
|
import { assertSchema, readSchema } from "./schema-validation.js";
|
|
7
7
|
import { getPackageRoot } from "./templates.js";
|
|
8
|
-
import {
|
|
8
|
+
import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
|
|
9
9
|
|
|
10
10
|
export const ARTIFACT_PATHS = Object.freeze({
|
|
11
11
|
contract: ".forgeloop/current-contract.json",
|
|
@@ -76,7 +76,7 @@ export async function readJsonArtifact(
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
const artifactPath = ensureWithin(target, relativePath);
|
|
79
|
-
const transaction =
|
|
79
|
+
const transaction = (await getTaskTransaction(target));
|
|
80
80
|
const stagedText = transaction ? await transaction.readText(relativePath) : null;
|
|
81
81
|
if (stagedText === null && !(await fileExists(artifactPath))) {
|
|
82
82
|
throw new ArtifactError(
|
|
@@ -124,7 +124,7 @@ export async function writeJsonArtifact(
|
|
|
124
124
|
} catch (error) {
|
|
125
125
|
throw artifactError("ARTIFACT_PATH_INVALID", relativePath, error);
|
|
126
126
|
}
|
|
127
|
-
const activeTransaction =
|
|
127
|
+
const activeTransaction = (await getTaskTransaction(target));
|
|
128
128
|
if (!activeTransaction && taskId && !dryRun) {
|
|
129
129
|
return withTaskTransaction({ target, taskId, operation, packageRoot }, async () => (
|
|
130
130
|
writeJsonArtifact(target, relativePath, value, schemaName, packageRoot, { dryRun, taskId, operation })
|
package/src/core/checks.js
CHANGED
|
@@ -134,36 +134,3 @@ export function assertCheckList(value, label = "checks", options = {}) {
|
|
|
134
134
|
});
|
|
135
135
|
return value;
|
|
136
136
|
}
|
|
137
|
-
|
|
138
|
-
function requiredChecksSatisfiedBy(checks, requiredValues, selector, { allowInferred = false } = {}) {
|
|
139
|
-
assertCheckList(checks);
|
|
140
|
-
if (!Array.isArray(requiredValues)) throw checkError("E_CHECK_INVALID", "required values must be an array");
|
|
141
|
-
const errors = [];
|
|
142
|
-
for (const value of requiredValues) {
|
|
143
|
-
const candidates = checks.filter((check) => selector(check) === value);
|
|
144
|
-
const check = candidates.find((candidate) => candidate.status === "passed"
|
|
145
|
-
&& (allowInferred || candidate.evidenceKind === "OBSERVED"))
|
|
146
|
-
?? candidates.find((candidate) => candidate.status === "passed")
|
|
147
|
-
?? candidates[0];
|
|
148
|
-
if (!check) {
|
|
149
|
-
errors.push(checkError("E_EVIDENCE_REQUIRED", `Required check is missing: ${value}`, [value]));
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if (check.status !== "passed") {
|
|
153
|
-
errors.push(checkError("E_EVIDENCE_REQUIRED", `Required check is not passed: ${value}`, [value]));
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (!allowInferred && check.evidenceKind !== "OBSERVED") {
|
|
157
|
-
errors.push(checkError("E_EVIDENCE_KIND_INVALID", `Required check must be observed: ${value}`, [value]));
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return errors;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function requiredChecksSatisfied(checks, requiredIds, options = {}) {
|
|
164
|
-
return requiredChecksSatisfiedBy(checks, requiredIds, (check) => check.id, options);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function requiredChecksSatisfiedForRequirements(checks, requiredRequirements, options = {}) {
|
|
168
|
-
return requiredChecksSatisfiedBy(checks, requiredRequirements, (check) => check.requirement, options);
|
|
169
|
-
}
|
package/src/core/completion.js
CHANGED
|
@@ -17,7 +17,7 @@ import { listActions } from "./actions.js";
|
|
|
17
17
|
import { readConfig } from "./config.js";
|
|
18
18
|
import { resolveResponsibilityStatus } from "./responsibility.js";
|
|
19
19
|
import { createCodeManifest, readCodeManifest, validateCodeManifestBindings, writeCodeManifest } from "./code-manifest.js";
|
|
20
|
-
import {
|
|
20
|
+
import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
|
|
21
21
|
import { validateStructuralQualityCheckProvenance } from "./structural-quality/service.js";
|
|
22
22
|
|
|
23
23
|
async function attestationConfiguration(target, packageRoot, errors) {
|
|
@@ -856,7 +856,7 @@ async function runCompleteInternal({
|
|
|
856
856
|
|
|
857
857
|
export async function runComplete(options = {}) {
|
|
858
858
|
const taskId = options.taskId ?? null;
|
|
859
|
-
if (options.persist !== false && taskId && !
|
|
859
|
+
if (options.persist !== false && taskId && !(await getTaskTransaction(options.target))) {
|
|
860
860
|
return withTaskTransaction({
|
|
861
861
|
target: options.target,
|
|
862
862
|
taskId,
|
package/src/core/events.js
CHANGED
|
@@ -11,7 +11,7 @@ import { PROTOCOL_VERSION } from "./protocol.js";
|
|
|
11
11
|
import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
|
|
12
12
|
|
|
13
13
|
import { taskArtifactPath } from "./task-paths.js";
|
|
14
|
-
import {
|
|
14
|
+
import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
|
|
15
15
|
|
|
16
16
|
import { assertDiagnosisDetails } from "./diagnosis-model.js";
|
|
17
17
|
import {
|
|
@@ -311,7 +311,7 @@ export function buildProtocolEvent(input, { checkpoint } = {}) {
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
export async function previewProtocolEvent(target, input, packageRoot, options = {}) {
|
|
314
|
-
const activeTransaction =
|
|
314
|
+
const activeTransaction = (await getTaskTransaction(target));
|
|
315
315
|
if (!activeTransaction) {
|
|
316
316
|
return withTaskTransaction({
|
|
317
317
|
target,
|
|
@@ -337,7 +337,7 @@ export async function readEvents(target, packageRoot, options = {}) {
|
|
|
337
337
|
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
338
338
|
await assertSafePath(target, relPath);
|
|
339
339
|
const eventsPath = ensureWithin(target, relPath);
|
|
340
|
-
const transaction =
|
|
340
|
+
const transaction = (await getTaskTransaction(target));
|
|
341
341
|
if (transaction) {
|
|
342
342
|
const stagedText = await transaction.readText(relPath);
|
|
343
343
|
if (stagedText !== null) return parseEventsText(stagedText, relPath, packageRoot);
|
|
@@ -355,7 +355,7 @@ export async function readEventTail(target, packageRoot, options = {}) {
|
|
|
355
355
|
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
356
356
|
await assertSafePath(target, relPath);
|
|
357
357
|
const eventsPath = ensureWithin(target, relPath);
|
|
358
|
-
const transaction =
|
|
358
|
+
const transaction = (await getTaskTransaction(target));
|
|
359
359
|
if (transaction) {
|
|
360
360
|
const stagedText = await transaction.readText(relPath);
|
|
361
361
|
if (stagedText !== null) {
|
|
@@ -411,7 +411,7 @@ async function parseEventsText(text, relPath, packageRoot) {
|
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
export async function appendProtocolEvent(target, input, packageRoot, options = {}) {
|
|
414
|
-
const activeTransaction =
|
|
414
|
+
const activeTransaction = (await getTaskTransaction(target));
|
|
415
415
|
if (typeof input?.taskId !== "string" || !input.taskId) throw protocolError("E_EVENT_INVALID", "event taskId is required");
|
|
416
416
|
if (typeof input?.event !== "string" || !input.event) throw protocolError("E_EVENT_INVALID", "event type is required");
|
|
417
417
|
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
@@ -66,24 +66,37 @@ function contractCollections(contract) {
|
|
|
66
66
|
|
|
67
67
|
function contractSignals(contract) {
|
|
68
68
|
const collections = contractCollections(contract);
|
|
69
|
+
const obligations = [];
|
|
70
|
+
const types = new Set();
|
|
71
|
+
function collect(requirement) {
|
|
72
|
+
if (typeof requirement === "string") obligations.push(requirement);
|
|
73
|
+
else if (requirement && typeof requirement === "object") {
|
|
74
|
+
if (typeof requirement.text === "string") obligations.push(requirement.text);
|
|
75
|
+
types.add(requirement.type);
|
|
76
|
+
if (Array.isArray(requirement.requirements)) requirement.requirements.forEach(collect);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
collections.successCriteria.forEach(collect);
|
|
80
|
+
if (Array.isArray(contract?.verification)) contract.verification.forEach(collect);
|
|
69
81
|
const riskText = collections.risks.join(" ").toLowerCase();
|
|
82
|
+
// Constraints and stop conditions describe boundaries, not obligations.
|
|
83
|
+
// Declared route risks remain authoritative even when a constraint excludes
|
|
84
|
+
// an operation; do not try to interpret prose negation as authorization.
|
|
70
85
|
const allText = [
|
|
71
86
|
...collections.deliverables,
|
|
72
|
-
...
|
|
73
|
-
...collections.constraints,
|
|
74
|
-
...collections.stopConditions,
|
|
87
|
+
...obligations,
|
|
75
88
|
].join(" ").toLowerCase();
|
|
76
89
|
const combinedText = `${riskText} ${allText}`;
|
|
77
90
|
const signals = {
|
|
78
91
|
secrets: /\bsecrets?\b|\bcredentials?\b|\bpasswords?\b|\bprivate keys?\b/.test(combinedText),
|
|
79
92
|
personalData: /\bpersonal[- ]data\b|\bpii\b|\bsensitive data\b/.test(combinedText),
|
|
80
|
-
publication: /\bpublication\b|\bpublish(?:ing|ed)?\b|\bdeploy(?:ment|ed)?\b/.test(combinedText),
|
|
93
|
+
publication: types.has("PUBLICATION") || /\bpublication\b|\bpublish(?:ing|ed)?\b|\bdeploy(?:ment|ed)?\b/.test(combinedText),
|
|
81
94
|
destructive: /\bdestructive\b|\birreversible\b|\bdrop database\b|\bdelete production\b/.test(combinedText),
|
|
82
95
|
migration: /\bmigration\b|\bmigrate\b|\bschema change\b|\birreversible persistence\b/.test(combinedText),
|
|
83
96
|
payment: /\bpayments?\b|\bcheckout\b|\bbilling\b/.test(combinedText),
|
|
84
97
|
externalMutation: /\bexternal mutation\b|\bmutate(?:s|d)? external\b|\bpublish(?:ing|ed)?\b/.test(combinedText),
|
|
85
98
|
authoritySensitiveExternalMutation: /\bauthority[- ]sensitive\b|\bhost[- ]authorized external\b|\bexternal mutation requiring authority\b/.test(combinedText),
|
|
86
|
-
broadProductionValidation: /\bbroad production validation\b|\bproduction validation\b|\bvalidate in production\b/.test(combinedText),
|
|
99
|
+
broadProductionValidation: types.has("PRODUCTION_READINESS") || /\bbroad production validation\b|\bproduction validation\b|\bvalidate in production\b/.test(combinedText),
|
|
87
100
|
};
|
|
88
101
|
return signals;
|
|
89
102
|
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { taskArtifactPath } from "./task-paths.js";
|
|
2
|
+
import { NEXT_ACTIONS, result } from "./next-action-model.js";
|
|
3
|
+
import { artifactError } from "./next-action-artifacts.js";
|
|
4
|
+
import { listActions } from "./actions.js";
|
|
5
|
+
import { listApprovals } from "./approvals.js";
|
|
6
|
+
import { loadPolicyIdentity } from "./policy-engine.js";
|
|
7
|
+
|
|
8
|
+
export async function resolvePendingActionGuidance({ target, packageRoot, state, context, eventsRel, authorityContext, runtimeContext, helpers }) {
|
|
9
|
+
const {
|
|
10
|
+
capabilityDecisionMetadata,
|
|
11
|
+
actionApprovalGuidance,
|
|
12
|
+
actionAuthorityGuidance,
|
|
13
|
+
actionApprovalResolutionGuidance,
|
|
14
|
+
actionDeniedGuidance,
|
|
15
|
+
actionPolicyEpochGuidance,
|
|
16
|
+
authorizeActionGuidance,
|
|
17
|
+
evaluateProposedActionCapability,
|
|
18
|
+
findApplicablePendingApproval,
|
|
19
|
+
} = helpers;
|
|
20
|
+
let actionsForApproval;
|
|
21
|
+
try {
|
|
22
|
+
actionsForApproval = await listActions(target, { packageRoot, taskId: state.taskId });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error.code === "E_ACTION_INVALID") {
|
|
25
|
+
return result({
|
|
26
|
+
...context,
|
|
27
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
28
|
+
commands: [],
|
|
29
|
+
reasons: [artifactError(
|
|
30
|
+
error.code,
|
|
31
|
+
`A durable action artifact is invalid and cannot be offered for authorization: ${error.message}`,
|
|
32
|
+
[taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
33
|
+
)],
|
|
34
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
const ambiguousAction = actionsForApproval.find((action) => action.state === "COMMIT_UNKNOWN");
|
|
40
|
+
if (ambiguousAction) {
|
|
41
|
+
return result({ ...context, nextAction: NEXT_ACTIONS.RECONCILE_ACTION,
|
|
42
|
+
commands: [`forgeloop action-reconcile --task ${state.taskId} --action ${ambiguousAction.actionId} --outcome UNKNOWN`],
|
|
43
|
+
reasons: [artifactError("E_ACTION_RECONCILIATION_REQUIRED",
|
|
44
|
+
`Action ${ambiguousAction.actionId} has an unknown external commit outcome; retry is forbidden until reconciliation.`)],
|
|
45
|
+
reconciliationAuthorityRequired: {
|
|
46
|
+
outcomesRequiringTrust: ["COMMITTED", "NOT_COMMITTED"],
|
|
47
|
+
reason: "Recording an UNKNOWN observation is safe from any surface; settling COMMITTED or NOT_COMMITTED requires a trusted host boundary and evidence.",
|
|
48
|
+
},
|
|
49
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel] });
|
|
50
|
+
}
|
|
51
|
+
// A PROPOSED required action ready for authorization should use the
|
|
52
|
+
// canonical authorization surface; host/approval blockers remain structured.
|
|
53
|
+
const authorizableAction = actionsForApproval.find((action) => action.requiredForCompletion
|
|
54
|
+
&& action.state === "PROPOSED");
|
|
55
|
+
if (authorizableAction) {
|
|
56
|
+
let policyIdentity;
|
|
57
|
+
try {
|
|
58
|
+
policyIdentity = await loadPolicyIdentity(target, packageRoot, state.taskId);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return actionPolicyEpochGuidance({
|
|
61
|
+
context,
|
|
62
|
+
state,
|
|
63
|
+
action: authorizableAction,
|
|
64
|
+
policyIdentity: {
|
|
65
|
+
code: error.code ?? "E_POLICY_INVALID",
|
|
66
|
+
message: `Capability policy epoch validation failed for required action ${authorizableAction.actionId}: ${error.message}`,
|
|
67
|
+
},
|
|
68
|
+
eventsRel,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (policyIdentity.status !== "VALID") {
|
|
72
|
+
return actionPolicyEpochGuidance({
|
|
73
|
+
context,
|
|
74
|
+
state,
|
|
75
|
+
action: authorizableAction,
|
|
76
|
+
policyIdentity,
|
|
77
|
+
eventsRel,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let capability;
|
|
82
|
+
try {
|
|
83
|
+
capability = await evaluateProposedActionCapability({
|
|
84
|
+
target,
|
|
85
|
+
packageRoot,
|
|
86
|
+
action: authorizableAction,
|
|
87
|
+
authorityContext,
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return result({
|
|
91
|
+
...context,
|
|
92
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
93
|
+
commands: [],
|
|
94
|
+
reasons: [artifactError(
|
|
95
|
+
error.code ?? "E_ACTION_CAPABILITY_INVALID",
|
|
96
|
+
`Capability policy evaluation failed for required action ${authorizableAction.actionId}: ${error.message}`,
|
|
97
|
+
[taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
98
|
+
)],
|
|
99
|
+
capabilityDecision: {
|
|
100
|
+
capability: authorizableAction.capability,
|
|
101
|
+
decision: "DENY",
|
|
102
|
+
reasonCode: error.code ?? "E_ACTION_CAPABILITY_INVALID",
|
|
103
|
+
policyFingerprint: null,
|
|
104
|
+
},
|
|
105
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (capability.decision === "REQUIRE_AUTHORITY") {
|
|
109
|
+
return actionAuthorityGuidance({
|
|
110
|
+
context,
|
|
111
|
+
state,
|
|
112
|
+
action: authorizableAction,
|
|
113
|
+
capability,
|
|
114
|
+
eventsRel,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (capability.allowed) {
|
|
118
|
+
return authorizeActionGuidance({
|
|
119
|
+
context,
|
|
120
|
+
state,
|
|
121
|
+
action: authorizableAction,
|
|
122
|
+
capability,
|
|
123
|
+
eventsRel,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (capability.decision === "REQUIRE_APPROVAL") {
|
|
127
|
+
let approvals;
|
|
128
|
+
try {
|
|
129
|
+
approvals = await listApprovals(target, { packageRoot, taskId: state.taskId });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return result({
|
|
132
|
+
...context,
|
|
133
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
134
|
+
commands: [],
|
|
135
|
+
reasons: [artifactError(
|
|
136
|
+
error.code ?? "E_APPROVAL_INVALID",
|
|
137
|
+
`Approval artifact inspection failed for required action ${authorizableAction.actionId}: ${error.message}`,
|
|
138
|
+
[taskArtifactPath(state.taskId, "approvals"), eventsRel],
|
|
139
|
+
)],
|
|
140
|
+
capabilityDecision: capabilityDecisionMetadata(authorizableAction, capability),
|
|
141
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "approvals"), eventsRel],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
capability = await evaluateProposedActionCapability({
|
|
146
|
+
target,
|
|
147
|
+
packageRoot,
|
|
148
|
+
action: authorizableAction,
|
|
149
|
+
approvals,
|
|
150
|
+
authorityContext,
|
|
151
|
+
});
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return result({
|
|
154
|
+
...context,
|
|
155
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
156
|
+
commands: [],
|
|
157
|
+
reasons: [artifactError(
|
|
158
|
+
error.code ?? "E_ACTION_CAPABILITY_INVALID",
|
|
159
|
+
`Capability policy evaluation failed for required action ${authorizableAction.actionId}: ${error.message}`,
|
|
160
|
+
[taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
161
|
+
)],
|
|
162
|
+
capabilityDecision: {
|
|
163
|
+
capability: authorizableAction.capability,
|
|
164
|
+
decision: "DENY",
|
|
165
|
+
reasonCode: error.code ?? "E_ACTION_CAPABILITY_INVALID",
|
|
166
|
+
policyFingerprint: null,
|
|
167
|
+
},
|
|
168
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (capability.allowed) {
|
|
172
|
+
return authorizeActionGuidance({
|
|
173
|
+
context,
|
|
174
|
+
state,
|
|
175
|
+
action: authorizableAction,
|
|
176
|
+
capability,
|
|
177
|
+
eventsRel,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (capability.decision !== "REQUIRE_APPROVAL") {
|
|
181
|
+
if (capability.decision === "REQUIRE_AUTHORITY") {
|
|
182
|
+
return actionAuthorityGuidance({
|
|
183
|
+
context,
|
|
184
|
+
state,
|
|
185
|
+
action: authorizableAction,
|
|
186
|
+
capability,
|
|
187
|
+
eventsRel,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (capability.allowed) {
|
|
191
|
+
return authorizeActionGuidance({
|
|
192
|
+
context,
|
|
193
|
+
state,
|
|
194
|
+
action: authorizableAction,
|
|
195
|
+
capability,
|
|
196
|
+
eventsRel,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return actionDeniedGuidance({
|
|
200
|
+
context,
|
|
201
|
+
state,
|
|
202
|
+
action: authorizableAction,
|
|
203
|
+
capability,
|
|
204
|
+
eventsRel,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const pendingApproval = await findApplicablePendingApproval({
|
|
208
|
+
target,
|
|
209
|
+
packageRoot,
|
|
210
|
+
taskId: state.taskId,
|
|
211
|
+
action: authorizableAction,
|
|
212
|
+
approvals,
|
|
213
|
+
});
|
|
214
|
+
if (pendingApproval) {
|
|
215
|
+
return actionApprovalResolutionGuidance({
|
|
216
|
+
context,
|
|
217
|
+
state,
|
|
218
|
+
action: authorizableAction,
|
|
219
|
+
approval: pendingApproval,
|
|
220
|
+
eventsRel,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return actionApprovalGuidance({
|
|
224
|
+
context,
|
|
225
|
+
state,
|
|
226
|
+
action: authorizableAction,
|
|
227
|
+
capability,
|
|
228
|
+
eventsRel,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return actionDeniedGuidance({
|
|
232
|
+
context,
|
|
233
|
+
state,
|
|
234
|
+
action: authorizableAction,
|
|
235
|
+
capability,
|
|
236
|
+
eventsRel,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// Committed-but-unverified required action needs canonical postcondition
|
|
240
|
+
// evidence before it can satisfy completion.
|
|
241
|
+
const committedActions = actionsForApproval;
|
|
242
|
+
const unverifiedRequired = committedActions.find((action) => action.requiredForCompletion
|
|
243
|
+
&& action.state === "COMMITTED");
|
|
244
|
+
if (unverifiedRequired) {
|
|
245
|
+
return result({ ...context, nextAction: NEXT_ACTIONS.VERIFY_EXTERNAL_ACTION,
|
|
246
|
+
commands: [
|
|
247
|
+
`forgeloop run-check --task ${state.taskId} --id check-postcondition --requirement <requirement> -- <independent verification argv>`,
|
|
248
|
+
`forgeloop action-verify --task ${state.taskId} --action ${unverifiedRequired.actionId} --evidence <execution-ref>`,
|
|
249
|
+
],
|
|
250
|
+
reasons: [artifactError("E_ACTION_VERIFICATION_REQUIRED",
|
|
251
|
+
`Committed action ${unverifiedRequired.actionId} requires independent canonical postcondition evidence; exit code 0 alone is not verification.`)],
|
|
252
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel] });
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|