@cassiomc1/forgeloop 1.3.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/.github/copilot-instructions.md +1 -0
- package/AGENTS.md +1 -0
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +20 -8
- package/EXECUTION_STATE.md +60 -0
- package/LOOP_ENGINEERING.md +135 -5
- package/LOOP_SYSTEM_DESIGN.md +54 -1
- package/PROTOCOL_INTEGRATION.md +87 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +69 -9
- package/TERMINOLOGY.md +15 -0
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/THREAT_MODEL.md +59 -1
- package/docs/ARTIFACT_REFERENCE.md +183 -0
- package/docs/CLI_REFERENCE.md +391 -6
- package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
- package/docs/DIAGNOSTIC_MODEL.md +181 -0
- package/docs/DOCUMENTATION_GUIDE.md +36 -13
- package/docs/EXECUTION_TRACE.md +76 -0
- package/docs/GETTING_STARTED.md +1 -0
- package/docs/MCP.md +159 -0
- package/docs/RECIPES.md +149 -0
- package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
- package/docs/TROUBLESHOOTING.md +217 -3
- package/docs/UNIVERSAL_INTEGRATION.md +48 -0
- 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 +21 -8
- 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/task-recovery.schema.json +61 -0
- package/schemas/trajectory-evaluation.schema.json +64 -0
- package/schemas/trajectory-scenario.schema.json +42 -0
- package/src/cli.js +267 -347
- 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/audit.js +5 -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 +55 -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/progress.js +6 -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/status.js +17 -0
- package/src/commands/task-create.js +39 -1
- package/src/commands/task-list.js +14 -1
- package/src/commands/task-lock-status.js +2 -2
- package/src/commands/task-recover.js +202 -0
- package/src/commands/task-repair-legacy-recovery.js +417 -0
- package/src/commands/task-resume.js +172 -0
- package/src/commands/task-scope.js +23 -4
- package/src/commands/task-show.js +18 -4
- package/src/commands/trace.js +34 -0
- package/src/commands/validate-protocol.js +40 -15
- 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 +60 -0
- package/src/core/audit.js +45 -4
- package/src/core/bundles.js +30 -0
- package/src/core/capability-policy.js +226 -0
- package/src/core/cli-command-definitions.js +260 -5
- package/src/core/command-executors.js +543 -0
- package/src/core/command-input.js +107 -0
- package/src/core/command-runtime.js +117 -0
- package/src/core/completion-artifacts.js +39 -15
- package/src/core/completion-ownership.js +88 -0
- 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 +461 -1
- package/src/core/events.js +171 -2
- 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/filesystem.js +55 -6
- 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 +132 -7
- package/src/core/integration-invocation-policy.js +217 -0
- package/src/core/integration-limits.js +20 -0
- package/src/core/integration-resources.js +178 -0
- package/src/core/next-action-model.js +94 -0
- package/src/core/next-action.js +490 -3
- package/src/core/phase.js +42 -22
- 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/project-root.js +21 -0
- package/src/core/protocol-info.js +61 -0
- package/src/core/protocol.js +14 -0
- package/src/core/receipt.js +1 -0
- package/src/core/reconcile-closure.js +35 -10
- package/src/core/recovery-history.js +116 -0
- package/src/core/reflection.js +305 -0
- package/src/core/resumability.js +57 -3
- package/src/core/schema-validation.js +9 -0
- package/src/core/strategy-analysis.js +97 -0
- package/src/core/task-claim-state.js +272 -0
- package/src/core/task-command.js +5 -1
- package/src/core/task-conflict-inspection.js +321 -0
- package/src/core/task-context.js +32 -29
- package/src/core/task-discovery.js +14 -1
- package/src/core/task-lock.js +216 -22
- package/src/core/task-paths.js +31 -2
- package/src/core/task-recovery-migration.js +192 -0
- package/src/core/task-recovery.js +205 -0
- package/src/core/task-scope.js +33 -1
- package/src/core/task-snapshot.js +53 -0
- package/src/core/templates.js +9 -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 +36 -2
- package/src/core/work-state.js +10 -5
- package/src/integration.js +47 -0
- package/docs/assets/forgeloop-flow.svg +0 -1
- package/docs/forgeloop-flow.mmd +0 -51
package/src/core/next-action.js
CHANGED
|
@@ -16,10 +16,13 @@ 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,
|
|
25
|
+
recoveryGuidanceForClassification,
|
|
23
26
|
result,
|
|
24
27
|
uniqueSorted,
|
|
25
28
|
} from "./next-action-model.js";
|
|
@@ -33,15 +36,196 @@ import {
|
|
|
33
36
|
} from "./next-action-artifacts.js";
|
|
34
37
|
import { PHASES_REQUIRING_EXECUTION_CHRONOLOGY } from "./next-action-phases.js";
|
|
35
38
|
import { evaluateContinuityNextAction } from "./next-action-continuity.js";
|
|
36
|
-
import {
|
|
39
|
+
import { resolveCurrentCycleDiagnostic } from "./diagnostic-projection.js";
|
|
40
|
+
import { buildTaskReflection } from "./reflection.js";
|
|
37
41
|
import { evaluateProgress, PROGRESS_STATUS } from "./progress.js";
|
|
38
42
|
import { criterionForDecision } from "./settlement-model.js";
|
|
39
43
|
import { readEvents } from "./events.js";
|
|
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";
|
|
40
49
|
|
|
41
50
|
export { NEXT_ACTIONS } from "./next-action-model.js";
|
|
42
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
|
+
|
|
43
223
|
export function policyRecoveryAction(errors = []) {
|
|
44
|
-
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))) {
|
|
45
229
|
return NEXT_ACTIONS.RESTORE_POLICY;
|
|
46
230
|
}
|
|
47
231
|
if (errors.some((e) => e.code === "E_CHECK_MUTATION_EXECUTION_ERROR" || e.code === "E_CHECK_MUTATION_NOT_DETECTED")) {
|
|
@@ -145,6 +329,269 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
145
329
|
|
|
146
330
|
const state = workState.value;
|
|
147
331
|
const context = { taskId: state.taskId, currentPhase: state.phase };
|
|
332
|
+
if (explicitTaskId) {
|
|
333
|
+
let inspection;
|
|
334
|
+
try {
|
|
335
|
+
inspection = await inspectTaskConflictState(target, {
|
|
336
|
+
taskId: explicitTaskId,
|
|
337
|
+
packageRoot,
|
|
338
|
+
});
|
|
339
|
+
} catch (error) {
|
|
340
|
+
inspection = {
|
|
341
|
+
classification: "INCONSISTENT",
|
|
342
|
+
reasonCodes: [error.code ?? "E_TASK_RECOVERY_INCONSISTENT"],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (!["ACTIVE", "COMPLETE"].includes(inspection.classification)) {
|
|
346
|
+
const guidance = recoveryGuidanceForClassification(inspection.classification, explicitTaskId);
|
|
347
|
+
return result({
|
|
348
|
+
...context,
|
|
349
|
+
nextAction: guidance.nextAction,
|
|
350
|
+
commands: guidance.commands,
|
|
351
|
+
commandSpecs: guidance.commandSpecs,
|
|
352
|
+
reasons: inspection.reasonCodes.map((code) => artifactError(
|
|
353
|
+
code,
|
|
354
|
+
`Task conflict state is ${inspection.classification}; follow the structured recovery guidance.`,
|
|
355
|
+
[stateRel, eventsRel, taskArtifactPath(explicitTaskId, "recovery")],
|
|
356
|
+
)),
|
|
357
|
+
requiredArtifacts: [stateRel, eventsRel],
|
|
358
|
+
});
|
|
359
|
+
}
|
|
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
|
+
}
|
|
148
595
|
if (state.phase === "RECEIVED") {
|
|
149
596
|
return decision(
|
|
150
597
|
context,
|
|
@@ -498,7 +945,7 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
498
945
|
if (state.phase === "DIAGNOSING") {
|
|
499
946
|
const ledger = await validateEventLedger(target, packageRoot, { taskId: normalized.taskId ?? null });
|
|
500
947
|
const cycle = state.verificationCycle ?? 1;
|
|
501
|
-
const diagEvent =
|
|
948
|
+
const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
|
|
502
949
|
if (!diagEvent) {
|
|
503
950
|
return result({
|
|
504
951
|
...context,
|
|
@@ -516,6 +963,13 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
516
963
|
}
|
|
517
964
|
const progress = evaluateProgress({ state, events: ledger.events });
|
|
518
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
|
+
}
|
|
519
973
|
return result({
|
|
520
974
|
...context,
|
|
521
975
|
nextAction: NEXT_ACTIONS.CHANGE_STRATEGY,
|
|
@@ -527,6 +981,11 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
527
981
|
),
|
|
528
982
|
],
|
|
529
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
|
+
},
|
|
530
989
|
requiredArtifacts,
|
|
531
990
|
});
|
|
532
991
|
}
|
|
@@ -542,6 +1001,34 @@ async function computeNextAction(targetOrOptions = {}, packageRootOption) {
|
|
|
542
1001
|
});
|
|
543
1002
|
}
|
|
544
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
|
+
}
|
|
545
1032
|
return decision(context, NEXT_ACTIONS.ENTER_VERIFYING, artifactError("PHASE_CORRECTING", "Correction is ready for verification"));
|
|
546
1033
|
}
|
|
547
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
|
|
|
@@ -187,7 +190,8 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
187
190
|
|
|
188
191
|
const nonCompleteTasks = discovered.filter((t) => t.phase !== "COMPLETE");
|
|
189
192
|
if (nonCompleteTasks.length > 1) {
|
|
190
|
-
const
|
|
193
|
+
const currentTask = discovered.find((task) => task.taskId === taskId && task.healthy !== false);
|
|
194
|
+
const claims = currentTask?.writeClaims ?? [];
|
|
191
195
|
if (claims.length === 0) {
|
|
192
196
|
throw phaseError(
|
|
193
197
|
E_TASK_SCOPE_REQUIRED,
|
|
@@ -222,30 +226,33 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
222
226
|
const eventType = PHASE_EVENTS[toPhase];
|
|
223
227
|
const reenteringVerification = toPhase === "VERIFYING" && ["CORRECTING", "REVIEWING"].includes(state.phase);
|
|
224
228
|
if (toPhase === "VERIFYING" && state.phase === "REVIEWING") {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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,
|
|
236
240
|
});
|
|
237
|
-
if (!recoveryAuth.authorized) {
|
|
238
|
-
const firstError = recoveryAuth.errors[0] ?? {};
|
|
241
|
+
if (!recovery.recoveryAuth.authorized) {
|
|
242
|
+
const firstError = recovery.recoveryAuth.errors[0] ?? {};
|
|
239
243
|
throw phaseError(
|
|
240
244
|
firstError.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
|
|
241
245
|
firstError.message ?? "REVIEWING -> VERIFYING requires authorized completion recovery",
|
|
242
246
|
[stateRel, eventsRel],
|
|
243
247
|
);
|
|
244
248
|
}
|
|
249
|
+
if (recovery.rebound) {
|
|
250
|
+
state = recovery.state;
|
|
251
|
+
}
|
|
245
252
|
}
|
|
246
253
|
if (toPhase === "CORRECTING" && state.phase === "DIAGNOSING") {
|
|
247
254
|
const cycle = state.verificationCycle ?? 1;
|
|
248
|
-
const diagEvent =
|
|
255
|
+
const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
|
|
249
256
|
if (!diagEvent) {
|
|
250
257
|
throw phaseError(
|
|
251
258
|
"E_DIAGNOSIS_REQUIRED",
|
|
@@ -253,8 +260,21 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
253
260
|
[eventsRel],
|
|
254
261
|
);
|
|
255
262
|
}
|
|
256
|
-
|
|
257
|
-
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")) {
|
|
258
278
|
throw phaseError(
|
|
259
279
|
"E_DIAGNOSIS_NO_NEW_INFORMATION",
|
|
260
280
|
"The proposed retry repeats the previous hypothesis with the same evidence without new information",
|
|
@@ -264,7 +284,7 @@ export async function advanceWorkState(target, toPhase, options = {}) {
|
|
|
264
284
|
}
|
|
265
285
|
if (toPhase === "VERIFYING" && state.phase === "CORRECTING") {
|
|
266
286
|
const cycle = state.verificationCycle ?? 1;
|
|
267
|
-
const diagEvent =
|
|
287
|
+
const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
|
|
268
288
|
if (!diagEvent) {
|
|
269
289
|
throw phaseError(
|
|
270
290
|
"E_DIAGNOSIS_REQUIRED",
|