@cassiomc1/forgeloop 1.10.0 → 1.10.2

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.
Files changed (67) hide show
  1. package/DOCS_INDEX.md +12 -1
  2. package/LOOP_ENGINEERING.md +4 -4
  3. package/LOOP_SYSTEM_DESIGN.md +23 -0
  4. package/QUALITY_SCORECARD.md +2 -2
  5. package/README.md +22 -8
  6. package/docs/ADVISORY_CONTEXT.md +24 -0
  7. package/docs/AGENT_PROTOCOL_SUMMARY.md +1 -1
  8. package/docs/ARTIFACT_REFERENCE.md +15 -0
  9. package/docs/CODE_ATTESTATION.md +9 -0
  10. package/docs/DOCUMENTATION_GUIDE.md +4 -4
  11. package/docs/EXECUTION_PROFILE_BENCHMARKS.md +10 -0
  12. package/docs/MCP.md +13 -1
  13. package/docs/PACKAGE_CONTENTS.md +88 -0
  14. package/docs/RELEASE_CHECKLIST.md +10 -2
  15. package/docs/REVISION_PROVIDERS.md +9 -0
  16. package/docs/RIPWIRE_ADAPTER.md +189 -0
  17. package/docs/TROUBLESHOOTING.md +12 -0
  18. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +13 -2
  19. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +6 -6
  20. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +10 -1
  21. package/docs/assets/diagrams/forgeloop-engineering-flow.html +22 -11
  22. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  23. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +17 -8
  24. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +14 -3
  25. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +6 -6
  26. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +10 -1
  27. package/docs/diagrams/README.md +26 -0
  28. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +383 -57
  29. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +374 -55
  30. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +328 -47
  31. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  32. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  33. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  34. package/package.json +14 -4
  35. package/scripts/CI_VALIDATORS.md +24 -0
  36. package/scripts/check-critical-coverage.mjs +9 -0
  37. package/src/adapters/ripwire/normalize.js +352 -0
  38. package/src/adapters/ripwire/process.js +248 -0
  39. package/src/adapters/ripwire/provider.js +245 -0
  40. package/src/commands/doctor.js +11 -10
  41. package/src/core/actions.js +2 -2
  42. package/src/core/advisory-context/service.js +36 -14
  43. package/src/core/approvals.js +2 -2
  44. package/src/core/artifacts.js +3 -3
  45. package/src/core/checks.js +0 -33
  46. package/src/core/completion.js +2 -2
  47. package/src/core/events.js +5 -5
  48. package/src/core/execution-profile.js +18 -5
  49. package/src/core/handoff-acceptance.js +10 -1
  50. package/src/core/next-action-pending-actions.js +255 -0
  51. package/src/core/next-action-phases.js +26 -764
  52. package/src/core/next-action-planned-phase.js +51 -0
  53. package/src/core/next-action-quality-guidance.js +19 -0
  54. package/src/core/next-action-recovery-phases.js +97 -0
  55. package/src/core/next-action-refresh.js +20 -0
  56. package/src/core/next-action-review-phase.js +189 -0
  57. package/src/core/next-action-verification-phase.js +192 -0
  58. package/src/core/task-recovery.js +2 -2
  59. package/src/core/transaction-maintenance.js +70 -0
  60. package/src/core/transaction.js +31 -10
  61. package/src/core/work-state.js +20 -11
  62. package/src/integration.d.ts +30 -3
  63. package/src/integration.js +2 -0
  64. package/src/core/cli-metadata.js +0 -23
  65. package/src/core/decision-classification.js +0 -55
  66. package/src/core/gates.js +0 -57
  67. package/src/core/workflow-compatibility.js +0 -151
@@ -0,0 +1,51 @@
1
+ import { taskArtifactPath } from "./task-paths.js";
2
+ import { evaluateStartExecutionPrerequisites, PREFLIGHT_ROUTE_IDENTITY_ERROR_MESSAGE } from "./execution-prerequisites.js";
3
+ import { NEXT_ACTIONS, commandFor, decision, result } from "./next-action-model.js";
4
+ import { artifactError } from "./next-action-artifacts.js";
5
+ import { projectStructuralQualityStatus } from "./structural-quality/service.js";
6
+ import { structuralQualityOptionalActions } from "./next-action-quality-guidance.js";
7
+
8
+ export async function resolvePlannedPhase({ target, packageRoot, explicitTaskId, state, runtimeContext, context, preflightArtifact }) {
9
+ const quality = await projectStructuralQualityStatus({ target, packageRoot, taskId: explicitTaskId ?? state.taskId, runtimeContext });
10
+ if (quality.mode === "gate" && quality.baseline.status !== "OBSERVED") {
11
+ return result({
12
+ ...context,
13
+ nextAction: NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE,
14
+ commands: [commandFor(NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE).replace("<id>", state.taskId)],
15
+ reasons: [artifactError("E_STRUCTURAL_QUALITY_BASELINE_MISSING", "Gate mode requires a structural-quality baseline before execution", [quality.baseline.artifactRef ?? taskArtifactPath(state.taskId, "structuralQuality")])],
16
+ requiredArtifacts: [taskArtifactPath(state.taskId, "structuralQuality")],
17
+ });
18
+ }
19
+ const prerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot, taskId: explicitTaskId, runtimeContext });
20
+ if (prerequisites.errors.length > 0) {
21
+ const preflightOnly = prerequisites.errors.every((error) => error.code.startsWith("E_PREFLIGHT_")
22
+ || (error.code === "E_PHASE_CHRONOLOGY_INVALID"
23
+ && error.message === PREFLIGHT_ROUTE_IDENTITY_ERROR_MESSAGE));
24
+ if (preflightOnly) {
25
+ return result({
26
+ ...context,
27
+ nextAction: NEXT_ACTIONS.RUN_PREFLIGHT,
28
+ reasons: prerequisites.errors,
29
+ commands: [commandFor(NEXT_ACTIONS.RUN_PREFLIGHT)],
30
+ requiredArtifacts: prerequisites.requiredArtifacts,
31
+ missingArtifacts: preflightArtifact.missingArtifacts,
32
+ });
33
+ }
34
+ const routeOnly = prerequisites.errors.every((error) => error.code === "E_ROUTE_STALE" || error.code === "E_ROUTE_GUIDE_MISMATCH");
35
+ return result({
36
+ ...context,
37
+ nextAction: routeOnly ? NEXT_ACTIONS.RESOLVE_STALE_ROUTE : NEXT_ACTIONS.RESOLVE_BLOCKER,
38
+ reasons: prerequisites.errors,
39
+ requiredArtifacts: prerequisites.requiredArtifacts,
40
+ missingArtifacts: preflightArtifact.missingArtifacts,
41
+ });
42
+ }
43
+ return decision(
44
+ context,
45
+ NEXT_ACTIONS.START_EXECUTION,
46
+ artifactError("PHASE_PLANNED", "The persisted preflight is READY"),
47
+ [],
48
+ [],
49
+ structuralQualityOptionalActions(quality, state.taskId),
50
+ );
51
+ }
@@ -0,0 +1,19 @@
1
+ import { NEXT_ACTIONS } from "./next-action-model.js";
2
+
3
+ export function structuralQualityOptionalActions(quality, taskId) {
4
+ if (quality?.mode !== "observe") return [];
5
+ if (quality.baseline?.status !== "OBSERVED") {
6
+ return [{
7
+ action: NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE,
8
+ command: `forgeloop quality-baseline --task ${taskId} --json`,
9
+ }];
10
+ }
11
+ if (quality.freshness === "STALE" || quality.current?.verificationCycle === null
12
+ || quality.current?.verificationCycle === undefined) {
13
+ return [{
14
+ action: NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY,
15
+ command: `forgeloop quality-verify --task ${taskId} --json`,
16
+ }];
17
+ }
18
+ return [];
19
+ }
@@ -0,0 +1,97 @@
1
+ import { validateEventLedger } from "./events.js";
2
+ import { NEXT_ACTIONS, commandFor, decision, recordDiagnosisCommandSpec, recordInterventionCommandSpec, result } from "./next-action-model.js";
3
+ import { artifactError } from "./next-action-artifacts.js";
4
+ import { resolveCurrentCycleDiagnostic } from "./diagnostic-projection.js";
5
+ import { buildTaskReflection } from "./reflection.js";
6
+ import { evaluateProgress, PROGRESS_STATUS } from "./progress.js";
7
+
8
+ export async function resolveDiagnosingPhase({ target, packageRoot, normalized, state, context, eventsRel, requiredArtifacts, stateRel }) {
9
+ const ledger = await validateEventLedger(target, packageRoot, { taskId: normalized.taskId ?? null });
10
+ const cycle = state.verificationCycle ?? 1;
11
+ const diagEvent = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, cycle);
12
+ if (!diagEvent) {
13
+ return result({
14
+ ...context,
15
+ nextAction: NEXT_ACTIONS.RECORD_DIAGNOSIS,
16
+ reasons: [
17
+ artifactError(
18
+ "E_DIAGNOSIS_REQUIRED",
19
+ "Current correction cycle has no append-only diagnosis record.",
20
+ [eventsRel],
21
+ ),
22
+ ],
23
+ commandSpecs: [recordDiagnosisCommandSpec()],
24
+ requiredArtifacts: [...requiredArtifacts, eventsRel],
25
+ });
26
+ }
27
+ const progress = evaluateProgress({ state, events: ledger.events });
28
+ if (progress.status === PROGRESS_STATUS.STALLED) {
29
+ let oscillating = false;
30
+ try {
31
+ const reflection = await buildTaskReflection({ target, packageRoot, taskId: normalized.taskId ?? null });
32
+ oscillating = reflection.oscillation.detected;
33
+ } catch {
34
+ // Reflection is advisory guidance; stall handling must not depend on it.
35
+ }
36
+ return result({
37
+ ...context,
38
+ nextAction: NEXT_ACTIONS.CHANGE_STRATEGY,
39
+ reasons: [
40
+ artifactError(
41
+ "E_PROGRESS_STALLED",
42
+ "The current correction strategy has no new diagnostic information.",
43
+ [stateRel, eventsRel],
44
+ ),
45
+ ],
46
+ progress,
47
+ diagnosticGuidance: {
48
+ action: oscillating ? NEXT_ACTIONS.INTRODUCE_NEW_OBSERVATION : NEXT_ACTIONS.REQUIRE_NEW_DIAGNOSTIC_INFORMATION,
49
+ signals: oscillating ? ["OSCILLATING_STRATEGY"] : ["NO_INFORMATION_GAIN", ...progress.signals.map((signal) => signal.code)],
50
+ errorCodes: oscillating ? ["E_STRATEGY_OSCILLATION", "E_PROGRESS_STALLED"] : ["E_PROGRESS_STALLED"],
51
+ },
52
+ requiredArtifacts,
53
+ });
54
+ }
55
+ return result({
56
+ ...context,
57
+ nextAction: NEXT_ACTIONS.CORRECT,
58
+ commands: [commandFor(NEXT_ACTIONS.CORRECT)],
59
+ reasons: [
60
+ artifactError("PHASE_DIAGNOSING", "The persisted diagnosis hypothesis permits correction"),
61
+ ],
62
+ ...(progress.status === PROGRESS_STATUS.WATCH ? { progress } : {}),
63
+ requiredArtifacts,
64
+ });
65
+ }
66
+
67
+ export async function resolveCorrectingPhase({ target, packageRoot, normalized, state, context, requiredArtifacts }) {
68
+ const ledgerForCorrection = await validateEventLedger(target, packageRoot, { taskId: normalized.taskId ?? null });
69
+ const correctionCycle = state.verificationCycle ?? 1;
70
+ const hasStructuredCase = ledgerForCorrection.events.some(
71
+ (event) => event.event === "DIAGNOSTIC_CASE_RECORDED"
72
+ && event.taskId === state.taskId
73
+ && event.details?.verificationCycle === correctionCycle,
74
+ );
75
+ const hasIntervention = ledgerForCorrection.events.some(
76
+ (event) => event.event === "INTERVENTION_RECORDED"
77
+ && event.taskId === state.taskId
78
+ && event.details?.verificationCycle === correctionCycle,
79
+ );
80
+ if (hasStructuredCase && !hasIntervention) {
81
+ return result({
82
+ ...context,
83
+ nextAction: NEXT_ACTIONS.ENTER_VERIFYING,
84
+ reasons: [
85
+ artifactError("PHASE_CORRECTING", "A structured diagnostic case is awaiting a recorded intervention"),
86
+ ],
87
+ diagnosticGuidance: {
88
+ action: NEXT_ACTIONS.RECORD_INTERVENTION,
89
+ signals: ["INTERVENTION_NOT_BOUND_TO_HYPOTHESIS"],
90
+ errorCodes: [],
91
+ commandSpecs: [recordInterventionCommandSpec(normalized.taskId)],
92
+ },
93
+ requiredArtifacts,
94
+ });
95
+ }
96
+ return decision(context, NEXT_ACTIONS.ENTER_VERIFYING, artifactError("PHASE_CORRECTING", "Correction is ready for verification"));
97
+ }
@@ -0,0 +1,20 @@
1
+ import { validateEventLedger } from "./events.js";
2
+ import { readPersistedRoute } from "./route-artifact.js";
3
+ import { directCommandSpec } from "./next-action-model.js";
4
+
5
+ /** Offer checkpoint removal only before execution, with valid blocked chronology. */
6
+ export async function preExecutionRefreshGuidance({ target, packageRoot, state, contract }) {
7
+ if (!["ROUTED", "DESIGNING", "PLANNED"].includes(state.phase)) return {};
8
+ const ledger = await validateEventLedger(target, packageRoot, { taskId: state.taskId });
9
+ if (!ledger.valid || ledger.events.some((entry) => entry.event === "EXECUTION_STARTED")) return {};
10
+ const lastPreflight = ledger.events.filter((entry) => ["PREFLIGHT_READY", "PREFLIGHT_BLOCKED"].includes(entry.event)).at(-1);
11
+ if (lastPreflight?.event !== "PREFLIGHT_BLOCKED") return {};
12
+ let route;
13
+ try { route = await readPersistedRoute(target, packageRoot, { taskId: state.taskId }); }
14
+ catch { return {}; }
15
+ if (route.value.contractFingerprint !== contract.fingerprint) return {};
16
+ return {
17
+ commands: [`forgeloop clear-state --task ${state.taskId}`, `forgeloop preflight --task ${state.taskId}`],
18
+ commandSpecs: [directCommandSpec("clear-state", state.taskId), directCommandSpec("preflight", state.taskId)],
19
+ };
20
+ }
@@ -0,0 +1,189 @@
1
+ import { readJsonArtifact } from "./artifacts.js";
2
+ import { evaluateCompletion } from "./completion.js";
3
+ import { validateReceipt } from "./receipt.js";
4
+ import { completionRelationshipErrors } from "./completion-relationships.js";
5
+ import { evaluateRequiredEvidence, classifyRequirement } from "./evidence-readiness.js";
6
+ import { validateEventLedger, validateCompletionRecoveryAuthorization } from "./events.js";
7
+ import { NEXT_ACTIONS, commandFor, decision, recordTerminalResultCommandSpec, result } from "./next-action-model.js";
8
+ import { artifactError, checkListReasons, loadArtifact, requirementsAndCoverage } from "./next-action-artifacts.js";
9
+
10
+ export async function resolveReviewingPhase({ state, context, requiredArtifacts, target, packageRoot, contract, route, preflight, authorityContext, runtimeContext, explicitTaskId, eventsRel, receiptRel, stateRel, policyRecoveryAction }) {
11
+ const invalidChecks = checkListReasons(state);
12
+ if (invalidChecks.length > 0) {
13
+ return result({
14
+ ...context,
15
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
16
+ reasons: invalidChecks,
17
+ requiredArtifacts,
18
+ });
19
+ }
20
+ const evidence = await requirementsAndCoverage({
21
+ target,
22
+ packageRoot,
23
+ contract,
24
+ route,
25
+ checks: state.checks,
26
+ additionalEvidence: preflight.policy?.requiredEvidence ?? [],
27
+ authorityContext,
28
+ runtimeContext,
29
+ });
30
+ const readiness = evaluateRequiredEvidence({
31
+ requirements: evidence.requirements,
32
+ checks: state.checks,
33
+ target,
34
+ taskId: contract?.value?.taskId,
35
+ options: { authorityContext, runtimeContext },
36
+ });
37
+ if (!readiness.ready) {
38
+ let recoveryAuthorized = false;
39
+ if (state.lastCompletionAttempt?.status === "REJECTED") {
40
+ try {
41
+ const ledger = await validateEventLedger(target, packageRoot, { taskId: explicitTaskId, eventsPath: eventsRel });
42
+ let currentReceipt = null;
43
+ try {
44
+ const receiptArtifact = await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot);
45
+ currentReceipt = receiptArtifact?.value;
46
+ } catch {}
47
+ const recoveryAuth = validateCompletionRecoveryAuthorization({
48
+ state,
49
+ receipt: currentReceipt,
50
+ events: ledger.events,
51
+ });
52
+ recoveryAuthorized = recoveryAuth.authorized;
53
+ } catch {
54
+ recoveryAuthorized = false;
55
+ }
56
+ }
57
+ return result({
58
+ ...context,
59
+ nextAction: recoveryAuthorized
60
+ ? NEXT_ACTIONS.ENTER_VERIFYING
61
+ : NEXT_ACTIONS.RESOLVE_BLOCKER,
62
+ commands: recoveryAuthorized
63
+ ? [commandFor(NEXT_ACTIONS.ENTER_VERIFYING)]
64
+ : [],
65
+ reasons: [...readiness.invalid, ...readiness.partial, ...readiness.missing]
66
+ .map((item) => artifactError(
67
+ readiness.missing.some((candidate) => candidate.id === item.id)
68
+ ? "E_EVIDENCE_REQUIRED"
69
+ : item.reasonCode ?? "E_EVIDENCE_PARTIAL",
70
+ `Evidence is not ready: ${item.text}`,
71
+ [stateRel],
72
+ )),
73
+ requiredArtifacts,
74
+ });
75
+ }
76
+ const receipt = await loadArtifact(
77
+ () => readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot),
78
+ receiptRel,
79
+ );
80
+ if (receipt.error) {
81
+ if (receipt.error.code !== "ARTIFACT_MISSING") {
82
+ return decision(
83
+ context,
84
+ NEXT_ACTIONS.RESOLVE_BLOCKER,
85
+ artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${receipt.error.message}`, [receiptRel]),
86
+ [...requiredArtifacts, receiptRel],
87
+ );
88
+ }
89
+ return decision(
90
+ context,
91
+ NEXT_ACTIONS.PREPARE_COMPLETION,
92
+ artifactError(
93
+ receipt.error.code === "ARTIFACT_MISSING" ? "E_RECEIPT_MISSING" : "E_RECEIPT_INVALID",
94
+ receipt.error.message,
95
+ [receiptRel],
96
+ ),
97
+ [...requiredArtifacts, receiptRel],
98
+ receipt.missingArtifacts,
99
+ );
100
+ }
101
+ try {
102
+ await validateReceipt(receipt.value.value, packageRoot, {
103
+ target,
104
+ taskId: contract?.value?.taskId,
105
+ authorityContext,
106
+ runtimeContext,
107
+ });
108
+ } catch (error) {
109
+ return decision(
110
+ context,
111
+ NEXT_ACTIONS.RESOLVE_BLOCKER,
112
+ artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${error.message}`, [receiptRel]),
113
+ [...requiredArtifacts, receiptRel],
114
+ );
115
+ }
116
+ const receiptRelationships = completionRelationshipErrors({
117
+ contract,
118
+ route,
119
+ state,
120
+ receipt: receipt.value.value,
121
+ requiredEvidence: evidence.requirements,
122
+ target,
123
+ taskId: contract?.value?.taskId,
124
+ authorityContext,
125
+ runtimeContext,
126
+ });
127
+ if (receiptRelationships.length > 0) {
128
+ if (receiptRelationships.some((err) => (
129
+ err.code === "E_RECEIPT_STATE_MISMATCH"
130
+ || err.code === "E_RECEIPT_CYCLE_MISMATCH"
131
+ || err.code === "E_RECEIPT_CONTRACT_MISMATCH"
132
+ || err.code === "E_ROUTE_GUIDE_MISMATCH"
133
+ || err.code === "E_EVIDENCE_COVERAGE_INVALID"
134
+ ))) {
135
+ return decision(
136
+ context,
137
+ NEXT_ACTIONS.PREPARE_COMPLETION,
138
+ artifactError("E_RECEIPT_STATE_MISMATCH", "Run forgeloop prepare-completion to refresh the execution receipt with current state", [receiptRel]),
139
+ [...requiredArtifacts, receiptRel],
140
+ );
141
+ }
142
+ return result({
143
+ ...context,
144
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
145
+ reasons: receiptRelationships,
146
+ requiredArtifacts: [...requiredArtifacts, receiptRel],
147
+ });
148
+ }
149
+
150
+ const completion = await evaluateCompletion({ target, packageRoot, taskId: explicitTaskId, authorityContext, runtimeContext });
151
+ if (completion.status !== "VALID") {
152
+ const terminalPendingErrors = completion.errors.filter((err) => (
153
+ err.code === "E_PUBLICATION_REQUIREMENT_PENDING" || err.code === "E_PRODUCTION_REQUIREMENT_PENDING"
154
+ ));
155
+ if (terminalPendingErrors.length > 0 && terminalPendingErrors.length === completion.errors.length) {
156
+ const terminalPendingReqs = terminalPendingErrors.map((err) => {
157
+ const reqId = err.requirementId;
158
+ const matchingReq = evidence.requirements.find((r) => r.id === reqId)
159
+ ?? evidence.requirements.find((r) => r.text === reqId)
160
+ ?? classifyRequirement(reqId ?? err.message);
161
+ return matchingReq;
162
+ });
163
+ return result({
164
+ ...context,
165
+ nextAction: NEXT_ACTIONS.RECORD_TERMINAL_RESULT,
166
+ commands: ["forgeloop record-terminal-result"],
167
+ commandSpecs: terminalPendingReqs.map(recordTerminalResultCommandSpec),
168
+ reasons: completion.errors,
169
+ requiredArtifacts: [...requiredArtifacts, receiptRel, eventsRel],
170
+ });
171
+ }
172
+ const policyAction = policyRecoveryAction(completion.errors);
173
+ if (policyAction) {
174
+ return result({
175
+ ...context,
176
+ nextAction: policyAction,
177
+ reasons: completion.errors,
178
+ requiredArtifacts: [...requiredArtifacts, receiptRel, eventsRel],
179
+ });
180
+ }
181
+ return result({
182
+ ...context,
183
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
184
+ reasons: completion.errors,
185
+ requiredArtifacts: [...requiredArtifacts, receiptRel, eventsRel],
186
+ });
187
+ }
188
+ return decision(context, NEXT_ACTIONS.RUN_COMPLETE, artifactError("COMPLETION_READY", "Completion artifacts and cross-artifact validation are valid"));
189
+ }
@@ -0,0 +1,192 @@
1
+ import { readJsonArtifact } from "./artifacts.js";
2
+ import { taskArtifactPath, taskStructuralQualityDirectory } from "./task-paths.js";
3
+ import { validateReceipt } from "./receipt.js";
4
+ import { completionRelationshipErrors } from "./completion-relationships.js";
5
+ import { evaluateRequiredEvidence, authoritativeChecksForRequirements, ordinaryLeafRequirements } from "./evidence-readiness.js";
6
+ import { NEXT_ACTIONS, commandFor, decision, recordCheckCommandSpec, result } from "./next-action-model.js";
7
+ import { artifactError, checkListReasons, loadArtifact, requirementsAndCoverage } from "./next-action-artifacts.js";
8
+ import { projectStructuralQualityStatus } from "./structural-quality/service.js";
9
+ import { structuralQualityOptionalActions } from "./next-action-quality-guidance.js";
10
+
11
+ export async function resolveVerifyingPhase({ target, packageRoot, explicitTaskId, state, runtimeContext, context, requiredArtifacts, contract, route, preflight, authorityContext, stateRel, receiptRel }) {
12
+ const quality = await projectStructuralQualityStatus({ target, packageRoot, taskId: explicitTaskId ?? state.taskId, runtimeContext });
13
+ if (quality.mode === "gate") {
14
+ if (quality.baseline.status !== "OBSERVED") {
15
+ return result({
16
+ ...context,
17
+ nextAction: NEXT_ACTIONS.RESOLVE_STRUCTURAL_QUALITY_BLOCKER,
18
+ reasons: [artifactError("E_STRUCTURAL_QUALITY_BASELINE_MISSING", "Structural-quality gate cannot verify without its immutable baseline", [taskArtifactPath(state.taskId, "structuralQuality")])],
19
+ requiredArtifacts: [taskArtifactPath(state.taskId, "structuralQuality")],
20
+ });
21
+ }
22
+ if (!quality.current.artifactRef || quality.current.verificationCycle !== (state.verificationCycle ?? 1)) {
23
+ return result({
24
+ ...context,
25
+ nextAction: NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY,
26
+ commands: [commandFor(NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY).replace("<id>", state.taskId)],
27
+ reasons: [artifactError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "The current verification cycle has no structural-quality evaluation", [taskStructuralQualityDirectory(state.taskId)])],
28
+ requiredArtifacts: [taskStructuralQualityDirectory(state.taskId)],
29
+ });
30
+ }
31
+ if (quality.freshness === "STALE") {
32
+ return result({
33
+ ...context,
34
+ nextAction: NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY,
35
+ commands: [commandFor(NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY).replace("<id>", state.taskId)],
36
+ reasons: [artifactError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "Structural-quality evidence is stale; fresh verification is required before review", [quality.current.artifactRef])],
37
+ requiredArtifacts: [taskStructuralQualityDirectory(state.taskId)],
38
+ });
39
+ }
40
+ if (quality.current.status === "FAIL") {
41
+ return decision(context, NEXT_ACTIONS.DIAGNOSE_STRUCTURAL_QUALITY_REGRESSION, artifactError("E_STRUCTURAL_QUALITY_REGRESSION", "Structural-quality verification detected a regression", [quality.current.artifactRef]));
42
+ }
43
+ if (quality.current.status === "BLOCKED") {
44
+ return decision(context, NEXT_ACTIONS.RESOLVE_STRUCTURAL_QUALITY_BLOCKER, artifactError("E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE", "Structural-quality verification is blocked", [quality.current.artifactRef]));
45
+ }
46
+ }
47
+ const invalidChecks = checkListReasons(state);
48
+ if (invalidChecks.length > 0) {
49
+ return result({
50
+ ...context,
51
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
52
+ reasons: invalidChecks,
53
+ requiredArtifacts,
54
+ });
55
+ }
56
+ const evidence = await requirementsAndCoverage({
57
+ target,
58
+ packageRoot,
59
+ contract,
60
+ route,
61
+ checks: state.checks,
62
+ additionalEvidence: preflight.policy?.requiredEvidence ?? [],
63
+ authorityContext,
64
+ runtimeContext,
65
+ });
66
+ const authoritative = authoritativeChecksForRequirements({
67
+ requirements: ordinaryLeafRequirements(evidence.requirements),
68
+ checks: state.checks,
69
+ });
70
+ const failed = authoritative.find(({ check }) => check?.status === "failed");
71
+ if (failed) {
72
+ return decision(
73
+ context,
74
+ NEXT_ACTIONS.DIAGNOSE,
75
+ artifactError("E_CHECK_FAILED", `Observed check failed: ${failed.check.id}`, [stateRel]),
76
+ );
77
+ }
78
+ const blocked = authoritative.find(({ check }) => check?.status === "blocked");
79
+ if (blocked) {
80
+ return decision(
81
+ context,
82
+ NEXT_ACTIONS.RESOLVE_BLOCKER,
83
+ artifactError("E_CHECK_BLOCKED", `Observed check is blocked: ${blocked.check.id}`, [stateRel]),
84
+ );
85
+ }
86
+ const receipt = await loadArtifact(
87
+ () => readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot),
88
+ receiptRel,
89
+ );
90
+ if (receipt.error) {
91
+ if (receipt.error.code === "ARTIFACT_MISSING") {
92
+ return decision(
93
+ context,
94
+ NEXT_ACTIONS.PREPARE_COMPLETION,
95
+ artifactError("E_RECEIPT_MISSING", "Prepare the execution receipt before recording verification checks", [receiptRel]),
96
+ [...requiredArtifacts, receiptRel],
97
+ receipt.missingArtifacts,
98
+ );
99
+ }
100
+ return decision(
101
+ context,
102
+ NEXT_ACTIONS.RESOLVE_BLOCKER,
103
+ artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${receipt.error.message}`, [receiptRel]),
104
+ [...requiredArtifacts, receiptRel],
105
+ );
106
+ }
107
+ try {
108
+ await validateReceipt(receipt.value.value, packageRoot, {
109
+ target,
110
+ taskId: contract?.value?.taskId,
111
+ authorityContext,
112
+ runtimeContext,
113
+ });
114
+ } catch (error) {
115
+ return decision(
116
+ context,
117
+ NEXT_ACTIONS.RESOLVE_BLOCKER,
118
+ artifactError("E_RECEIPT_INVALID", `Repair or remove the invalid execution receipt before continuing: ${error.message}`, [receiptRel]),
119
+ [...requiredArtifacts, receiptRel],
120
+ );
121
+ }
122
+ const readiness = evaluateRequiredEvidence({
123
+ requirements: evidence.requirements,
124
+ checks: state.checks,
125
+ target,
126
+ taskId: contract?.value?.taskId,
127
+ options: { authorityContext, runtimeContext },
128
+ });
129
+ const receiptRelationships = completionRelationshipErrors({
130
+ contract,
131
+ route,
132
+ state,
133
+ receipt: receipt.value.value,
134
+ requiredEvidence: evidence.requirements,
135
+ requireRequiredChecks: false,
136
+ target,
137
+ taskId: contract?.value?.taskId,
138
+ authorityContext,
139
+ runtimeContext,
140
+ });
141
+ if (receiptRelationships.length > 0) {
142
+ if (receiptRelationships.some((err) => (
143
+ err.code === "E_RECEIPT_STATE_MISMATCH"
144
+ || err.code === "E_RECEIPT_CYCLE_MISMATCH"
145
+ || err.code === "E_RECEIPT_CONTRACT_MISMATCH"
146
+ || err.code === "E_ROUTE_GUIDE_MISMATCH"
147
+ || err.code === "E_EVIDENCE_COVERAGE_INVALID"
148
+ ))) {
149
+ return decision(
150
+ context,
151
+ NEXT_ACTIONS.PREPARE_COMPLETION,
152
+ artifactError("E_RECEIPT_STATE_MISMATCH", "Run forgeloop prepare-completion to refresh the execution receipt with current state", [receiptRel]),
153
+ [...requiredArtifacts, receiptRel],
154
+ );
155
+ }
156
+ return result({
157
+ ...context,
158
+ nextAction: NEXT_ACTIONS.RESOLVE_BLOCKER,
159
+ reasons: receiptRelationships,
160
+ requiredArtifacts: [...requiredArtifacts, receiptRel],
161
+ });
162
+ }
163
+ if (readiness.ready) {
164
+ return decision(
165
+ context,
166
+ NEXT_ACTIONS.ENTER_REVIEWING,
167
+ artifactError("EVIDENCE_COVERED", "All required observed verification evidence is covered"),
168
+ [],
169
+ [],
170
+ structuralQualityOptionalActions(quality, state.taskId),
171
+ );
172
+ }
173
+ const uncovered = [...readiness.invalid, ...readiness.partial, ...readiness.missing];
174
+ return result({
175
+ ...context,
176
+ nextAction: NEXT_ACTIONS.RECORD_VERIFICATION,
177
+ commands: ["forgeloop record-check"],
178
+ commandSpecs: uncovered.map((item) => recordCheckCommandSpec(item.text)),
179
+ reasons: uncovered
180
+ .map((item) => artifactError(
181
+ readiness.invalid.some((candidate) => candidate.id === item.id)
182
+ ? "E_EVIDENCE_INVALID"
183
+ : readiness.partial.some((candidate) => candidate.id === item.id)
184
+ ? "E_EVIDENCE_PARTIAL"
185
+ : "E_EVIDENCE_REQUIRED",
186
+ "Run the required check and record observed evidence through the structured command specification.",
187
+ [stateRel],
188
+ )),
189
+ requiredArtifacts: requiredArtifacts,
190
+ optionalActions: structuralQualityOptionalActions(quality, state.taskId),
191
+ });
192
+ }
@@ -5,7 +5,7 @@ import {
5
5
  E_TASK_RECOVERY_AUTHORITY_INVALID,
6
6
  } from "./error-codes.js";
7
7
  import { taskArtifactPath } from "./task-paths.js";
8
- import { getActiveTaskTransaction } from "./transaction.js";
8
+ import { getTaskTransaction } from "./transaction.js";
9
9
  import { classifyRecoveryHistory } from "./recovery-history.js";
10
10
 
11
11
  export const TASK_RECOVERY_SCHEMA_VERSION = 1;
@@ -86,7 +86,7 @@ export function writeTaskRecovery(target, recovery, packageRoot) {
86
86
  }
87
87
 
88
88
  export async function clearTaskRecovery(target, { taskId } = {}) {
89
- const transaction = getActiveTaskTransaction();
89
+ const transaction = (await getTaskTransaction(target));
90
90
  if (!transaction) {
91
91
  throw new Error("clearTaskRecovery requires an active task transaction");
92
92
  }
@@ -0,0 +1,70 @@
1
+ import { lstat, readdir, readFile, rm } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { assertSafePath, ensureWithin, fileExists, writeFileAtomic } from "./filesystem.js";
4
+ import { withTaskLock } from "./task-lock.js";
5
+
6
+ const ROOT = ".forgeloop/.txn";
7
+ const TERMINAL = new Set(["COMMITTED", "ROLLED_BACK", "ABORTED"]);
8
+ const DAY_MS = 86_400_000;
9
+
10
+ async function payloadBytes(directory) {
11
+ let bytes = 0;
12
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
13
+ const child = path.join(directory, entry.name);
14
+ const metadata = await lstat(child);
15
+ if (metadata.isSymbolicLink()) throw new Error("Transaction payload contains a symlink");
16
+ if (metadata.isDirectory()) bytes += await payloadBytes(child);
17
+ else if (metadata.isFile()) bytes += metadata.size;
18
+ else throw new Error("Transaction payload contains a special file");
19
+ }
20
+ return bytes;
21
+ }
22
+
23
+ /** Compact terminal payloads only; preserve manifests and all event ledgers. */
24
+ export async function compactTransactions({ target, retainDays = 7, apply = false, now = Date.now() } = {}) {
25
+ if (!Number.isFinite(retainDays) || retainDays < 1) throw new Error("retainDays must be at least one day");
26
+ await assertSafePath(target, ROOT);
27
+ const root = ensureWithin(target, ROOT);
28
+ const report = { apply, scanned: 0, eligible: 0, bytes: 0, compacted: 0, skipped: [] };
29
+ if (!await fileExists(root)) return report;
30
+ for (const entry of await readdir(root, { withFileTypes: true })) {
31
+ if (!entry.isDirectory()) continue;
32
+ report.scanned += 1;
33
+ const relative = `${ROOT}/${entry.name}`;
34
+ const manifestPath = `${relative}/manifest.json`;
35
+ try {
36
+ await assertSafePath(target, manifestPath);
37
+ const original = JSON.parse(await readFile(ensureWithin(target, manifestPath), "utf8"));
38
+ const lockTaskId = original.lockTaskId ?? original.taskId;
39
+ if (typeof lockTaskId !== "string" || !lockTaskId) throw new Error("Missing lock identity");
40
+ await withTaskLock(target, lockTaskId, "compact-transactions", async () => {
41
+ const manifest = JSON.parse(await readFile(ensureWithin(target, manifestPath), "utf8"));
42
+ if ((manifest.lockTaskId ?? manifest.taskId) !== lockTaskId || manifest.transactionId !== entry.name) throw new Error("Changed transaction identity");
43
+ const timestamp = Date.parse(manifest.committedAt ?? manifest.recoveredAt ?? manifest.failedAt ?? manifest.startedAt);
44
+ if (!TERMINAL.has(manifest.status) || !Number.isFinite(timestamp) || now - timestamp < retainDays * DAY_MS) return;
45
+ const payloads = [];
46
+ let bytes = 0;
47
+ for (const name of ["stage", "backup"]) {
48
+ const payload = `${relative}/${name}`;
49
+ await assertSafePath(target, payload);
50
+ const fullPath = ensureWithin(target, payload);
51
+ if (await fileExists(fullPath)) {
52
+ bytes += await payloadBytes(fullPath);
53
+ payloads.push(fullPath);
54
+ }
55
+ }
56
+ if (!payloads.length) return;
57
+ report.eligible += 1;
58
+ report.bytes += bytes;
59
+ if (!apply) return;
60
+ for (const payload of payloads) await rm(payload, { recursive: true, force: true, maxRetries: 3 });
61
+ manifest.compactedAt = new Date(now).toISOString();
62
+ await writeFileAtomic(ensureWithin(target, manifestPath), `${JSON.stringify(manifest, null, 2)}\n`);
63
+ report.compacted += 1;
64
+ });
65
+ } catch (error) {
66
+ report.skipped.push({ transactionId: entry.name, reason: error.message });
67
+ }
68
+ }
69
+ return report;
70
+ }