@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.
Files changed (126) hide show
  1. package/DOCS_INDEX.md +13 -8
  2. package/EXECUTION_STATE.md +20 -0
  3. package/LOOP_ENGINEERING.md +81 -0
  4. package/LOOP_SYSTEM_DESIGN.md +32 -0
  5. package/PROTOCOL_INTEGRATION.md +46 -0
  6. package/QUALITY_SCORECARD.md +2 -0
  7. package/README.md +31 -9
  8. package/THIRD_PARTY_NOTICES.md +15 -0
  9. package/THREAT_MODEL.md +39 -0
  10. package/docs/ARTIFACT_REFERENCE.md +140 -0
  11. package/docs/CLI_REFERENCE.md +294 -3
  12. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  13. package/docs/DOCUMENTATION_GUIDE.md +22 -13
  14. package/docs/EXECUTION_TRACE.md +76 -0
  15. package/docs/MCP.md +33 -0
  16. package/docs/RECIPES.md +67 -0
  17. package/docs/TROUBLESHOOTING.md +106 -2
  18. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  19. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  20. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  21. package/docs/diagrams/README.md +55 -0
  22. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  23. package/docs/diagrams/manifest.json +42 -0
  24. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  25. package/package.json +8 -6
  26. package/schemas/action.schema.json +100 -0
  27. package/schemas/approval.schema.json +51 -0
  28. package/schemas/capability-policy.schema.json +41 -0
  29. package/schemas/diagnostic-case.schema.json +85 -0
  30. package/schemas/execution-receipt.schema.json +16 -0
  31. package/schemas/hypothesis-disposition.schema.json +16 -0
  32. package/schemas/intervention.schema.json +27 -0
  33. package/schemas/policy-lock.schema.json +1 -0
  34. package/schemas/policy-snapshot.schema.json +2 -0
  35. package/schemas/trajectory-evaluation.schema.json +64 -0
  36. package/schemas/trajectory-scenario.schema.json +42 -0
  37. package/src/cli.js +94 -0
  38. package/src/commands/action-authorize.js +41 -0
  39. package/src/commands/action-propose.js +10 -0
  40. package/src/commands/action-reconcile.js +10 -0
  41. package/src/commands/action-record.js +47 -0
  42. package/src/commands/action-show.js +10 -0
  43. package/src/commands/action-verify.js +10 -0
  44. package/src/commands/advance.js +7 -2
  45. package/src/commands/approval-request.js +64 -0
  46. package/src/commands/approval-resolve.js +10 -0
  47. package/src/commands/baseline.js +3 -3
  48. package/src/commands/eval.js +6 -0
  49. package/src/commands/history.js +18 -0
  50. package/src/commands/init.js +2 -2
  51. package/src/commands/inspect.js +49 -0
  52. package/src/commands/metrics.js +7 -0
  53. package/src/commands/next.js +8 -2
  54. package/src/commands/policy-discover.js +2 -2
  55. package/src/commands/record-diagnosis.js +37 -1
  56. package/src/commands/record-hypothesis-disposition.js +45 -0
  57. package/src/commands/record-intervention.js +35 -0
  58. package/src/commands/reflect.js +38 -0
  59. package/src/commands/report.js +9 -1
  60. package/src/commands/run-action.js +18 -0
  61. package/src/commands/trace.js +34 -0
  62. package/src/commands/validate-protocol.js +21 -13
  63. package/src/core/action-authorization.js +106 -0
  64. package/src/core/action-constants.js +86 -0
  65. package/src/core/action-execution.js +105 -0
  66. package/src/core/action-ledger-projection.js +302 -0
  67. package/src/core/action-model.js +581 -0
  68. package/src/core/action-readiness.js +141 -0
  69. package/src/core/action-reconciliation-policy.js +49 -0
  70. package/src/core/action-reconciliation.js +66 -0
  71. package/src/core/action-verification.js +111 -0
  72. package/src/core/actions.js +462 -0
  73. package/src/core/approvals.js +405 -0
  74. package/src/core/artifact-registry.js +48 -0
  75. package/src/core/audit.js +25 -0
  76. package/src/core/bundles.js +15 -0
  77. package/src/core/capability-policy.js +226 -0
  78. package/src/core/cli-command-definitions.js +210 -1
  79. package/src/core/command-executors.js +171 -15
  80. package/src/core/command-runtime.js +12 -1
  81. package/src/core/completion-artifacts.js +37 -12
  82. package/src/core/completion-recovery-rebind.js +194 -0
  83. package/src/core/completion.js +70 -0
  84. package/src/core/continuity-reconciliation.js +24 -5
  85. package/src/core/diagnostic-model.js +396 -0
  86. package/src/core/diagnostic-projection.js +51 -0
  87. package/src/core/diagnostic-record.js +360 -0
  88. package/src/core/error-codes.js +343 -0
  89. package/src/core/events.js +41 -1
  90. package/src/core/execution-prerequisites.js +4 -1
  91. package/src/core/execution.js +26 -188
  92. package/src/core/failure-signature.js +70 -0
  93. package/src/core/failure-surface.js +57 -0
  94. package/src/core/history.js +110 -0
  95. package/src/core/hypothesis-projection.js +85 -0
  96. package/src/core/information-gain-projection.js +283 -0
  97. package/src/core/information-gain.js +138 -0
  98. package/src/core/inspect.js +105 -7
  99. package/src/core/integration-invocation-policy.js +47 -0
  100. package/src/core/integration-resources.js +51 -0
  101. package/src/core/next-action-model.js +35 -1
  102. package/src/core/next-action.js +459 -3
  103. package/src/core/phase.js +40 -21
  104. package/src/core/policy-engine.js +113 -6
  105. package/src/core/preflight-consistency.js +31 -5
  106. package/src/core/preflight.js +19 -2
  107. package/src/core/prepared-execution.js +227 -0
  108. package/src/core/progress.js +41 -4
  109. package/src/core/protocol-info.js +48 -0
  110. package/src/core/protocol.js +14 -0
  111. package/src/core/receipt.js +1 -0
  112. package/src/core/reconcile-closure.js +15 -12
  113. package/src/core/reflection.js +305 -0
  114. package/src/core/resumability.js +57 -3
  115. package/src/core/schema-validation.js +8 -0
  116. package/src/core/strategy-analysis.js +97 -0
  117. package/src/core/task-paths.js +28 -0
  118. package/src/core/task-snapshot.js +53 -0
  119. package/src/core/templates.js +8 -0
  120. package/src/core/trace.js +548 -0
  121. package/src/core/trajectory-evaluation.js +71 -0
  122. package/src/core/trajectory-metrics.js +80 -0
  123. package/src/core/transaction.js +8 -0
  124. package/src/core/work-state.js +10 -5
  125. package/docs/assets/forgeloop-flow.svg +0 -1
  126. package/docs/forgeloop-flow.mmd +0 -51
@@ -1,6 +1,7 @@
1
1
  import { readContract } from "./contract.js";
2
2
  import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
3
- import { appendProtocolEvent, validateEventLedger, readEvents, validateCompletionRecoveryAuthorization } from "./events.js";
3
+ import { appendProtocolEvent, validateEventLedger } from "./events.js";
4
+ import { authorizeCompletionRecoveryOrRebind } from "./completion-recovery-rebind.js";
4
5
  import { runCommandExecution } from "./execution.js";
5
6
  import { createReceipt } from "./receipt.js";
6
7
  import { currentRepositoryFingerprint } from "./repository.js";
@@ -68,7 +69,7 @@ export async function runReconcileClosure({
68
69
  const eventsRel = taskArtifactPath(taskId, "events");
69
70
  const receiptRel = taskArtifactPath(taskId, "receipt");
70
71
 
71
- const state = await readWorkState(target, { packageRoot, taskId });
72
+ let state = await readWorkState(target, { packageRoot, taskId });
72
73
  if (!state) {
73
74
  throw reconcileError("E_RECONCILE_PHASE_INVALID", "Cannot reconcile without work state", [stateRel]);
74
75
  }
@@ -80,22 +81,24 @@ export async function runReconcileClosure({
80
81
  );
81
82
  }
82
83
  if (state.phase === "REVIEWING") {
83
- let receipt = null;
84
- try {
85
- receipt = (await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot))?.value ?? null;
86
- } catch {
87
- receipt = null;
88
- }
89
- const events = await readEvents(target, packageRoot, { taskId });
90
- const recoveryAuth = validateCompletionRecoveryAuthorization({ state, receipt, events });
91
- if (!recoveryAuth.authorized) {
92
- const first = recoveryAuth.errors?.[0] ?? {};
84
+ const recovery = await authorizeCompletionRecoveryOrRebind({
85
+ target,
86
+ packageRoot,
87
+ taskId,
88
+ authorityContext,
89
+ runtimeContext,
90
+ });
91
+ if (!recovery.recoveryAuth.authorized) {
92
+ const first = recovery.recoveryAuth.errors?.[0] ?? {};
93
93
  throw reconcileError(
94
94
  first.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
95
95
  `REVIEWING reconciliation requires authorized completion recovery: ${first.message ?? "unauthorized"}`,
96
96
  [stateRel, receiptRel],
97
97
  );
98
98
  }
99
+ if (recovery.rebound) {
100
+ state = recovery.state;
101
+ }
99
102
  }
100
103
 
101
104
  const freshness = await classifyLoadedWorkState({ target, state, contractFile: contractRel });
@@ -0,0 +1,305 @@
1
+ import { buildTaskTrace } from "./trace.js";
2
+ import { readEvents } from "./events.js";
3
+ import { findTaskById } from "./task-discovery.js";
4
+ import { projectHypothesisStates } from "./hypothesis-projection.js";
5
+ import {
6
+ buildInformationGainProjection,
7
+ evaluateStructuredDiagnosticStall,
8
+ } from "./information-gain-projection.js";
9
+ import { computeFailureSignature } from "./failure-signature.js";
10
+ import {
11
+ computeStrategyFingerprints as computeStrategyFingerprintsImpl,
12
+ detectOscillation as detectOscillationImpl,
13
+ evaluateInterventionEffectiveness as evaluateInterventionEffectivenessImpl,
14
+ } from "./strategy-analysis.js";
15
+
16
+ export const REFLECTION_STATUS = Object.freeze({
17
+ ADVANCING: "ADVANCING",
18
+ WATCH: "WATCH",
19
+ STALLED: "STALLED",
20
+ });
21
+
22
+
23
+ export function summarizeHypotheses(trace) {
24
+ const summary = { created: 0, supported: 0, weakened: 0, falsified: 0, superseded: 0, unresolved: 0, open: 0 };
25
+
26
+ const pseudoEvents = [
27
+ ...trace.diagnostics.cases.map((diagnosticCase) => ({
28
+ seq: diagnosticCase.sequence,
29
+ event: "DIAGNOSTIC_CASE_RECORDED",
30
+ details: diagnosticCase,
31
+ })),
32
+ ...trace.diagnostics.legacyDiagnoses.map((legacy) => ({
33
+ seq: legacy.sequence,
34
+ event: "DIAGNOSIS_RECORDED",
35
+ details: legacy,
36
+ })),
37
+ ...trace.diagnostics.dispositions.map((disposition) => ({
38
+ seq: disposition.sequence,
39
+ event: "HYPOTHESIS_DISPOSITION_RECORDED",
40
+ details: disposition,
41
+ })),
42
+ ];
43
+ const projection = projectHypothesisStates(pseudoEvents);
44
+
45
+ for (const hypothesis of projection.hypotheses) {
46
+ if (hypothesis.id === "h-legacy" && hypothesis.sourceEventSeq !== null) {
47
+ summary.created += 1;
48
+ } else if (hypothesis.id !== "h-legacy") {
49
+ summary.created += 1;
50
+ }
51
+ const status = hypothesis.currentStatus.toLowerCase();
52
+ if (summary[status] !== undefined) summary[status] += 1;
53
+ }
54
+ summary.open = projection.openHypotheses.length;
55
+ return summary;
56
+ }
57
+
58
+ export function detectOscillation(strategies) {
59
+ return detectOscillationImpl(strategies);
60
+ }
61
+
62
+ export function evaluateInterventionEffectiveness(trace, surfacesByCycle) {
63
+ return evaluateInterventionEffectivenessImpl(trace, surfacesByCycle);
64
+ }
65
+
66
+ function failedRequirementSurfacesFromEvents(events = [], state = null) {
67
+ const byCycle = new Map();
68
+ const record = (cycle, requirement) => {
69
+ if (!requirement) return;
70
+ if (!byCycle.has(cycle)) byCycle.set(cycle, new Set());
71
+ byCycle.get(cycle).add(requirement);
72
+ };
73
+ for (const event of events) {
74
+ if (event.event !== "VERIFICATION_RECORDED") continue;
75
+ const d = event.details ?? {};
76
+ if (d.status === "failed" || d.status === "blocked") {
77
+ record(d.verificationCycle ?? 1, d.requirement ?? d.id ?? d.checkId);
78
+ }
79
+ }
80
+ for (const check of state?.checks ?? []) {
81
+ if (check.status !== "failed" && check.status !== "blocked") continue;
82
+ record(check.details?.verificationCycle ?? state?.verificationCycle ?? 1, check.requirement ?? check.id ?? check.checkId);
83
+ }
84
+ return byCycle;
85
+ }
86
+
87
+ function signatureSetsByCycle(trace) {
88
+ const byCycle = new Map();
89
+ for (const entry of trace.failureSignatures) {
90
+ for (const cycle of entry.cycles ?? []) {
91
+ if (!byCycle.has(cycle)) byCycle.set(cycle, new Set());
92
+ byCycle.get(cycle).add(entry.signature);
93
+ }
94
+ }
95
+ return byCycle;
96
+ }
97
+
98
+ export function deriveDiagnosticContext(events = [], state = null) {
99
+ const cycle = state?.verificationCycle ?? null;
100
+ const taskEvents = events.filter((event) => !state?.taskId || event.taskId === state.taskId);
101
+
102
+ // Canonical failure-signature hashes for the active verification cycle.
103
+ const activeFailureSignatures = [...new Set(
104
+ taskEvents
105
+ .filter((event) => event.event === "VERIFICATION_RECORDED"
106
+ && ["failed", "blocked"].includes(event.details?.status)
107
+ && (cycle === null || event.details?.verificationCycle === cycle))
108
+ .map((event) => {
109
+ const d = event.details;
110
+ return computeFailureSignature({
111
+ requirement: d.requirement ?? d.id ?? d.checkId,
112
+ status: d.status,
113
+ exitCode: Number.isInteger(d.exitCode) ? d.exitCode : null,
114
+ failureToken: typeof d.failureToken === "string" && d.failureToken
115
+ ? d.failureToken
116
+ : (typeof d.details?.failureToken === "string" ? d.details.failureToken : null),
117
+ });
118
+ }),
119
+ )].sort();
120
+
121
+ const activeFailedRequirements = [...new Set(
122
+ [
123
+ ...taskEvents
124
+ .filter((event) => event.event === "VERIFICATION_RECORDED"
125
+ && ["failed", "blocked"].includes(event.details?.status)
126
+ && (cycle === null || event.details?.verificationCycle === cycle))
127
+ .map((event) => event.details?.requirement ?? event.details?.id ?? event.details?.checkId),
128
+ ...((state?.checks ?? []))
129
+ .filter((check) => ["failed", "blocked"].includes(check.status)
130
+ && (cycle === null || check.details?.verificationCycle === cycle))
131
+ .map((check) => check.requirement ?? check.id ?? check.checkId),
132
+ ].filter(Boolean),
133
+ )].sort();
134
+
135
+ const projection = projectHypothesisStates(taskEvents);
136
+ const openHypotheses = [...projection.openHypotheses].sort();
137
+
138
+ const interventions = taskEvents.filter((event) => event.event === "INTERVENTION_RECORDED");
139
+ const latestIntervention = interventions.at(-1)?.details?.intervention?.id ?? null;
140
+
141
+ // doNotRepeat requires semantic repetition AND at least two completed
142
+ // post-intervention verification cycles AND unchanged failure surface.
143
+ const surfacesByCycle = failedRequirementSurfacesFromEvents(taskEvents, state);
144
+ const completedCycles = [...new Set(
145
+ taskEvents.filter((event) => event.event === "VERIFICATION_STARTED")
146
+ .map((event) => event.details?.verificationCycle)
147
+ .filter(Number.isInteger),
148
+ )].sort((a, b) => a - b);
149
+
150
+ const fingerprintGroups = new Map();
151
+ for (const event of interventions) {
152
+ const fingerprint = event.details?.interventionSemanticFingerprint;
153
+ if (!fingerprint) continue;
154
+ if (!fingerprintGroups.has(fingerprint)) fingerprintGroups.set(fingerprint, []);
155
+ fingerprintGroups.get(fingerprint).push(event);
156
+ }
157
+
158
+ const doNotRepeat = [];
159
+ for (const [fingerprint, group] of fingerprintGroups.entries()) {
160
+ if (group.length < 2) continue;
161
+ const lastInterventionCycle = Math.max(...group.map((event) => event.details?.verificationCycle ?? 1));
162
+ const postCycles = completedCycles.filter((completed) => completed > lastInterventionCycle);
163
+ if (postCycles.length < 2) continue;
164
+ const firstSurface = [...(surfacesByCycle.get(lastInterventionCycle) ?? [])].sort();
165
+ const latestSurface = [...(surfacesByCycle.get(postCycles.at(-1)) ?? [])].sort();
166
+ if (JSON.stringify(firstSurface) === JSON.stringify(latestSurface)) {
167
+ doNotRepeat.push(fingerprint);
168
+ }
169
+ }
170
+ doNotRepeat.sort();
171
+
172
+ return {
173
+ activeFailureSignatures,
174
+ activeFailedRequirements,
175
+ openHypotheses,
176
+ latestIntervention,
177
+ nextExperiment: null,
178
+ doNotRepeat,
179
+ };
180
+ }
181
+
182
+ export async function buildTaskReflection({ target, packageRoot, taskId = null } = {}) {
183
+ const trace = await buildTaskTrace({ target, packageRoot, taskId });
184
+ // Modern tasks own a scoped ledger; legacy singleton callers still pass a
185
+ // taskId that must be used only as a filter over the canonical ledger.
186
+ const task = taskId ? await findTaskById(target, taskId, packageRoot) : null;
187
+ const rawEvents = await readEvents(target, packageRoot, task ? { taskId } : {});
188
+
189
+ // Authoritative surfaces come from the canonical trace projection.
190
+ const surfaceEntries = {};
191
+ for (const entry of trace.failureSurfaces) {
192
+ surfaceEntries[entry.verificationCycle] = { surface: entry.surface, signatures: [] };
193
+ }
194
+ const signatureSets = signatureSetsByCycle(trace);
195
+ for (const [cycle, signatures] of signatureSets.entries()) {
196
+ if (!surfaceEntries[cycle]) surfaceEntries[cycle] = { surface: [], signatures: [] };
197
+ surfaceEntries[cycle].signatures = [...signatures].sort();
198
+ }
199
+
200
+ const cycles = [...new Set([
201
+ ...trace.failureSurfaces.map((entry) => entry.verificationCycle),
202
+ ...trace.diagnostics.cases.map((diagnosticCase) => diagnosticCase.verificationCycle),
203
+ ])].sort((a, b) => a - b);
204
+
205
+ const strategies = computeStrategyFingerprintsImpl(trace);
206
+ const oscillation = detectOscillation(strategies);
207
+
208
+ const signals = [];
209
+ let status = REFLECTION_STATUS.ADVANCING;
210
+
211
+ const evaluatedInterventions = evaluateInterventionEffectiveness(trace, surfaceEntries);
212
+ const repeatedNonInformative = evaluatedInterventions
213
+ .filter((intervention) => intervention.effectiveness === "NON_INFORMATIVE");
214
+ if (oscillation.detected) {
215
+ signals.push("OSCILLATING_STRATEGY");
216
+ status = REFLECTION_STATUS.WATCH;
217
+ }
218
+ if (repeatedNonInformative.length >= 2) {
219
+ signals.push("REPEATED_INTERVENTION");
220
+ status = REFLECTION_STATUS.WATCH;
221
+ }
222
+
223
+ const lastTwoSameStrategy = strategies.length >= 2
224
+ && strategies.at(-1).strategyFingerprint === strategies.at(-2).strategyFingerprint
225
+ && JSON.stringify(surfaceEntries[strategies.at(-1)?.verificationCycle]?.signatures ?? [])
226
+ === JSON.stringify(surfaceEntries[strategies.at(-2)?.verificationCycle]?.signatures ?? []);
227
+ if (lastTwoSameStrategy && !signals.includes("OSCILLATING_STRATEGY")) {
228
+ status = status === REFLECTION_STATUS.ADVANCING ? REFLECTION_STATUS.WATCH : status;
229
+ }
230
+
231
+ // Information Gain v2 truth comes fully from the authoritative cycle
232
+ // analysis projection. Consumers must not redefine gain semantics.
233
+ const gainProjection = buildInformationGainProjection(rawEvents, taskId ?? null);
234
+ const cyclesWithoutEffectiveGain = gainProjection
235
+ .filter((entry) => !entry.effectiveGain)
236
+ .map((entry) => entry.verificationCycle);
237
+
238
+ // One canonical structured-stall truth shared with phase and progress.
239
+ const stallAnalysis = evaluateStructuredDiagnosticStall(gainProjection);
240
+ const latestNoGain = Boolean(stallAnalysis.stalled);
241
+ let consecutiveNoGainCycles = 0;
242
+ for (let i = gainProjection.length - 1; i >= 0 && gainProjection[i].effectiveGain === false; i--) {
243
+ consecutiveNoGainCycles += 1;
244
+ }
245
+ const previousEntry = gainProjection.length >= 2 ? gainProjection.at(-2) : null;
246
+ const currentEntry = gainProjection.at(-1) ?? null;
247
+ if (latestNoGain) {
248
+ status = REFLECTION_STATUS.STALLED;
249
+ if (!signals.includes("NO_EFFECTIVE_INFORMATION_GAIN")) signals.push("NO_EFFECTIVE_INFORMATION_GAIN");
250
+ }
251
+ if ((trace.actions?.ambiguous ?? 0) > 0) {
252
+ signals.push("EXTERNAL_ACTION_RECONCILIATION_REQUIRED");
253
+ status = status === REFLECTION_STATUS.ADVANCING ? REFLECTION_STATUS.WATCH : status;
254
+ }
255
+
256
+ const recommendedProtocolAction = (trace.actions?.ambiguous ?? 0) > 0
257
+ ? "RECONCILE_EXTERNAL_ACTION"
258
+ : oscillation.detected
259
+ ? "INTRODUCE_NEW_OBSERVATION"
260
+ : (status === REFLECTION_STATUS.STALLED ? "REQUIRE_NEW_DIAGNOSTIC_INFORMATION" : "CONTINUE");
261
+
262
+ return {
263
+ schemaVersion: 1,
264
+ command: "reflect",
265
+ taskId: trace.task.id,
266
+ taskPhase: trace.task.phase,
267
+ integrityValid: trace.integrity.valid,
268
+ snapshotConsistent: trace.snapshot.consistent,
269
+ status,
270
+ verificationCycles: cycles.length,
271
+ failureSurfaces: trace.failureSurfaces,
272
+ hypotheses: summarizeHypotheses(trace),
273
+ interventions: (() => {
274
+ const evaluated = evaluatedInterventions;
275
+ return {
276
+ count: evaluated.length,
277
+ informative: evaluated.filter((intervention) => ["INFORMATIVE", "IMPROVED"].includes(intervention.effectiveness)).length,
278
+ nonInformative: evaluated.filter((intervention) => intervention.effectiveness === "NON_INFORMATIVE").length,
279
+ details: evaluated,
280
+ };
281
+ })(),
282
+ informationGain: {
283
+ cyclesWithCases: trace.diagnostics.cases.map((diagnosticCase) => diagnosticCase.verificationCycle),
284
+ cyclesWithoutEffectiveGain,
285
+ cycles: gainProjection.map(({ dimensions, ...rest }) => ({ ...rest, dimensions })),
286
+ },
287
+ strategies,
288
+ oscillation,
289
+ signals,
290
+ stallAnalysis: {
291
+ latestNoGain,
292
+ consecutiveNoGainCycles,
293
+ sameStrategyAsPrevious: Boolean(previousEntry && currentEntry
294
+ && previousEntry.evidence.strategyFingerprint === currentEntry.evidence.strategyFingerprint),
295
+ sameFailureSurfaceAsPrevious: Boolean(previousEntry && currentEntry
296
+ && JSON.stringify(previousEntry.evidence.failureSurface) === JSON.stringify(currentEntry.evidence.failureSurface)),
297
+ sameFailureSignaturesAsPrevious: Boolean(previousEntry && currentEntry
298
+ && JSON.stringify(previousEntry.evidence.failureSignatures) === JSON.stringify(currentEntry.evidence.failureSignatures)),
299
+ },
300
+ recommendedProtocolAction,
301
+ actions: trace.actions,
302
+ };
303
+ }
304
+
305
+ export { computeStrategyFingerprintsImpl as computeStrategyFingerprints };
@@ -1,22 +1,76 @@
1
1
  import { currentRepositoryFingerprint } from "./repository.js";
2
+ import { validateEventLedger } from "./events.js";
2
3
  import { createWorkState, initializeWorkState, readWorkState, mutateWorkState } from "./work-state.js";
3
4
 
4
5
  const DEFAULT_PENDING_STEPS = ["planning", "implementation", "verification"];
5
6
 
7
+ /**
8
+ * Resume phase derived from the highest lifecycle milestone already recorded in
9
+ * a validated ledger. Recreating a checkpoint at ROUTED for a task whose ledger
10
+ * already passed EXECUTION_STARTED would make every subsequent advance append a
11
+ * duplicate non-repeatable milestone and invalidate the ledger, so restoration
12
+ * must resume at the phase the recorded chronology supports.
13
+ */
14
+ const RESUME_PHASE_BY_MILESTONE = Object.freeze({
15
+ PLAN_RECORDED: "PLANNED",
16
+ EXECUTION_STARTED: "EXECUTING",
17
+ VERIFICATION_STARTED: "VERIFYING",
18
+ VERIFICATION_RECORDED: "VERIFYING",
19
+ });
20
+
21
+ async function deriveResumePhaseFromLedger(target, packageRoot, taskId) {
22
+ let ledger;
23
+ try {
24
+ ledger = await validateEventLedger(target, packageRoot, { taskId });
25
+ } catch {
26
+ return null;
27
+ }
28
+ if (!ledger?.valid) return null;
29
+ const scoped = (ledger.events ?? []).filter((event) => event.taskId === taskId);
30
+ if (scoped.some((event) => event.event === "COMPLETION_VALIDATED")) return null;
31
+ const positions = RESUME_PHASE_BY_MILESTONE;
32
+ let derived = null;
33
+ for (const event of scoped) {
34
+ const phase = positions[event.event];
35
+ if (!phase) continue;
36
+ if (!derived) {
37
+ derived = phase;
38
+ continue;
39
+ }
40
+ if (phase === "VERIFYING") derived = "VERIFYING";
41
+ }
42
+ return derived;
43
+ }
44
+
45
+ function resumeSteps(phase) {
46
+ if (phase === "EXECUTING" || phase === "VERIFYING") {
47
+ return {
48
+ completedSteps: ["contract", "route", "planning", "implementation"],
49
+ pendingSteps: ["verification"],
50
+ };
51
+ }
52
+ return {
53
+ completedSteps: ["contract", "route"],
54
+ pendingSteps: [...DEFAULT_PENDING_STEPS],
55
+ };
56
+ }
57
+
6
58
  export async function ensureResumableState({ target, packageRoot, contract, route, taskId, statePath }) {
7
59
  if (!contract || !route) return null;
8
60
  const existing = await readWorkState(target, { packageRoot, taskId, statePath });
9
61
  if (existing) return existing;
10
62
 
63
+ const resumedPhase = await deriveResumePhaseFromLedger(target, packageRoot, taskId) ?? "ROUTED";
64
+ const steps = resumeSteps(resumedPhase);
11
65
  const state = createWorkState({
12
66
  taskId: contract.value.taskId,
13
67
  contractFingerprint: contract.fingerprint,
14
68
  routeFingerprint: route.fingerprint,
15
69
  repositoryFingerprint: await currentRepositoryFingerprint(target),
16
- phase: "ROUTED",
70
+ phase: resumedPhase,
17
71
  selectedGuides: route.value.guides,
18
- completedSteps: ["contract", "route"],
19
- pendingSteps: DEFAULT_PENDING_STEPS,
72
+ completedSteps: steps.completedSteps,
73
+ pendingSteps: steps.pendingSteps,
20
74
  checks: [],
21
75
  failures: [],
22
76
  blockers: [],
@@ -34,6 +34,14 @@ export const SHIPPED_SCHEMA_NAMES = Object.freeze([
34
34
  "authority",
35
35
  "task-descriptor",
36
36
  "task-recovery",
37
+ "diagnostic-case",
38
+ "intervention",
39
+ "hypothesis-disposition",
40
+ "action",
41
+ "approval",
42
+ "capability-policy",
43
+ "trajectory-evaluation",
44
+ "trajectory-scenario",
37
45
  ]);
38
46
 
39
47
  export class SchemaValidationError extends Error {
@@ -0,0 +1,97 @@
1
+ import { canonicalFingerprint } from "./artifacts.js";
2
+ import { compareFailureSurface } from "./failure-surface.js";
3
+
4
+ export function computeStrategyFingerprints(trace) {
5
+ const byCycle = new Map();
6
+ const add = (cycle, component, fingerprint) => {
7
+ if (!byCycle.has(cycle)) byCycle.set(cycle, { hypotheses: new Set(), contributors: new Set(), interventions: new Set() });
8
+ byCycle.get(cycle)[component].add(fingerprint);
9
+ };
10
+ for (const diagnosticCase of trace.diagnostics.cases) {
11
+ for (const hypothesis of diagnosticCase.hypotheses ?? []) {
12
+ add(diagnosticCase.verificationCycle, "hypotheses", `${hypothesis.statement}`.trim().toLowerCase());
13
+ }
14
+ for (const contributor of diagnosticCase.contributors ?? []) {
15
+ add(diagnosticCase.verificationCycle, "contributors", `${contributor.statement}`.trim().toLowerCase());
16
+ }
17
+ }
18
+ for (const legacy of trace.diagnostics.legacyDiagnoses) {
19
+ add(legacy.verificationCycle, "hypotheses", `${legacy.hypothesis}`.trim().toLowerCase());
20
+ }
21
+ for (const intervention of trace.diagnostics.interventions) {
22
+ add(intervention.verificationCycle, "interventions", intervention.interventionSemanticFingerprint ?? `${intervention.intervention?.statement ?? ""}`.trim().toLowerCase());
23
+ }
24
+
25
+ return [...byCycle.entries()]
26
+ .sort(([a], [b]) => a - b)
27
+ .map(([cycle, components]) => ({
28
+ verificationCycle: cycle,
29
+ strategyFingerprint: JSON.stringify([
30
+ [...components.hypotheses].sort(),
31
+ [...components.contributors].sort(),
32
+ [...components.interventions].sort(),
33
+ ]),
34
+ components: {
35
+ hypotheses: [...components.hypotheses].sort(),
36
+ contributors: [...components.contributors].sort(),
37
+ interventions: [...components.interventions].sort(),
38
+ },
39
+ }));
40
+ }
41
+
42
+ export function detectOscillation(strategies) {
43
+ const fingerprints = strategies.map((strategy) => strategy.strategyFingerprint);
44
+ const patterns = [];
45
+ for (let i = 2; i < fingerprints.length; i++) {
46
+ if (fingerprints[i] === fingerprints[i - 2] && fingerprints[i] !== fingerprints[i - 1]) {
47
+ patterns.push({ kind: "A_B_A", cycles: [strategies[i - 2].verificationCycle, strategies[i - 1].verificationCycle, strategies[i].verificationCycle] });
48
+ }
49
+ }
50
+ for (let i = 3; i < fingerprints.length; i++) {
51
+ if (fingerprints[i] === fingerprints[i - 2]
52
+ && fingerprints[i - 1] === fingerprints[i - 3]
53
+ && fingerprints[i] !== fingerprints[i - 1]) {
54
+ patterns.push({ kind: "A_B_A_B", cycles: [strategies[i - 3].verificationCycle, strategies[i - 2].verificationCycle, strategies[i - 1].verificationCycle, strategies[i].verificationCycle] });
55
+ }
56
+ }
57
+ const detected = patterns.length > 0;
58
+ return { detected, patterns };
59
+ }
60
+
61
+ export function evaluateInterventionEffectiveness(trace, surfacesByCycle) {
62
+ const interventions = [];
63
+ const list = trace.diagnostics.interventions;
64
+ for (const entry of list) {
65
+ const cycle = entry.verificationCycle;
66
+ const laterVerificationCycles = Object.keys(surfacesByCycle).map(Number).filter((value) => value > cycle);
67
+ const nextCycle = laterVerificationCycles.length > 0 ? Math.min(...laterVerificationCycles) : null;
68
+
69
+ let effectiveness = "PENDING";
70
+ if (nextCycle !== null) {
71
+ const previousSurface = surfacesByCycle[cycle]?.surface ?? [];
72
+ const currentSurface = surfacesByCycle[nextCycle]?.surface ?? [];
73
+ const comparison = compareFailureSurface(previousSurface, currentSurface);
74
+ const signatureChanged = JSON.stringify(surfacesByCycle[cycle]?.signatures ?? []) !== JSON.stringify(surfacesByCycle[nextCycle]?.signatures ?? []);
75
+ if (comparison.direction === "REDUCED") effectiveness = "IMPROVED";
76
+ else if (comparison.direction === "EXPANDED") effectiveness = "REGRESSED";
77
+ else if (signatureChanged || comparison.changed) effectiveness = "INFORMATIVE";
78
+ else effectiveness = "NON_INFORMATIVE";
79
+ }
80
+ interventions.push({
81
+ id: entry.intervention?.id ?? null,
82
+ sequence: entry.sequence,
83
+ verificationCycle: cycle,
84
+ kind: entry.intervention?.kind ?? null,
85
+ reversible: entry.intervention?.reversible ?? null,
86
+ semanticFingerprint: entry.interventionSemanticFingerprint ?? null,
87
+ effectiveness,
88
+ });
89
+ }
90
+ return interventions;
91
+ }
92
+
93
+
94
+ export function interventionSemanticFingerprintOf(entry) {
95
+ return entry?.interventionSemanticFingerprint
96
+ ?? (entry?.intervention ? canonicalFingerprint(entry.intervention) : null);
97
+ }
@@ -15,6 +15,9 @@ export const TASK_ARTIFACT_FILES = Object.freeze({
15
15
  events: "events.ndjson",
16
16
  gates: "gates",
17
17
  executions: "executions",
18
+ actions: "actions",
19
+ approvals: "approvals",
20
+ evaluations: "evaluations",
18
21
  policySnapshot: "policy-snapshot.json",
19
22
  recovery: "recovery.json",
20
23
  });
@@ -32,6 +35,7 @@ export const PROJECT_ARTIFACT_PATHS = Object.freeze({
32
35
  policyBaseline: ".forgeloop/policy/baseline.json",
33
36
  policyLock: ".forgeloop/policy/policy.lock",
34
37
  policyDiscovery: ".forgeloop/policy/discovery.json",
38
+ capabilityPolicy: ".forgeloop/policy/capabilities.json",
35
39
  });
36
40
 
37
41
  export const LEGACY_TASK_ARTIFACT_PATHS = Object.freeze({
@@ -74,6 +78,27 @@ export function taskExecutionPath(taskId, executionId) {
74
78
  return `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.executions}/${executionId}.json`;
75
79
  }
76
80
 
81
+ export function taskActionPath(taskId, actionId) {
82
+ if (typeof actionId !== "string" || !/^action-[A-Za-z0-9_-]+$/.test(actionId)) {
83
+ throw new Error(`Invalid action ID: ${actionId}`);
84
+ }
85
+ return `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.actions}/${actionId}.json`;
86
+ }
87
+
88
+ export function taskApprovalPath(taskId, approvalId) {
89
+ if (typeof approvalId !== "string" || !/^approval-[A-Za-z0-9_-]+$/.test(approvalId)) {
90
+ throw new Error(`Invalid approval ID: ${approvalId}`);
91
+ }
92
+ return `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.approvals}/${approvalId}.json`;
93
+ }
94
+
95
+ export function taskEvaluationPath(taskId, evaluationId) {
96
+ if (typeof evaluationId !== "string" || !/^eval-[A-Za-z0-9_-]+$/.test(evaluationId)) {
97
+ throw new Error(`Invalid evaluation ID: ${evaluationId}`);
98
+ }
99
+ return `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.evaluations}/${evaluationId}.json`;
100
+ }
101
+
77
102
  export function taskLockPath(taskId) {
78
103
  assertTaskId(taskId);
79
104
  return `${TASK_LOCK_ROOT}/${taskStorageKey(taskId)}.lock`;
@@ -101,6 +126,9 @@ export function buildTaskArtifactPaths(taskId) {
101
126
  events: `${dir}/${TASK_ARTIFACT_FILES.events}`,
102
127
  gates: `${dir}/${TASK_ARTIFACT_FILES.gates}`,
103
128
  executions: `${dir}/${TASK_ARTIFACT_FILES.executions}`,
129
+ actions: `${dir}/${TASK_ARTIFACT_FILES.actions}`,
130
+ approvals: `${dir}/${TASK_ARTIFACT_FILES.approvals}`,
131
+ evaluations: `${dir}/${TASK_ARTIFACT_FILES.evaluations}`,
104
132
  lock: taskLockPath(taskId),
105
133
  policySnapshot: `${dir}/${TASK_ARTIFACT_FILES.policySnapshot}`,
106
134
  recovery: `${dir}/${TASK_ARTIFACT_FILES.recovery}`,
@@ -0,0 +1,53 @@
1
+ import { readEvents, validateEventLedger } from "./events.js";
2
+ import { readWorkState } from "./work-state.js";
3
+ import { canonicalFingerprint } from "./artifacts.js";
4
+
5
+ function ledgerTail(events) {
6
+ const last = events.at(-1) ?? null;
7
+ return {
8
+ sequence: last?.seq ?? 0,
9
+ hash: last?.hash ?? null,
10
+ };
11
+ }
12
+
13
+ export async function buildTaskSnapshot({
14
+ target,
15
+ packageRoot,
16
+ taskId = null,
17
+ eventsPath = null,
18
+ stateFile = null,
19
+ } = {}) {
20
+ const options = { packageRoot, taskId, ...(eventsPath ? { eventsPath } : {}) };
21
+ const state = await readWorkState(target, { ...options, ...(stateFile ? { statePath: stateFile } : {}) });
22
+ const events = await readEvents(target, packageRoot, options);
23
+ const before = {
24
+ stateRevision: state?.revision ?? null,
25
+ ...ledgerTail(events),
26
+ };
27
+
28
+ const validation = await validateEventLedger(target, packageRoot, options);
29
+
30
+ const rereadState = await readWorkState(target, { ...options, ...(stateFile ? { statePath: stateFile } : {}) });
31
+ const rereadEvents = await readEvents(target, packageRoot, options);
32
+ const after = {
33
+ stateRevision: rereadState?.revision ?? null,
34
+ ...ledgerTail(rereadEvents),
35
+ };
36
+
37
+ const consistent = JSON.stringify(before) === JSON.stringify(after);
38
+
39
+ return {
40
+ consistent,
41
+ taskId: taskId ?? state?.taskId ?? rereadState?.taskId ?? null,
42
+ anchors: before,
43
+ capturedAt: new Date().toISOString(),
44
+ integrity: {
45
+ valid: validation.valid,
46
+ errors: validation.errors,
47
+ eventCount: validation.events.length,
48
+ fingerprint: canonicalFingerprint(validation.events.map(({ seq, event, at }) => ({ seq, event, at }))),
49
+ },
50
+ state: rereadState,
51
+ events: validation.events,
52
+ };
53
+ }
@@ -67,6 +67,14 @@ export const TEMPLATE_PATHS = [
67
67
  "schemas/authority.schema.json",
68
68
  "schemas/task-descriptor.schema.json",
69
69
  "schemas/task-recovery.schema.json",
70
+ "schemas/diagnostic-case.schema.json",
71
+ "schemas/intervention.schema.json",
72
+ "schemas/hypothesis-disposition.schema.json",
73
+ "schemas/action.schema.json",
74
+ "schemas/approval.schema.json",
75
+ "schemas/capability-policy.schema.json",
76
+ "schemas/trajectory-evaluation.schema.json",
77
+ "schemas/trajectory-scenario.schema.json",
70
78
  ];
71
79
 
72
80
  export function getPackageRoot() {