@cassiomc1/forgeloop 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CONTRACT_COVERAGE.md +1 -0
  2. package/DOCS_INDEX.md +20 -10
  3. package/EXECUTION_STATE.md +20 -0
  4. package/LOOP_ENGINEERING.md +103 -1
  5. package/LOOP_SYSTEM_DESIGN.md +32 -0
  6. package/PROTOCOL_INTEGRATION.md +90 -0
  7. package/QUALITY_SCORECARD.md +2 -0
  8. package/README.md +47 -9
  9. package/THIRD_PARTY_NOTICES.md +15 -0
  10. package/THREAT_MODEL.md +41 -1
  11. package/docs/ARTIFACT_REFERENCE.md +159 -0
  12. package/docs/CLI_REFERENCE.md +294 -3
  13. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  14. package/docs/DOCUMENTATION_GUIDE.md +22 -13
  15. package/docs/EXECUTION_TRACE.md +76 -0
  16. package/docs/MCP.md +34 -1
  17. package/docs/RECIPES.md +67 -0
  18. package/docs/RELEASE_CHECKLIST_1_6_1.md +121 -0
  19. package/docs/TROUBLESHOOTING.md +133 -2
  20. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  21. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  22. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  23. package/docs/diagrams/README.md +55 -0
  24. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  25. package/docs/diagrams/manifest.json +42 -0
  26. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  27. package/package.json +13 -8
  28. package/schemas/action.schema.json +100 -0
  29. package/schemas/approval.schema.json +51 -0
  30. package/schemas/capability-policy.schema.json +41 -0
  31. package/schemas/diagnostic-case.schema.json +85 -0
  32. package/schemas/execution-receipt.schema.json +16 -0
  33. package/schemas/execution.schema.json +15 -0
  34. package/schemas/hypothesis-disposition.schema.json +16 -0
  35. package/schemas/intervention.schema.json +27 -0
  36. package/schemas/policy-lock.schema.json +1 -0
  37. package/schemas/policy-snapshot.schema.json +2 -0
  38. package/schemas/trajectory-evaluation.schema.json +64 -0
  39. package/schemas/trajectory-scenario.schema.json +42 -0
  40. package/src/cli.js +94 -0
  41. package/src/commands/action-authorize.js +41 -0
  42. package/src/commands/action-propose.js +10 -0
  43. package/src/commands/action-reconcile.js +10 -0
  44. package/src/commands/action-record.js +47 -0
  45. package/src/commands/action-show.js +10 -0
  46. package/src/commands/action-verify.js +10 -0
  47. package/src/commands/advance.js +7 -2
  48. package/src/commands/approval-request.js +64 -0
  49. package/src/commands/approval-resolve.js +10 -0
  50. package/src/commands/baseline.js +3 -3
  51. package/src/commands/eval.js +6 -0
  52. package/src/commands/history.js +18 -0
  53. package/src/commands/init.js +2 -2
  54. package/src/commands/inspect.js +49 -0
  55. package/src/commands/metrics.js +7 -0
  56. package/src/commands/next.js +8 -2
  57. package/src/commands/policy-discover.js +2 -2
  58. package/src/commands/record-diagnosis.js +37 -1
  59. package/src/commands/record-hypothesis-disposition.js +45 -0
  60. package/src/commands/record-intervention.js +35 -0
  61. package/src/commands/reflect.js +38 -0
  62. package/src/commands/report.js +9 -1
  63. package/src/commands/run-action.js +18 -0
  64. package/src/commands/run-check.js +1 -0
  65. package/src/commands/trace.js +34 -0
  66. package/src/commands/validate-protocol.js +21 -13
  67. package/src/core/action-authorization.js +106 -0
  68. package/src/core/action-constants.js +86 -0
  69. package/src/core/action-execution.js +106 -0
  70. package/src/core/action-ledger-projection.js +302 -0
  71. package/src/core/action-model.js +581 -0
  72. package/src/core/action-readiness.js +141 -0
  73. package/src/core/action-reconciliation-policy.js +49 -0
  74. package/src/core/action-reconciliation.js +66 -0
  75. package/src/core/action-verification.js +111 -0
  76. package/src/core/actions.js +462 -0
  77. package/src/core/approvals.js +405 -0
  78. package/src/core/artifact-registry.js +48 -0
  79. package/src/core/audit.js +25 -0
  80. package/src/core/bundles.js +15 -0
  81. package/src/core/capability-policy.js +226 -0
  82. package/src/core/cli-command-definitions.js +210 -1
  83. package/src/core/command-executors.js +171 -15
  84. package/src/core/command-runtime.js +12 -1
  85. package/src/core/completion-artifacts.js +71 -14
  86. package/src/core/completion-recovery-rebind.js +194 -0
  87. package/src/core/completion.js +70 -0
  88. package/src/core/continuity-reconciliation.js +24 -5
  89. package/src/core/diagnostic-model.js +396 -0
  90. package/src/core/diagnostic-projection.js +51 -0
  91. package/src/core/diagnostic-record.js +360 -0
  92. package/src/core/error-codes.js +363 -0
  93. package/src/core/events.js +41 -1
  94. package/src/core/execution-prerequisites.js +4 -1
  95. package/src/core/execution.js +29 -188
  96. package/src/core/failure-signature.js +70 -0
  97. package/src/core/failure-surface.js +57 -0
  98. package/src/core/history.js +110 -0
  99. package/src/core/hypothesis-projection.js +85 -0
  100. package/src/core/information-gain-projection.js +283 -0
  101. package/src/core/information-gain.js +138 -0
  102. package/src/core/inspect.js +105 -7
  103. package/src/core/integration-invocation-policy.js +55 -0
  104. package/src/core/integration-resources.js +51 -0
  105. package/src/core/next-action-model.js +35 -1
  106. package/src/core/next-action.js +459 -3
  107. package/src/core/phase.js +40 -21
  108. package/src/core/policy-engine.js +113 -6
  109. package/src/core/preflight-consistency.js +31 -5
  110. package/src/core/preflight.js +19 -2
  111. package/src/core/prepared-execution.js +256 -0
  112. package/src/core/progress.js +41 -4
  113. package/src/core/protocol-info.js +56 -0
  114. package/src/core/protocol.js +14 -0
  115. package/src/core/receipt.js +1 -0
  116. package/src/core/reconcile-closure.js +15 -12
  117. package/src/core/reflection.js +305 -0
  118. package/src/core/resumability.js +57 -3
  119. package/src/core/runtime-context.js +19 -1
  120. package/src/core/schema-validation.js +8 -0
  121. package/src/core/strategy-analysis.js +97 -0
  122. package/src/core/task-paths.js +28 -0
  123. package/src/core/task-snapshot.js +53 -0
  124. package/src/core/templates.js +8 -0
  125. package/src/core/trace.js +548 -0
  126. package/src/core/trajectory-evaluation.js +71 -0
  127. package/src/core/trajectory-metrics.js +80 -0
  128. package/src/core/transaction.js +8 -0
  129. package/src/core/verification-execution.js +257 -0
  130. package/src/core/work-state.js +10 -5
  131. package/src/integration.js +8 -0
  132. package/docs/assets/forgeloop-flow.svg +0 -1
  133. package/docs/forgeloop-flow.mmd +0 -51
@@ -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() {
@@ -0,0 +1,548 @@
1
+ import { buildTaskSnapshot } from "./task-snapshot.js";
2
+ import { diagnosisEventsForTask } from "./diagnosis-model.js";
3
+ import {
4
+ assertDiagnosticCaseDetails,
5
+ assertInterventionDetails,
6
+ assertHypothesisDispositionDetails,
7
+ } from "./diagnostic-model.js";
8
+ import { projectFailureSignatures } from "./failure-signature.js";
9
+ import { projectFailureSurfaces } from "./failure-surface.js";
10
+ import { listActions } from "./actions.js";
11
+
12
+ export const EVENT_CATEGORIES = Object.freeze([
13
+ "task",
14
+ "contract",
15
+ "routing",
16
+ "lifecycle",
17
+ "execution",
18
+ "verification",
19
+ "diagnosis",
20
+ "intervention",
21
+ "evidence",
22
+ "review",
23
+ "continuity",
24
+ "recovery",
25
+ "policy",
26
+ "audit",
27
+ "completion",
28
+ "integrity",
29
+ ]);
30
+
31
+ const EVENT_CATEGORY_MAP = Object.freeze({
32
+ TASK_RECEIVED: "task",
33
+ CONTRACT_VALIDATED: "contract",
34
+ DECISION_CRITERION_RECORDED: "contract",
35
+ PLAN_RECORDED: "contract",
36
+ ROUTE_VALIDATED: "routing",
37
+ PREFLIGHT_READY: "lifecycle",
38
+ PREFLIGHT_BLOCKED: "lifecycle",
39
+ EXECUTION_STARTED: "execution",
40
+ VERIFICATION_STARTED: "lifecycle",
41
+ VERIFICATION_RECORDED: "verification",
42
+ CHECK_RECORDED: "verification",
43
+ TERMINAL_RESULT_RECORDED: "verification",
44
+ REVIEW_STARTED: "review",
45
+ DIAGNOSIS_RECORDED: "diagnosis",
46
+ DIAGNOSTIC_CASE_RECORDED: "diagnosis",
47
+ HYPOTHESIS_DISPOSITION_RECORDED: "diagnosis",
48
+ INTERVENTION_RECORDED: "intervention",
49
+ CONTINUITY_RECORDED: "continuity",
50
+ CHECKPOINT_RECONCILED: "continuity",
51
+ TASK_RECOVERY_RECORDED: "recovery",
52
+ TASK_RECOVERY_RESUMED: "recovery",
53
+ OPERATOR_RECOVERY_RECORDED: "recovery",
54
+ LEGACY_RECOVERY_MIGRATION_RECORDED: "recovery",
55
+ GATE_SATISFIED: "policy",
56
+ COMPLETION_VALIDATED: "completion",
57
+ COMPLETION_REJECTED: "completion",
58
+ TRANSACTION_COMMITTED: "integrity",
59
+ ACTION_PROPOSED: "execution",
60
+ ACTION_AUTHORIZED: "execution",
61
+ ACTION_STARTED: "execution",
62
+ ACTION_COMMIT_RECORDED: "execution",
63
+ ACTION_VERIFIED: "execution",
64
+ ACTION_FAILED: "execution",
65
+ ACTION_COMMIT_UNKNOWN: "execution",
66
+ ACTION_RECONCILED: "execution",
67
+ ACTION_CANCELLED: "execution",
68
+ APPROVAL_REQUESTED: "execution",
69
+ APPROVAL_RESOLVED: "execution",
70
+ TRAJECTORY_EVALUATED: "audit",
71
+ });
72
+
73
+ const LIFECYCLE_TRANSITIONS = Object.freeze([
74
+ "CONTRACT_VALIDATED",
75
+ "ROUTE_VALIDATED",
76
+ "PREFLIGHT_READY",
77
+ "EXECUTION_STARTED",
78
+ "VERIFICATION_STARTED",
79
+ "REVIEW_STARTED",
80
+ "COMPLETION_VALIDATED",
81
+ "COMPLETION_REJECTED",
82
+ ]);
83
+
84
+ export function eventCategory(eventType) {
85
+ return EVENT_CATEGORY_MAP[eventType] ?? "integrity";
86
+ }
87
+
88
+ export function timestampQuality(event) {
89
+ if (typeof event?.at === "string" && !Number.isNaN(Date.parse(event.at))) return "authoritative";
90
+ return "unknown";
91
+ }
92
+
93
+ function eventSummary(event) {
94
+ const d = event.details ?? {};
95
+ switch (event.event) {
96
+ case "TASK_RECEIVED":
97
+ return `Task received${d.taskId ? ` (${d.taskId})` : ""}`;
98
+ case "CONTRACT_VALIDATED":
99
+ return "Contract validated";
100
+ case "ROUTE_VALIDATED":
101
+ return `Route selected${Array.isArray(d.selectedGuides) ? `: ${d.selectedGuides.join(", ")}` : ""}`;
102
+ case "PREFLIGHT_READY":
103
+ return "Preflight READY";
104
+ case "PREFLIGHT_BLOCKED":
105
+ return "Preflight BLOCKED";
106
+ case "EXECUTION_STARTED":
107
+ return "Execution started";
108
+ case "VERIFICATION_RECORDED": {
109
+ const id = d.id ?? d.checkId ?? d.requirement ?? "check";
110
+ return `${id} ${String(d.status ?? "recorded").toUpperCase()}${d.exitCode !== undefined ? ` (exit ${d.exitCode})` : ""}`;
111
+ }
112
+ case "TERMINAL_RESULT_RECORDED":
113
+ return `Terminal result ${d.type ?? ""} ${d.status ?? ""}`.trim();
114
+ case "REVIEW_STARTED":
115
+ return `Review started${Number.isInteger(d.verificationCycle) ? ` for cycle ${d.verificationCycle}` : ""}`;
116
+ case "DIAGNOSIS_RECORDED":
117
+ return `Diagnosis recorded (${d.informationGain ?? "gain unknown"})`;
118
+ case "DIAGNOSTIC_CASE_RECORDED":
119
+ return `Diagnostic case recorded (cycle ${d.verificationCycle}, revision ${d.diagnosticRevision})`;
120
+ case "INTERVENTION_RECORDED":
121
+ return `Intervention recorded (${d.intervention?.id ?? "unknown"})`;
122
+ case "HYPOTHESIS_DISPOSITION_RECORDED":
123
+ return `Hypothesis ${d.hypothesisRef ?? "unknown"} ${d.status ?? "dispositioned"}`;
124
+ case "DECISION_CRITERION_RECORDED":
125
+ return "Decision criterion recorded";
126
+ case "GATE_SATISFIED":
127
+ return `Gate satisfied: ${d.gate ?? "unknown"}`;
128
+ case "CHECKPOINT_RECONCILED":
129
+ return "Checkpoint reconciled";
130
+ case "CONTINUITY_RECORDED":
131
+ return "Continuity checkpoint recorded";
132
+ case "TASK_RECOVERY_RECORDED":
133
+ return `Task recovery recorded (${d.classification ?? "unknown"})`;
134
+ case "TASK_RECOVERY_RESUMED":
135
+ return "Task resumed after recovery";
136
+ case "OPERATOR_RECOVERY_RECORDED":
137
+ return "Operator recovery recorded";
138
+ case "LEGACY_RECOVERY_MIGRATION_RECORDED":
139
+ return "Legacy recovery migration recorded";
140
+ case "COMPLETION_VALIDATED":
141
+ return "Completion validated";
142
+ case "COMPLETION_REJECTED":
143
+ return "Completion rejected";
144
+ case "TRANSACTION_COMMITTED":
145
+ return "Transaction committed";
146
+ case "ACTION_PROPOSED":
147
+ return `Action proposed (${d.actionId ?? "unknown"})`;
148
+ case "ACTION_AUTHORIZED":
149
+ return `Action authorized (${d.actionId ?? "unknown"})`;
150
+ case "ACTION_STARTED":
151
+ return `Action started (${d.actionId ?? "unknown"})`;
152
+ case "ACTION_COMMIT_RECORDED":
153
+ return `Action commit recorded (${d.actionId ?? "unknown"})`;
154
+ case "ACTION_VERIFIED":
155
+ return `Action verified (${d.actionId ?? "unknown"})`;
156
+ case "ACTION_FAILED":
157
+ return `Action failed (${d.actionId ?? "unknown"})`;
158
+ case "ACTION_COMMIT_UNKNOWN":
159
+ return `Action commit unknown (${d.actionId ?? "unknown"})`;
160
+ case "ACTION_RECONCILED":
161
+ return `Action reconciled (${d.actionId ?? "unknown"})`;
162
+ case "ACTION_CANCELLED":
163
+ return `Action cancelled (${d.actionId ?? "unknown"})`;
164
+ default:
165
+ return event.event;
166
+ }
167
+ }
168
+
169
+ export function normalizeProtocolEvent(event, context = {}) {
170
+ const phase = typeof context.phase === "string" ? context.phase : null;
171
+ return {
172
+ sequence: event.seq,
173
+ timestamp: event.at ?? null,
174
+ timestampSource: "ledger",
175
+ timestampQuality: timestampQuality(event),
176
+ type: event.event,
177
+ category: eventCategory(event.event),
178
+ phase,
179
+ phaseQuality: context.phaseQuality ?? "unknown",
180
+ source: {
181
+ kind: "ledger",
182
+ artifact: context.artifactPath ?? ".forgeloop/task-state/<task-key>/events.ndjson",
183
+ },
184
+ summary: eventSummary(event),
185
+ data: event.details ?? {},
186
+ references: {
187
+ checkIds: [event.details?.id, event.details?.checkId].filter((v) => typeof v === "string"),
188
+ evidenceRefs: Array.isArray(event.details?.evidenceRefs) ? event.details.evidenceRefs : [],
189
+ hypothesisRefs: Array.isArray(event.details?.hypothesisRefs)
190
+ ? event.details.hypothesisRefs
191
+ : [event.details?.hypothesisRef].filter((v) => typeof v === "string"),
192
+ interventionRefs: [event.details?.intervention?.id].filter((v) => typeof v === "string"),
193
+ relatedSequences: [],
194
+ },
195
+ hash: event.hash,
196
+ };
197
+ }
198
+
199
+ function lifecycleTransitions(events) {
200
+ const transitions = [];
201
+ let lastPhaseEvent = null;
202
+ for (const event of events) {
203
+ if (!LIFECYCLE_TRANSITIONS.includes(event.event)) continue;
204
+ transitions.push({
205
+ sequence: event.seq,
206
+ at: event.at,
207
+ type: event.event,
208
+ details: event.details ?? {},
209
+ previous: lastPhaseEvent?.type ?? null,
210
+ });
211
+ lastPhaseEvent = event;
212
+ }
213
+ return transitions;
214
+ }
215
+
216
+ function projectChecks(snapshot) {
217
+ const state = snapshot.state;
218
+ const attemptsByCheck = new Map();
219
+ const pushAttempt = (key, attempt) => {
220
+ if (!attemptsByCheck.has(key)) attemptsByCheck.set(key, []);
221
+ attemptsByCheck.get(key).push(attempt);
222
+ };
223
+
224
+ // Ledger chronology is the primary source of historical attempts.
225
+ const ledgerIdentityKeys = new Set();
226
+ for (const event of snapshot.events) {
227
+ if (event.event !== "VERIFICATION_RECORDED") continue;
228
+ const d = event.details ?? {};
229
+ const key = d.id ?? d.checkId ?? d.requirement ?? null;
230
+ if (!key) continue;
231
+ ledgerIdentityKeys.add(`${key}@@${d.verificationCycle ?? "*"}`);
232
+ pushAttempt(key, {
233
+ sequence: event.seq,
234
+ at: event.at,
235
+ status: d.status ?? null,
236
+ exitCode: d.exitCode ?? null,
237
+ requirement: d.requirement ?? null,
238
+ verificationCycle: d.verificationCycle ?? null,
239
+ provenance: d.provenance ?? null,
240
+ executionMode: d.provenance === "FORGELOOP_EXECUTED"
241
+ ? "executed"
242
+ : (d.provenance === "ACTOR_REPORTED" || d.provenance === "MANUAL_OBSERVATION" ? "observed" : (d.executionId ? "executed" : "unknown")),
243
+ failureToken: d.failureToken ?? d.details?.failureToken ?? null,
244
+ source: "ledger",
245
+ });
246
+ }
247
+
248
+ // State checks only enrich or fill legacy gaps; never duplicate ledger attempts.
249
+ for (const check of state?.checks ?? []) {
250
+ const key = check.id ?? check.checkId ?? null;
251
+ if (!key) continue;
252
+ const identityKey = `${key}@@${check.details?.verificationCycle ?? "*"}`;
253
+ if (ledgerIdentityKeys.has(identityKey) || ledgerIdentityKeys.has(`${key}@@*`)) continue;
254
+ pushAttempt(key, {
255
+ sequence: null,
256
+ at: check.at ?? check.lastUpdatedAt ?? null,
257
+ status: check.status,
258
+ exitCode: check.exitCode ?? null,
259
+ requirement: check.requirement ?? null,
260
+ verificationCycle: check.details?.verificationCycle ?? null,
261
+ provenance: check.provenance ?? null,
262
+ executionMode: check.provenance === "ACTOR_REPORTED" || check.provenance === "MANUAL_OBSERVATION" ? "observed" : "executed",
263
+ failureToken: check.details?.failureToken ?? null,
264
+ source: "state-fallback",
265
+ });
266
+ }
267
+
268
+ const checks = [];
269
+ const MAX = Number.MAX_SAFE_INTEGER;
270
+ for (const [id, attempts] of [...attemptsByCheck.entries()].sort(([a], [b]) => a.localeCompare(b))) {
271
+ attempts.sort((a, b) =>
272
+ ((a.verificationCycle ?? MAX) - (b.verificationCycle ?? MAX))
273
+ || ((a.sequence ?? MAX) - (b.sequence ?? MAX)));
274
+ checks.push({
275
+ id,
276
+ requirement: attempts.findLast((a) => a.requirement)?.requirement ?? null,
277
+ attemptCount: attempts.length,
278
+ failedAttempts: attempts.filter((a) => a.status === "failed" || a.status === "blocked").length,
279
+ currentResult: attempts.at(-1)?.status ?? null,
280
+ attempts,
281
+ });
282
+ }
283
+ return checks;
284
+ }
285
+
286
+ function legacyDiagnoses(events, taskId) {
287
+ return diagnosisEventsForTask(events, taskId).map((event) => ({
288
+ sequence: event.seq,
289
+ at: event.at,
290
+ sourceModel: "LEGACY_DIAGNOSIS_V1",
291
+ ...event.details,
292
+ }));
293
+ }
294
+
295
+ function structuredDiagnostics(events) {
296
+ const cases = [];
297
+ const interventions = [];
298
+ const dispositions = [];
299
+ const invalidRevisions = [];
300
+ const lastCaseByCycle = new Map();
301
+ for (const event of events) {
302
+ try {
303
+ if (event.event === "DIAGNOSTIC_CASE_RECORDED") {
304
+ assertDiagnosticCaseDetails(event.details);
305
+ const cycle = event.details?.verificationCycle;
306
+ const previous = lastCaseByCycle.get(cycle);
307
+ if (Number.isInteger(event.details?.diagnosticRevision) && event.details.diagnosticRevision > 1 && previous) {
308
+ if (event.details.previousDiagnosticFingerprint !== previous.diagnosticFingerprint) {
309
+ invalidRevisions.push({
310
+ sequence: event.seq,
311
+ verificationCycle: cycle,
312
+ code: "E_DIAGNOSTIC_CASE_INVALID",
313
+ message: "Revision chain broken: previousDiagnosticFingerprint does not match the prior case fingerprint.",
314
+ });
315
+ continue;
316
+ }
317
+ }
318
+ lastCaseByCycle.set(cycle, event.details);
319
+ cases.push({ sequence: event.seq, at: event.at, ...event.details });
320
+ } else if (event.event === "INTERVENTION_RECORDED") {
321
+ assertInterventionDetails(event.details);
322
+ interventions.push({ sequence: event.seq, at: event.at, ...event.details });
323
+ } else if (event.event === "HYPOTHESIS_DISPOSITION_RECORDED") {
324
+ assertHypothesisDispositionDetails(event.details);
325
+ dispositions.push({ sequence: event.seq, at: event.at, ...event.details });
326
+ }
327
+ } catch {
328
+ // Invalid diagnostic details surface through ledger integrity errors;
329
+ // projections must not crash on them.
330
+ }
331
+ }
332
+ return { cases, interventions, dispositions, invalidRevisions };
333
+ }
334
+
335
+ const PHASE_MILESTONES = Object.freeze({
336
+ TASK_RECEIVED: { phase: "RECEIVED", quality: "authoritative" },
337
+ CONTRACT_VALIDATED: { phase: "CONTRACT_READY", quality: "authoritative" },
338
+ ROUTE_VALIDATED: { phase: "ROUTED", quality: "authoritative" },
339
+ DESIGN_GATE_STARTED: { phase: "DESIGNING", quality: "authoritative" },
340
+ PLAN_RECORDED: { phase: "PLANNED", quality: "authoritative" },
341
+ EXECUTION_STARTED: { phase: "EXECUTING", quality: "authoritative" },
342
+ VERIFICATION_STARTED: { phase: "VERIFYING", quality: "authoritative" },
343
+ REVIEW_STARTED: { phase: "REVIEWING", quality: "authoritative" },
344
+ COMPLETION_VALIDATED: { phase: "COMPLETE", quality: "authoritative" },
345
+ });
346
+
347
+ const PHASE_DERIVATIONS = Object.freeze({
348
+ VERIFICATION_RECORDED: (details) => (["failed", "blocked"].includes(details?.status) ? "DIAGNOSING" : null),
349
+ DIAGNOSIS_RECORDED: () => "DIAGNOSING",
350
+ DIAGNOSTIC_CASE_RECORDED: () => "DIAGNOSING",
351
+ HYPOTHESIS_DISPOSITION_RECORDED: () => "CORRECTING",
352
+ INTERVENTION_RECORDED: () => "CORRECTING",
353
+ });
354
+
355
+ function reconstructPhaseChronology(events) {
356
+ let currentPhase = null;
357
+ const phaseBySequence = new Map();
358
+ const qualityBySequence = new Map();
359
+ for (const event of events) {
360
+ const milestone = PHASE_MILESTONES[event.event];
361
+ if (milestone) {
362
+ currentPhase = milestone.phase;
363
+ qualityBySequence.set(event.seq, milestone.quality);
364
+ } else {
365
+ const derive = PHASE_DERIVATIONS[event.event];
366
+ const derived = derive ? derive(event.details ?? {}) : null;
367
+ if (derived) {
368
+ currentPhase = derived;
369
+ qualityBySequence.set(event.seq, "derived");
370
+ } else {
371
+ qualityBySequence.set(event.seq, currentPhase ? "derived" : "unknown");
372
+ }
373
+ }
374
+ phaseBySequence.set(event.seq, currentPhase);
375
+ }
376
+ return { phaseBySequence, qualityBySequence };
377
+ }
378
+
379
+ export function historyQualityFor({ snapshot, normalizedEvents }) {
380
+ const reasons = [];
381
+ const hasLedger = normalizedEvents.length > 0;
382
+ if (!hasLedger) reasons.push("LEDGER_ABSENT");
383
+ if (normalizedEvents.some((event) => event.timestampQuality !== "authoritative")) {
384
+ reasons.push("LEGACY_PHASE_TIMESTAMPS_UNAVAILABLE");
385
+ }
386
+ if (snapshot.state && Number.isInteger(snapshot.state.verificationCycle) && snapshot.state.verificationCycle > 1
387
+ && !snapshot.events.some((event) => event.event === "DIAGNOSIS_RECORDED" || event.event === "DIAGNOSTIC_CASE_RECORDED")) {
388
+ reasons.push("STRUCTURED_DIAGNOSTICS_UNAVAILABLE");
389
+ }
390
+ const level = reasons.length === 0 ? "COMPLETE" : (hasLedger ? "PARTIAL" : "MINIMAL");
391
+ return { level, reasons };
392
+ }
393
+
394
+ export async function buildTaskTrace({ target, packageRoot, taskId = null, eventsPath = null } = {}) {
395
+ const snapshot = await buildTaskSnapshot({ target, packageRoot, taskId, eventsPath });
396
+ const artifactPath = eventsPath ?? ".forgeloop/task-state/<task-key>/events.ndjson";
397
+
398
+ const taskEvents = taskId ? snapshot.events.filter((event) => !event.taskId || event.taskId === taskId) : snapshot.events;
399
+ const { phaseBySequence, qualityBySequence } = reconstructPhaseChronology(taskEvents);
400
+ const normalizedEvents = [];
401
+ for (const event of taskEvents) {
402
+ normalizedEvents.push(normalizeProtocolEvent(event, {
403
+ phase: phaseBySequence.get(event.seq) ?? null,
404
+ artifactPath,
405
+ phaseQuality: qualityBySequence.get(event.seq) ?? "unknown",
406
+ }));
407
+ }
408
+
409
+ const integrity = {
410
+ valid: snapshot.integrity.valid,
411
+ errors: snapshot.integrity.errors,
412
+ };
413
+ const historyQuality = historyQualityFor({ snapshot, normalizedEvents });
414
+ const diagnostics = structuredDiagnostics(taskEvents);
415
+
416
+ const failureSignatures = projectFailureSignatures({ state: snapshot.state, events: taskEvents });
417
+ const failureSurfaces = projectFailureSurfaces({ state: snapshot.state, events: taskEvents });
418
+
419
+ const executions = [];
420
+ const seenExecutions = new Set();
421
+ for (const event of taskEvents) {
422
+ const d = event.details ?? {};
423
+ const executionId = d.executionId ?? d.executionRef ?? null;
424
+ if (!executionId || seenExecutions.has(executionId)) continue;
425
+ seenExecutions.add(executionId);
426
+ executions.push({
427
+ executionId,
428
+ sequence: event.seq,
429
+ at: event.at,
430
+ status: d.status ?? null,
431
+ exitCode: d.exitCode ?? null,
432
+ resolution: d.resolution ?? null,
433
+ });
434
+ }
435
+
436
+ const evidenceSources = new Map();
437
+ for (const event of taskEvents) {
438
+ const refs = [
439
+ ...(Array.isArray(event.details?.evidenceRefs) ? event.details.evidenceRefs : []),
440
+ ...(Array.isArray(event.details?.hypotheses)
441
+ ? event.details.hypotheses.flatMap((hypothesis) => hypothesis.evidenceRefs ?? [])
442
+ : []),
443
+ ];
444
+ for (const ref of refs) {
445
+ if (!ref) continue;
446
+ if (!evidenceSources.has(ref)) evidenceSources.set(ref, { ref, sources: [] });
447
+ if (!evidenceSources.get(ref).sources.includes(event.event)) evidenceSources.get(ref).sources.push(event.event);
448
+ }
449
+ }
450
+ const evidence = [...evidenceSources.values()].sort((a, b) => a.ref.localeCompare(b.ref));
451
+
452
+ const recovery = taskEvents
453
+ .filter((event) => ["TASK_RECOVERY_RECORDED", "TASK_RECOVERY_RESUMED", "OPERATOR_RECOVERY_RECORDED", "LEGACY_RECOVERY_MIGRATION_RECORDED"].includes(event.event))
454
+ .map((event) => ({ sequence: event.seq, at: event.at, type: event.event, details: event.details ?? {} }));
455
+
456
+ const completionEvents = taskEvents.filter((event) => ["COMPLETION_VALIDATED", "COMPLETION_REJECTED"].includes(event.event));
457
+ const completion = {
458
+ validatedAt: completionEvents.findLast((event) => event.event === "COMPLETION_VALIDATED")?.at ?? null,
459
+ rejectedAt: completionEvents.findLast((event) => event.event === "COMPLETION_REJECTED")?.at ?? null,
460
+ attempts: completionEvents.map((event) => ({ sequence: event.seq, at: event.at, type: event.event })),
461
+ };
462
+
463
+ const actions = snapshot.taskId
464
+ ? await listActions(target, { packageRoot, taskId: snapshot.taskId })
465
+ : [];
466
+ // Artifacts are the current-state materialization; chronology comes from
467
+ // the canonical ledger replay projection, never from artifacts alone.
468
+ const { projectActionLedger } = await import("./action-ledger-projection.js");
469
+ const projections = [];
470
+ for (const action of actions) {
471
+ projections.push(await projectActionLedger({
472
+ target,
473
+ packageRoot,
474
+ taskId: snapshot.taskId,
475
+ actionId: action.actionId,
476
+ artifact: action,
477
+ }));
478
+ }
479
+ const actionEvents = taskEvents.filter((event) => event.event.startsWith("ACTION_") || event.event.startsWith("APPROVAL_"));
480
+ const byState = Object.fromEntries([...new Set(actions.map((action) => action.state))].sort()
481
+ .map((state) => [state, actions.filter((action) => action.state === state).length]));
482
+ const byCapability = Object.fromEntries([...new Set(actions.map((action) => action.capability))].sort()
483
+ .map((capability) => [capability, actions.filter((action) => action.capability === capability).length]));
484
+ const idempotencyAttempts = actions.map((action) => action.idempotencyKey).filter(Boolean);
485
+ const repeatedIdempotencyAttempts = idempotencyAttempts.length - new Set(idempotencyAttempts).size;
486
+ const actionProjection = {
487
+ total: actions.length,
488
+ byState,
489
+ byCapability,
490
+ required: actions.filter((action) => action.requiredForCompletion).length,
491
+ ambiguous: actions.filter((action) => action.state === "COMMIT_UNKNOWN").length,
492
+ failed: actions.filter((action) => action.state === "FAILED").length,
493
+ verified: actions.filter((action) => action.state === "VERIFIED").length,
494
+ trustedSatisfied: projections.filter(
495
+ (projection) => projection.valid && projection.state === "VERIFIED"
496
+ && projection.authorization.valid && projection.verification.valid,
497
+ ).length,
498
+ untrustedRequired: projections.filter((projection) =>
499
+ !projection.valid
500
+ || (projection.state === "VERIFIED" && !(projection.authorization.valid && projection.verification.valid))
501
+ ).length,
502
+ repeatedIdempotencyAttempts,
503
+ reconciliationCount: taskEvents.filter((event) => event.event === "ACTION_RECONCILED").length,
504
+ eventCount: actionEvents.length,
505
+ };
506
+
507
+ return {
508
+ schemaVersion: 1,
509
+ protocolVersion: 1,
510
+ command: "trace",
511
+ task: {
512
+ id: snapshot.taskId,
513
+ phase: snapshot.state?.phase ?? null,
514
+ status: snapshot.state?.status ?? null,
515
+ revision: snapshot.state?.revision ?? null,
516
+ verificationCycle: snapshot.state?.verificationCycle ?? null,
517
+ present: Boolean(snapshot.state),
518
+ },
519
+ snapshot: {
520
+ consistent: snapshot.consistent,
521
+ stateRevision: snapshot.anchors.stateRevision,
522
+ ledgerTailSequence: snapshot.anchors.sequence,
523
+ capturedAt: snapshot.capturedAt,
524
+ },
525
+ historyQuality,
526
+ integrity,
527
+ artifacts: {},
528
+ events: normalizedEvents,
529
+ transitions: lifecycleTransitions(taskEvents),
530
+ executions,
531
+ checks: projectChecks(snapshot),
532
+ evidence,
533
+ diagnostics: {
534
+ legacyDiagnoses: legacyDiagnoses(taskEvents, snapshot.taskId),
535
+ ...diagnostics,
536
+ },
537
+ failureSignatures,
538
+ failureSurfaces,
539
+ continuity: taskEvents
540
+ .filter((event) => ["CONTINUITY_RECORDED", "CHECKPOINT_RECONCILED"].includes(event.event))
541
+ .map((event) => ({ sequence: event.seq, at: event.at, type: event.event })),
542
+ recovery,
543
+ policy: {},
544
+ audit: {},
545
+ completion,
546
+ actions: actionProjection,
547
+ };
548
+ }