@cassiomc1/forgeloop 1.5.0 → 1.6.0
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 +13 -8
- package/EXECUTION_STATE.md +20 -0
- package/LOOP_ENGINEERING.md +81 -0
- package/LOOP_SYSTEM_DESIGN.md +32 -0
- package/PROTOCOL_INTEGRATION.md +46 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +31 -9
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/THREAT_MODEL.md +39 -0
- package/docs/ARTIFACT_REFERENCE.md +140 -0
- package/docs/CLI_REFERENCE.md +294 -3
- package/docs/DIAGNOSTIC_MODEL.md +181 -0
- package/docs/DOCUMENTATION_GUIDE.md +22 -13
- package/docs/EXECUTION_TRACE.md +76 -0
- package/docs/MCP.md +33 -0
- package/docs/RECIPES.md +67 -0
- package/docs/TROUBLESHOOTING.md +106 -2
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
- package/docs/diagrams/README.md +55 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
- package/package.json +8 -6
- package/schemas/action.schema.json +100 -0
- package/schemas/approval.schema.json +51 -0
- package/schemas/capability-policy.schema.json +41 -0
- package/schemas/diagnostic-case.schema.json +85 -0
- package/schemas/execution-receipt.schema.json +16 -0
- package/schemas/hypothesis-disposition.schema.json +16 -0
- package/schemas/intervention.schema.json +27 -0
- package/schemas/policy-lock.schema.json +1 -0
- package/schemas/policy-snapshot.schema.json +2 -0
- package/schemas/trajectory-evaluation.schema.json +64 -0
- package/schemas/trajectory-scenario.schema.json +42 -0
- package/src/cli.js +94 -0
- package/src/commands/action-authorize.js +41 -0
- package/src/commands/action-propose.js +10 -0
- package/src/commands/action-reconcile.js +10 -0
- package/src/commands/action-record.js +47 -0
- package/src/commands/action-show.js +10 -0
- package/src/commands/action-verify.js +10 -0
- package/src/commands/advance.js +7 -2
- package/src/commands/approval-request.js +64 -0
- package/src/commands/approval-resolve.js +10 -0
- package/src/commands/baseline.js +3 -3
- package/src/commands/eval.js +6 -0
- package/src/commands/history.js +18 -0
- package/src/commands/init.js +2 -2
- package/src/commands/inspect.js +49 -0
- package/src/commands/metrics.js +7 -0
- package/src/commands/next.js +8 -2
- package/src/commands/policy-discover.js +2 -2
- package/src/commands/record-diagnosis.js +37 -1
- package/src/commands/record-hypothesis-disposition.js +45 -0
- package/src/commands/record-intervention.js +35 -0
- package/src/commands/reflect.js +38 -0
- package/src/commands/report.js +9 -1
- package/src/commands/run-action.js +18 -0
- package/src/commands/trace.js +34 -0
- package/src/commands/validate-protocol.js +21 -13
- package/src/core/action-authorization.js +106 -0
- package/src/core/action-constants.js +86 -0
- package/src/core/action-execution.js +105 -0
- package/src/core/action-ledger-projection.js +302 -0
- package/src/core/action-model.js +581 -0
- package/src/core/action-readiness.js +141 -0
- package/src/core/action-reconciliation-policy.js +49 -0
- package/src/core/action-reconciliation.js +66 -0
- package/src/core/action-verification.js +111 -0
- package/src/core/actions.js +462 -0
- package/src/core/approvals.js +405 -0
- package/src/core/artifact-registry.js +48 -0
- package/src/core/audit.js +25 -0
- package/src/core/bundles.js +15 -0
- package/src/core/capability-policy.js +226 -0
- package/src/core/cli-command-definitions.js +210 -1
- package/src/core/command-executors.js +171 -15
- package/src/core/command-runtime.js +12 -1
- package/src/core/completion-artifacts.js +37 -12
- package/src/core/completion-recovery-rebind.js +194 -0
- package/src/core/completion.js +70 -0
- package/src/core/continuity-reconciliation.js +24 -5
- package/src/core/diagnostic-model.js +396 -0
- package/src/core/diagnostic-projection.js +51 -0
- package/src/core/diagnostic-record.js +360 -0
- package/src/core/error-codes.js +343 -0
- package/src/core/events.js +41 -1
- package/src/core/execution-prerequisites.js +4 -1
- package/src/core/execution.js +26 -188
- package/src/core/failure-signature.js +70 -0
- package/src/core/failure-surface.js +57 -0
- package/src/core/history.js +110 -0
- package/src/core/hypothesis-projection.js +85 -0
- package/src/core/information-gain-projection.js +283 -0
- package/src/core/information-gain.js +138 -0
- package/src/core/inspect.js +105 -7
- package/src/core/integration-invocation-policy.js +47 -0
- package/src/core/integration-resources.js +51 -0
- package/src/core/next-action-model.js +35 -1
- package/src/core/next-action.js +459 -3
- package/src/core/phase.js +40 -21
- package/src/core/policy-engine.js +113 -6
- package/src/core/preflight-consistency.js +31 -5
- package/src/core/preflight.js +19 -2
- package/src/core/prepared-execution.js +227 -0
- package/src/core/progress.js +41 -4
- package/src/core/protocol-info.js +48 -0
- package/src/core/protocol.js +14 -0
- package/src/core/receipt.js +1 -0
- package/src/core/reconcile-closure.js +15 -12
- package/src/core/reflection.js +305 -0
- package/src/core/resumability.js +57 -3
- package/src/core/schema-validation.js +8 -0
- package/src/core/strategy-analysis.js +97 -0
- package/src/core/task-paths.js +28 -0
- package/src/core/task-snapshot.js +53 -0
- package/src/core/templates.js +8 -0
- package/src/core/trace.js +548 -0
- package/src/core/trajectory-evaluation.js +71 -0
- package/src/core/trajectory-metrics.js +80 -0
- package/src/core/transaction.js +8 -0
- package/src/core/work-state.js +10 -5
- package/docs/assets/forgeloop-flow.svg +0 -1
- package/docs/forgeloop-flow.mmd +0 -51
package/src/core/next-action.js
CHANGED
|
@@ -16,9 +16,11 @@ import { validateEventLedger, validateCompletionRecoveryAuthorization } from "./
|
|
|
16
16
|
import {
|
|
17
17
|
NEXT_ACTIONS,
|
|
18
18
|
commandFor,
|
|
19
|
+
directCommandSpec,
|
|
19
20
|
decision,
|
|
20
21
|
recordCheckCommandSpec,
|
|
21
22
|
recordDiagnosisCommandSpec,
|
|
23
|
+
recordInterventionCommandSpec,
|
|
22
24
|
recordTerminalResultCommandSpec,
|
|
23
25
|
recoveryGuidanceForClassification,
|
|
24
26
|
result,
|
|
@@ -34,16 +36,196 @@ import {
|
|
|
34
36
|
} from "./next-action-artifacts.js";
|
|
35
37
|
import { PHASES_REQUIRING_EXECUTION_CHRONOLOGY } from "./next-action-phases.js";
|
|
36
38
|
import { evaluateContinuityNextAction } from "./next-action-continuity.js";
|
|
37
|
-
import {
|
|
39
|
+
import { resolveCurrentCycleDiagnostic } from "./diagnostic-projection.js";
|
|
40
|
+
import { buildTaskReflection } from "./reflection.js";
|
|
38
41
|
import { evaluateProgress, PROGRESS_STATUS } from "./progress.js";
|
|
39
42
|
import { criterionForDecision } from "./settlement-model.js";
|
|
40
43
|
import { readEvents } from "./events.js";
|
|
41
44
|
import { inspectTaskConflictState } from "./task-conflict-inspection.js";
|
|
45
|
+
import { listActions } from "./actions.js";
|
|
46
|
+
import { listApprovals, validateApprovalForAction } from "./approvals.js";
|
|
47
|
+
import { evaluateActionCapability } from "./capability-policy.js";
|
|
48
|
+
import { loadPolicyIdentity } from "./policy-engine.js";
|
|
42
49
|
|
|
43
50
|
export { NEXT_ACTIONS } from "./next-action-model.js";
|
|
44
51
|
|
|
52
|
+
function capabilityDecisionMetadata(action, capability, approvalId = null) {
|
|
53
|
+
return {
|
|
54
|
+
capability: action.capability,
|
|
55
|
+
decision: capability.decision,
|
|
56
|
+
reasonCode: capability.reasonCode ?? null,
|
|
57
|
+
policyFingerprint: capability.policyFingerprint ?? null,
|
|
58
|
+
...(capability.authority?.authorityRef ? { authorityRef: capability.authority.authorityRef } : {}),
|
|
59
|
+
...(capability.approval?.approvalId
|
|
60
|
+
? { approvalId: capability.approval.approvalId }
|
|
61
|
+
: approvalId ? { approvalId } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function authorizeActionGuidance({ context, state, action, capability, eventsRel }) {
|
|
66
|
+
const approvalId = capability.approval?.approvalId ?? null;
|
|
67
|
+
const approvalSuffix = approvalId ? ` --approval ${approvalId}` : "";
|
|
68
|
+
return result({
|
|
69
|
+
...context,
|
|
70
|
+
nextAction: NEXT_ACTIONS.AUTHORIZE_ACTION,
|
|
71
|
+
commands: [`forgeloop action-authorize --task ${state.taskId} --action ${action.actionId}${approvalSuffix}`],
|
|
72
|
+
reasons: [artifactError(
|
|
73
|
+
"E_ACTION_AUTHORIZATION_REQUIRED",
|
|
74
|
+
`Required action ${action.actionId} is PROPOSED and is authorizable under the canonical capability policy.`,
|
|
75
|
+
)],
|
|
76
|
+
capabilityDecision: capabilityDecisionMetadata(action, capability),
|
|
77
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function actionApprovalGuidance({ context, state, action, capability, eventsRel }) {
|
|
82
|
+
return result({
|
|
83
|
+
...context,
|
|
84
|
+
nextAction: NEXT_ACTIONS.REQUEST_ACTION_APPROVAL,
|
|
85
|
+
commands: [
|
|
86
|
+
`forgeloop approval-request --task ${state.taskId} --action ${action.actionId} --approval <approval-id> --reason <reason>`,
|
|
87
|
+
],
|
|
88
|
+
commandSpecs: [{
|
|
89
|
+
...directCommandSpec("approval-request", state.taskId, [
|
|
90
|
+
{ name: "approvalId", option: "--approval=<approval-id>" },
|
|
91
|
+
{ name: "actionId", option: "--action=<action-id>" },
|
|
92
|
+
{ name: "reason", option: "--reason=<text>" },
|
|
93
|
+
]),
|
|
94
|
+
}],
|
|
95
|
+
reasons: [artifactError(
|
|
96
|
+
"E_ACTION_APPROVAL_REQUIRED",
|
|
97
|
+
`Capability policy requires a current approval before required action ${action.actionId} can be authorized.`,
|
|
98
|
+
)],
|
|
99
|
+
approvalRequired: {
|
|
100
|
+
actionId: action.actionId,
|
|
101
|
+
capability: action.capability,
|
|
102
|
+
reason: "Capability policy requires approval before authorization.",
|
|
103
|
+
},
|
|
104
|
+
capabilityDecision: capabilityDecisionMetadata(action, capability),
|
|
105
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function actionAuthorityGuidance({ context, state, action, capability, eventsRel }) {
|
|
110
|
+
return result({
|
|
111
|
+
...context,
|
|
112
|
+
nextAction: NEXT_ACTIONS.AUTHORIZE_ACTION,
|
|
113
|
+
commands: [],
|
|
114
|
+
reasons: [artifactError(
|
|
115
|
+
capability.reasonCode ?? "E_ACTION_AUTHORITY_REQUIRED",
|
|
116
|
+
`Required action ${action.actionId} needs trusted host authority before it can be authorized.`,
|
|
117
|
+
)],
|
|
118
|
+
authorityRequired: {
|
|
119
|
+
kind: "HOST_ATTESTED",
|
|
120
|
+
actionId: action.actionId,
|
|
121
|
+
capability: action.capability,
|
|
122
|
+
reason: capability.allowed
|
|
123
|
+
? "Authorization must be performed by a trusted host boundary that preserves authority context."
|
|
124
|
+
: "Capability policy requires trusted host authority; no standalone CLI command can create it.",
|
|
125
|
+
},
|
|
126
|
+
hostActionRequired: {
|
|
127
|
+
action: "action-authorize",
|
|
128
|
+
actionId: action.actionId,
|
|
129
|
+
executionBoundary: "HOST",
|
|
130
|
+
requiresAuthorityContext: true,
|
|
131
|
+
},
|
|
132
|
+
capabilityDecision: capabilityDecisionMetadata(action, capability),
|
|
133
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function actionApprovalResolutionGuidance({ context, state, action, approval, eventsRel }) {
|
|
138
|
+
return result({
|
|
139
|
+
...context,
|
|
140
|
+
nextAction: NEXT_ACTIONS.RESOLVE_ACTION_APPROVAL,
|
|
141
|
+
commands: [],
|
|
142
|
+
reasons: [artifactError(
|
|
143
|
+
"E_ACTION_APPROVAL_REQUIRED",
|
|
144
|
+
`Required action ${action.actionId} is waiting for approval.`,
|
|
145
|
+
)],
|
|
146
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "approvals"), eventsRel],
|
|
147
|
+
authorityRequired: {
|
|
148
|
+
kind: "HOST_ATTESTED",
|
|
149
|
+
approvalId: approval.approvalId,
|
|
150
|
+
actionId: action.actionId,
|
|
151
|
+
reason: "This approval must be resolved by a trusted host/operator boundary.",
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function actionDeniedGuidance({ context, state, action, capability, eventsRel }) {
|
|
157
|
+
return result({
|
|
158
|
+
...context,
|
|
159
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
160
|
+
commands: [],
|
|
161
|
+
reasons: [artifactError(
|
|
162
|
+
capability.reasonCode ?? "E_ACTION_CAPABILITY_DENIED",
|
|
163
|
+
`Capability policy does not authorize required action ${action.actionId}: ${capability.decision}.`,
|
|
164
|
+
)],
|
|
165
|
+
capabilityDecision: capabilityDecisionMetadata(action, capability),
|
|
166
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function actionPolicyEpochGuidance({ context, state, action, policyIdentity, eventsRel }) {
|
|
171
|
+
const code = policyIdentity.code ?? "E_ACTION_POLICY_LOCK_REQUIRED";
|
|
172
|
+
const recoveryAction = policyRecoveryAction([{ code }]);
|
|
173
|
+
return result({
|
|
174
|
+
...context,
|
|
175
|
+
nextAction: recoveryAction ?? NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
176
|
+
commands: [],
|
|
177
|
+
reasons: [artifactError(
|
|
178
|
+
code,
|
|
179
|
+
policyIdentity.message
|
|
180
|
+
?? `Required action ${action.actionId} cannot be evaluated because the capability policy is not bound to the active task policy epoch.`,
|
|
181
|
+
[taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
182
|
+
)],
|
|
183
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function evaluateProposedActionCapability({ target, packageRoot, action, approvals, authorityContext }) {
|
|
188
|
+
const current = await evaluateActionCapability({ target, packageRoot, action, authorityContext });
|
|
189
|
+
if (current.decision !== "REQUIRE_APPROVAL" || !approvals) return current;
|
|
190
|
+
const approved = approvals.filter((approval) => approval.actionId === action.actionId && approval.status === "APPROVED");
|
|
191
|
+
for (const approval of approved) {
|
|
192
|
+
const evaluated = await evaluateActionCapability({
|
|
193
|
+
target,
|
|
194
|
+
packageRoot,
|
|
195
|
+
action,
|
|
196
|
+
authorityContext,
|
|
197
|
+
approval: { approvalId: approval.approvalId },
|
|
198
|
+
});
|
|
199
|
+
if (evaluated.allowed) return evaluated;
|
|
200
|
+
}
|
|
201
|
+
return current;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function findApplicablePendingApproval({ target, packageRoot, taskId, action, approvals }) {
|
|
205
|
+
for (const approval of approvals) {
|
|
206
|
+
if (approval.status !== "PENDING" || approval.actionId !== action.actionId) continue;
|
|
207
|
+
try {
|
|
208
|
+
return await validateApprovalForAction(target, {
|
|
209
|
+
packageRoot,
|
|
210
|
+
taskId,
|
|
211
|
+
action,
|
|
212
|
+
approvalId: approval.approvalId,
|
|
213
|
+
requireApproved: false,
|
|
214
|
+
});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (["E_APPROVAL_STALE", "E_APPROVAL_INVALID"].includes(error.code)) continue;
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
45
223
|
export function policyRecoveryAction(errors = []) {
|
|
46
|
-
if (errors.some((e) =>
|
|
224
|
+
if (errors.some((e) => [
|
|
225
|
+
"E_POLICY_WEAKENING",
|
|
226
|
+
"E_POLICY_LOCK_MISMATCH",
|
|
227
|
+
"E_ACTION_POLICY_DRIFT",
|
|
228
|
+
].includes(e.code))) {
|
|
47
229
|
return NEXT_ACTIONS.RESTORE_POLICY;
|
|
48
230
|
}
|
|
49
231
|
if (errors.some((e) => e.code === "E_CHECK_MUTATION_EXECUTION_ERROR" || e.code === "E_CHECK_MUTATION_NOT_DETECTED")) {
|
|
@@ -176,6 +358,240 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
176
358
|
});
|
|
177
359
|
}
|
|
178
360
|
}
|
|
361
|
+
let actionsForApproval;
|
|
362
|
+
try {
|
|
363
|
+
actionsForApproval = await listActions(target, { packageRoot, taskId: state.taskId });
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (error.code === "E_ACTION_INVALID") {
|
|
366
|
+
return result({
|
|
367
|
+
...context,
|
|
368
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
369
|
+
commands: [],
|
|
370
|
+
reasons: [artifactError(
|
|
371
|
+
error.code,
|
|
372
|
+
`A durable action artifact is invalid and cannot be offered for authorization: ${error.message}`,
|
|
373
|
+
[taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
374
|
+
)],
|
|
375
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
const ambiguousAction = actionsForApproval.find((action) => action.state === "COMMIT_UNKNOWN");
|
|
381
|
+
if (ambiguousAction) {
|
|
382
|
+
return result({ ...context, nextAction: NEXT_ACTIONS.RECONCILE_ACTION,
|
|
383
|
+
commands: [`forgeloop action-reconcile --task ${state.taskId} --action ${ambiguousAction.actionId} --outcome UNKNOWN`],
|
|
384
|
+
reasons: [artifactError("E_ACTION_RECONCILIATION_REQUIRED",
|
|
385
|
+
`Action ${ambiguousAction.actionId} has an unknown external commit outcome; retry is forbidden until reconciliation.`)],
|
|
386
|
+
reconciliationAuthorityRequired: {
|
|
387
|
+
outcomesRequiringTrust: ["COMMITTED", "NOT_COMMITTED"],
|
|
388
|
+
reason: "Recording an UNKNOWN observation is safe from any surface; settling COMMITTED or NOT_COMMITTED requires a trusted host boundary and evidence.",
|
|
389
|
+
},
|
|
390
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel] });
|
|
391
|
+
}
|
|
392
|
+
// A PROPOSED required action ready for authorization should use the
|
|
393
|
+
// canonical authorization surface; host/approval blockers remain structured.
|
|
394
|
+
const authorizableAction = actionsForApproval.find((action) => action.requiredForCompletion
|
|
395
|
+
&& action.state === "PROPOSED");
|
|
396
|
+
if (authorizableAction) {
|
|
397
|
+
let policyIdentity;
|
|
398
|
+
try {
|
|
399
|
+
policyIdentity = await loadPolicyIdentity(target, packageRoot, state.taskId);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
return actionPolicyEpochGuidance({
|
|
402
|
+
context,
|
|
403
|
+
state,
|
|
404
|
+
action: authorizableAction,
|
|
405
|
+
policyIdentity: {
|
|
406
|
+
code: error.code ?? "E_POLICY_INVALID",
|
|
407
|
+
message: `Capability policy epoch validation failed for required action ${authorizableAction.actionId}: ${error.message}`,
|
|
408
|
+
},
|
|
409
|
+
eventsRel,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (policyIdentity.status !== "VALID") {
|
|
413
|
+
return actionPolicyEpochGuidance({
|
|
414
|
+
context,
|
|
415
|
+
state,
|
|
416
|
+
action: authorizableAction,
|
|
417
|
+
policyIdentity,
|
|
418
|
+
eventsRel,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let capability;
|
|
423
|
+
try {
|
|
424
|
+
capability = await evaluateProposedActionCapability({
|
|
425
|
+
target,
|
|
426
|
+
packageRoot,
|
|
427
|
+
action: authorizableAction,
|
|
428
|
+
authorityContext,
|
|
429
|
+
});
|
|
430
|
+
} catch (error) {
|
|
431
|
+
return result({
|
|
432
|
+
...context,
|
|
433
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
434
|
+
commands: [],
|
|
435
|
+
reasons: [artifactError(
|
|
436
|
+
error.code ?? "E_ACTION_CAPABILITY_INVALID",
|
|
437
|
+
`Capability policy evaluation failed for required action ${authorizableAction.actionId}: ${error.message}`,
|
|
438
|
+
[taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
439
|
+
)],
|
|
440
|
+
capabilityDecision: {
|
|
441
|
+
capability: authorizableAction.capability,
|
|
442
|
+
decision: "DENY",
|
|
443
|
+
reasonCode: error.code ?? "E_ACTION_CAPABILITY_INVALID",
|
|
444
|
+
policyFingerprint: null,
|
|
445
|
+
},
|
|
446
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (capability.decision === "REQUIRE_AUTHORITY") {
|
|
450
|
+
return actionAuthorityGuidance({
|
|
451
|
+
context,
|
|
452
|
+
state,
|
|
453
|
+
action: authorizableAction,
|
|
454
|
+
capability,
|
|
455
|
+
eventsRel,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (capability.allowed) {
|
|
459
|
+
return authorizeActionGuidance({
|
|
460
|
+
context,
|
|
461
|
+
state,
|
|
462
|
+
action: authorizableAction,
|
|
463
|
+
capability,
|
|
464
|
+
eventsRel,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
if (capability.decision === "REQUIRE_APPROVAL") {
|
|
468
|
+
let approvals;
|
|
469
|
+
try {
|
|
470
|
+
approvals = await listApprovals(target, { packageRoot, taskId: state.taskId });
|
|
471
|
+
} catch (error) {
|
|
472
|
+
return result({
|
|
473
|
+
...context,
|
|
474
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
475
|
+
commands: [],
|
|
476
|
+
reasons: [artifactError(
|
|
477
|
+
error.code ?? "E_APPROVAL_INVALID",
|
|
478
|
+
`Approval artifact inspection failed for required action ${authorizableAction.actionId}: ${error.message}`,
|
|
479
|
+
[taskArtifactPath(state.taskId, "approvals"), eventsRel],
|
|
480
|
+
)],
|
|
481
|
+
capabilityDecision: capabilityDecisionMetadata(authorizableAction, capability),
|
|
482
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "approvals"), eventsRel],
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
capability = await evaluateProposedActionCapability({
|
|
487
|
+
target,
|
|
488
|
+
packageRoot,
|
|
489
|
+
action: authorizableAction,
|
|
490
|
+
approvals,
|
|
491
|
+
authorityContext,
|
|
492
|
+
});
|
|
493
|
+
} catch (error) {
|
|
494
|
+
return result({
|
|
495
|
+
...context,
|
|
496
|
+
nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
|
|
497
|
+
commands: [],
|
|
498
|
+
reasons: [artifactError(
|
|
499
|
+
error.code ?? "E_ACTION_CAPABILITY_INVALID",
|
|
500
|
+
`Capability policy evaluation failed for required action ${authorizableAction.actionId}: ${error.message}`,
|
|
501
|
+
[taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
502
|
+
)],
|
|
503
|
+
capabilityDecision: {
|
|
504
|
+
capability: authorizableAction.capability,
|
|
505
|
+
decision: "DENY",
|
|
506
|
+
reasonCode: error.code ?? "E_ACTION_CAPABILITY_INVALID",
|
|
507
|
+
policyFingerprint: null,
|
|
508
|
+
},
|
|
509
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel],
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
if (capability.allowed) {
|
|
513
|
+
return authorizeActionGuidance({
|
|
514
|
+
context,
|
|
515
|
+
state,
|
|
516
|
+
action: authorizableAction,
|
|
517
|
+
capability,
|
|
518
|
+
eventsRel,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
if (capability.decision !== "REQUIRE_APPROVAL") {
|
|
522
|
+
if (capability.decision === "REQUIRE_AUTHORITY") {
|
|
523
|
+
return actionAuthorityGuidance({
|
|
524
|
+
context,
|
|
525
|
+
state,
|
|
526
|
+
action: authorizableAction,
|
|
527
|
+
capability,
|
|
528
|
+
eventsRel,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
if (capability.allowed) {
|
|
532
|
+
return authorizeActionGuidance({
|
|
533
|
+
context,
|
|
534
|
+
state,
|
|
535
|
+
action: authorizableAction,
|
|
536
|
+
capability,
|
|
537
|
+
eventsRel,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
return actionDeniedGuidance({
|
|
541
|
+
context,
|
|
542
|
+
state,
|
|
543
|
+
action: authorizableAction,
|
|
544
|
+
capability,
|
|
545
|
+
eventsRel,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
const pendingApproval = await findApplicablePendingApproval({
|
|
549
|
+
target,
|
|
550
|
+
packageRoot,
|
|
551
|
+
taskId: state.taskId,
|
|
552
|
+
action: authorizableAction,
|
|
553
|
+
approvals,
|
|
554
|
+
});
|
|
555
|
+
if (pendingApproval) {
|
|
556
|
+
return actionApprovalResolutionGuidance({
|
|
557
|
+
context,
|
|
558
|
+
state,
|
|
559
|
+
action: authorizableAction,
|
|
560
|
+
approval: pendingApproval,
|
|
561
|
+
eventsRel,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return actionApprovalGuidance({
|
|
565
|
+
context,
|
|
566
|
+
state,
|
|
567
|
+
action: authorizableAction,
|
|
568
|
+
capability,
|
|
569
|
+
eventsRel,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return actionDeniedGuidance({
|
|
573
|
+
context,
|
|
574
|
+
state,
|
|
575
|
+
action: authorizableAction,
|
|
576
|
+
capability,
|
|
577
|
+
eventsRel,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
// Committed-but-unverified required action needs canonical postcondition
|
|
581
|
+
// evidence before it can satisfy completion.
|
|
582
|
+
const committedActions = actionsForApproval;
|
|
583
|
+
const unverifiedRequired = committedActions.find((action) => action.requiredForCompletion
|
|
584
|
+
&& action.state === "COMMITTED");
|
|
585
|
+
if (unverifiedRequired) {
|
|
586
|
+
return result({ ...context, nextAction: NEXT_ACTIONS.VERIFY_EXTERNAL_ACTION,
|
|
587
|
+
commands: [
|
|
588
|
+
`forgeloop run-check --task ${state.taskId} --id check-postcondition --requirement <requirement> -- <independent verification argv>`,
|
|
589
|
+
`forgeloop action-verify --task ${state.taskId} --action ${unverifiedRequired.actionId} --evidence <execution-ref>`,
|
|
590
|
+
],
|
|
591
|
+
reasons: [artifactError("E_ACTION_VERIFICATION_REQUIRED",
|
|
592
|
+
`Committed action ${unverifiedRequired.actionId} requires independent canonical postcondition evidence; exit code 0 alone is not verification.`)],
|
|
593
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "actions"), eventsRel] });
|
|
594
|
+
}
|
|
179
595
|
if (state.phase === "RECEIVED") {
|
|
180
596
|
return decision(
|
|
181
597
|
context,
|
|
@@ -529,7 +945,7 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
529
945
|
if (state.phase === "DIAGNOSING") {
|
|
530
946
|
const ledger = await validateEventLedger(target, packageRoot, { taskId: normalized.taskId ?? null });
|
|
531
947
|
const cycle = state.verificationCycle ?? 1;
|
|
532
|
-
const diagEvent =
|
|
948
|
+
const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
|
|
533
949
|
if (!diagEvent) {
|
|
534
950
|
return result({
|
|
535
951
|
...context,
|
|
@@ -547,6 +963,13 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
547
963
|
}
|
|
548
964
|
const progress = evaluateProgress({ state, events: ledger.events });
|
|
549
965
|
if (progress.status === PROGRESS_STATUS.STALLED) {
|
|
966
|
+
let oscillating = false;
|
|
967
|
+
try {
|
|
968
|
+
const reflection = await buildTaskReflection({ target, packageRoot, taskId: normalized.taskId ?? null });
|
|
969
|
+
oscillating = reflection.oscillation.detected;
|
|
970
|
+
} catch {
|
|
971
|
+
// Reflection is advisory guidance; stall handling must not depend on it.
|
|
972
|
+
}
|
|
550
973
|
return result({
|
|
551
974
|
...context,
|
|
552
975
|
nextAction: NEXT_ACTIONS.CHANGE_STRATEGY,
|
|
@@ -558,6 +981,11 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
558
981
|
),
|
|
559
982
|
],
|
|
560
983
|
progress,
|
|
984
|
+
diagnosticGuidance: {
|
|
985
|
+
action: oscillating ? NEXT_ACTIONS.INTRODUCE_NEW_OBSERVATION : NEXT_ACTIONS.REQUIRE_NEW_DIAGNOSTIC_INFORMATION,
|
|
986
|
+
signals: oscillating ? ["OSCILLATING_STRATEGY"] : ["NO_INFORMATION_GAIN", ...progress.signals.map((signal) => signal.code)],
|
|
987
|
+
errorCodes: oscillating ? ["E_STRATEGY_OSCILLATION", "E_PROGRESS_STALLED"] : ["E_PROGRESS_STALLED"],
|
|
988
|
+
},
|
|
561
989
|
requiredArtifacts,
|
|
562
990
|
});
|
|
563
991
|
}
|
|
@@ -573,6 +1001,34 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
573
1001
|
});
|
|
574
1002
|
}
|
|
575
1003
|
if (state.phase === "CORRECTING") {
|
|
1004
|
+
const ledgerForCorrection = await validateEventLedger(target, packageRoot, { taskId: normalized.taskId ?? null });
|
|
1005
|
+
const correctionCycle = state.verificationCycle ?? 1;
|
|
1006
|
+
const hasStructuredCase = ledgerForCorrection.events.some(
|
|
1007
|
+
(event) => event.event === "DIAGNOSTIC_CASE_RECORDED"
|
|
1008
|
+
&& event.taskId === state.taskId
|
|
1009
|
+
&& event.details?.verificationCycle === correctionCycle,
|
|
1010
|
+
);
|
|
1011
|
+
const hasIntervention = ledgerForCorrection.events.some(
|
|
1012
|
+
(event) => event.event === "INTERVENTION_RECORDED"
|
|
1013
|
+
&& event.taskId === state.taskId
|
|
1014
|
+
&& event.details?.verificationCycle === correctionCycle,
|
|
1015
|
+
);
|
|
1016
|
+
if (hasStructuredCase && !hasIntervention) {
|
|
1017
|
+
return result({
|
|
1018
|
+
...context,
|
|
1019
|
+
nextAction: NEXT_ACTIONS.ENTER_VERIFYING,
|
|
1020
|
+
reasons: [
|
|
1021
|
+
artifactError("PHASE_CORRECTING", "A structured diagnostic case is awaiting a recorded intervention"),
|
|
1022
|
+
],
|
|
1023
|
+
diagnosticGuidance: {
|
|
1024
|
+
action: NEXT_ACTIONS.RECORD_INTERVENTION,
|
|
1025
|
+
signals: ["INTERVENTION_NOT_BOUND_TO_HYPOTHESIS"],
|
|
1026
|
+
errorCodes: [],
|
|
1027
|
+
commandSpecs: [recordInterventionCommandSpec(normalized.taskId)],
|
|
1028
|
+
},
|
|
1029
|
+
requiredArtifacts,
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
576
1032
|
return decision(context, NEXT_ACTIONS.ENTER_VERIFYING, artifactError("PHASE_CORRECTING", "Correction is ready for verification"));
|
|
577
1033
|
}
|
|
578
1034
|
if (state.phase === "REVIEWING") {
|
package/src/core/phase.js
CHANGED
|
@@ -3,13 +3,13 @@ import { readContract } from "./contract.js";
|
|
|
3
3
|
import {
|
|
4
4
|
appendProtocolEvent,
|
|
5
5
|
LIFECYCLE_MILESTONES,
|
|
6
|
-
validateCompletionRecoveryAuthorization,
|
|
7
6
|
validateEventLedger,
|
|
8
7
|
validateStateLedgerCoherence,
|
|
9
8
|
} from "./events.js";
|
|
10
9
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
11
10
|
import { assertWorkPhase, isValidTransition } from "./protocol.js";
|
|
12
11
|
import { readWorkState, mutateWorkState } from "./work-state.js";
|
|
12
|
+
import { authorizeCompletionRecoveryOrRebind } from "./completion-recovery-rebind.js";
|
|
13
13
|
import { evaluateCompletion } from "./completion.js";
|
|
14
14
|
import { evaluatePreflight } from "./preflight.js";
|
|
15
15
|
import { requiredEvidenceForTarget } from "./completion-artifacts.js";
|
|
@@ -21,7 +21,11 @@ import { discoverTasks } from "./task-discovery.js";
|
|
|
21
21
|
import { assertNoScopeConflicts, assertScopeClean } from "./task-scope.js";
|
|
22
22
|
import { readTaskDescriptor } from "./task-descriptor.js";
|
|
23
23
|
import { E_TASK_SCOPE_REQUIRED } from "./error-codes.js";
|
|
24
|
-
import {
|
|
24
|
+
import { resolveCurrentCycleDiagnostic } from "./diagnostic-projection.js";
|
|
25
|
+
import {
|
|
26
|
+
buildInformationGainProjection,
|
|
27
|
+
evaluateStructuredDiagnosticStall,
|
|
28
|
+
} from "./information-gain-projection.js";
|
|
25
29
|
|
|
26
30
|
function phaseError(code, message, artifacts = []) {
|
|
27
31
|
const error = new Error(message);
|
|
@@ -164,10 +168,9 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
164
168
|
} = normalizedOptions;
|
|
165
169
|
assertWorkPhase(toPhase);
|
|
166
170
|
|
|
167
|
-
|
|
171
|
+
let state = await readWorkState(target, { packageRoot, taskId, statePath });
|
|
168
172
|
const stateRel = statePath ?? (taskId ? taskArtifactPath(taskId, "state") : ARTIFACT_PATHS.state);
|
|
169
173
|
if (!state) throw phaseError("E_PHASE_PREREQUISITE_MISSING", "Cannot advance without work state", [stateRel]);
|
|
170
|
-
|
|
171
174
|
const eventsRel = eventsPath ?? (taskId ? taskArtifactPath(taskId, "events") : ARTIFACT_PATHS.events);
|
|
172
175
|
const receiptRel = receiptPath ?? (taskId ? taskArtifactPath(taskId, "receipt") : ARTIFACT_PATHS.receipt);
|
|
173
176
|
|
|
@@ -223,30 +226,33 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
223
226
|
const eventType = PHASE_EVENTS[toPhase];
|
|
224
227
|
const reenteringVerification = toPhase === "VERIFYING" && ["CORRECTING", "REVIEWING"].includes(state.phase);
|
|
225
228
|
if (toPhase === "VERIFYING" && state.phase === "REVIEWING") {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
229
|
+
const recovery = await authorizeCompletionRecoveryOrRebind({
|
|
230
|
+
target,
|
|
231
|
+
packageRoot,
|
|
232
|
+
taskId,
|
|
233
|
+
statePath,
|
|
234
|
+
contractPath,
|
|
235
|
+
routePath,
|
|
236
|
+
receiptPath,
|
|
237
|
+
eventsPath,
|
|
238
|
+
authorityContext,
|
|
239
|
+
runtimeContext,
|
|
237
240
|
});
|
|
238
|
-
if (!recoveryAuth.authorized) {
|
|
239
|
-
const firstError = recoveryAuth.errors[0] ?? {};
|
|
241
|
+
if (!recovery.recoveryAuth.authorized) {
|
|
242
|
+
const firstError = recovery.recoveryAuth.errors[0] ?? {};
|
|
240
243
|
throw phaseError(
|
|
241
244
|
firstError.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
|
|
242
245
|
firstError.message ?? "REVIEWING -> VERIFYING requires authorized completion recovery",
|
|
243
246
|
[stateRel, eventsRel],
|
|
244
247
|
);
|
|
245
248
|
}
|
|
249
|
+
if (recovery.rebound) {
|
|
250
|
+
state = recovery.state;
|
|
251
|
+
}
|
|
246
252
|
}
|
|
247
253
|
if (toPhase === "CORRECTING" && state.phase === "DIAGNOSING") {
|
|
248
254
|
const cycle = state.verificationCycle ?? 1;
|
|
249
|
-
const diagEvent =
|
|
255
|
+
const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
|
|
250
256
|
if (!diagEvent) {
|
|
251
257
|
throw phaseError(
|
|
252
258
|
"E_DIAGNOSIS_REQUIRED",
|
|
@@ -254,8 +260,21 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
254
260
|
[eventsRel],
|
|
255
261
|
);
|
|
256
262
|
}
|
|
257
|
-
|
|
258
|
-
if (
|
|
263
|
+
let stalledNoGain = false;
|
|
264
|
+
if (diagEvent.sourceModel === "STRUCTURED_DIAGNOSTIC_CASE_V1") {
|
|
265
|
+
// One canonical structured-stall truth, shared with progress/reflect.
|
|
266
|
+
const stall = evaluateStructuredDiagnosticStall(
|
|
267
|
+
buildInformationGainProjection(ledger.events, state.taskId),
|
|
268
|
+
{ verificationCycle: cycle },
|
|
269
|
+
);
|
|
270
|
+
stalledNoGain = stall.stalled;
|
|
271
|
+
}
|
|
272
|
+
const gainClassification = diagEvent.sourceModel === "STRUCTURED_DIAGNOSTIC_CASE_V1"
|
|
273
|
+
? null
|
|
274
|
+
: diagEvent.details?.informationGain;
|
|
275
|
+
if (!diagEvent.details
|
|
276
|
+
|| stalledNoGain
|
|
277
|
+
|| (diagEvent.sourceModel !== "STRUCTURED_DIAGNOSTIC_CASE_V1" && gainClassification === "NONE")) {
|
|
259
278
|
throw phaseError(
|
|
260
279
|
"E_DIAGNOSIS_NO_NEW_INFORMATION",
|
|
261
280
|
"The proposed retry repeats the previous hypothesis with the same evidence without new information",
|
|
@@ -265,7 +284,7 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
265
284
|
}
|
|
266
285
|
if (toPhase === "VERIFYING" && state.phase === "CORRECTING") {
|
|
267
286
|
const cycle = state.verificationCycle ?? 1;
|
|
268
|
-
const diagEvent =
|
|
287
|
+
const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
|
|
269
288
|
if (!diagEvent) {
|
|
270
289
|
throw phaseError(
|
|
271
290
|
"E_DIAGNOSIS_REQUIRED",
|