loopforge 2.11.0 → 3.0.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 (166) hide show
  1. package/README.md +145 -128
  2. package/dist/canonical-event-writer.d.ts +20 -0
  3. package/dist/canonical-event-writer.d.ts.map +1 -0
  4. package/dist/canonical-event-writer.js +131 -0
  5. package/dist/canonical-event-writer.js.map +1 -0
  6. package/dist/canonical-state.d.ts +20 -16
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +170 -61
  9. package/dist/canonical-state.js.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +11 -10
  12. package/dist/cli.js.map +1 -1
  13. package/dist/cognitive-governance.d.ts +48 -0
  14. package/dist/cognitive-governance.d.ts.map +1 -0
  15. package/dist/cognitive-governance.js +226 -0
  16. package/dist/cognitive-governance.js.map +1 -0
  17. package/dist/compiler-subgoals.d.ts +7 -0
  18. package/dist/compiler-subgoals.d.ts.map +1 -0
  19. package/dist/compiler-subgoals.js +93 -0
  20. package/dist/compiler-subgoals.js.map +1 -0
  21. package/dist/derive-id.d.ts +12 -0
  22. package/dist/derive-id.d.ts.map +1 -0
  23. package/dist/derive-id.js +31 -0
  24. package/dist/derive-id.js.map +1 -0
  25. package/dist/evidence-provider.d.ts +23 -21
  26. package/dist/evidence-provider.d.ts.map +1 -1
  27. package/dist/evidence-provider.js +34 -74
  28. package/dist/evidence-provider.js.map +1 -1
  29. package/dist/generate-schema.js +90 -2
  30. package/dist/generate-schema.js.map +1 -1
  31. package/dist/index.d.ts +22 -19
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +15 -16
  34. package/dist/index.js.map +1 -1
  35. package/dist/loop-compiler.d.ts +1 -1
  36. package/dist/loop-compiler.d.ts.map +1 -1
  37. package/dist/loop-compiler.js +250 -337
  38. package/dist/loop-compiler.js.map +1 -1
  39. package/dist/loop-extras-parser.d.ts +2 -2
  40. package/dist/loop-extras-parser.d.ts.map +1 -1
  41. package/dist/loop-extras-parser.js +2 -2
  42. package/dist/loop-extras-parser.js.map +1 -1
  43. package/dist/loop-store.d.ts +20 -58
  44. package/dist/loop-store.d.ts.map +1 -1
  45. package/dist/loop-store.js +232 -309
  46. package/dist/loop-store.js.map +1 -1
  47. package/dist/mcp/server.d.ts +3 -3
  48. package/dist/mcp/server.d.ts.map +1 -1
  49. package/dist/mcp/server.js +83 -13
  50. package/dist/mcp/server.js.map +1 -1
  51. package/dist/mcp/session-projection.d.ts +5 -0
  52. package/dist/mcp/session-projection.d.ts.map +1 -0
  53. package/dist/mcp/session-projection.js +18 -0
  54. package/dist/mcp/session-projection.js.map +1 -0
  55. package/dist/mcp/session-queue.d.ts +3 -0
  56. package/dist/mcp/session-queue.d.ts.map +1 -0
  57. package/dist/mcp/session-queue.js +18 -0
  58. package/dist/mcp/session-queue.js.map +1 -0
  59. package/dist/mcp/session-types.d.ts +72 -0
  60. package/dist/mcp/session-types.d.ts.map +1 -0
  61. package/dist/mcp/session-types.js +2 -0
  62. package/dist/mcp/session-types.js.map +1 -0
  63. package/dist/mcp/session.d.ts +49 -93
  64. package/dist/mcp/session.d.ts.map +1 -1
  65. package/dist/mcp/session.js +544 -245
  66. package/dist/mcp/session.js.map +1 -1
  67. package/dist/mcp/tools.d.ts +26 -1
  68. package/dist/mcp/tools.d.ts.map +1 -1
  69. package/dist/mcp/tools.js +399 -274
  70. package/dist/mcp/tools.js.map +1 -1
  71. package/dist/policy-metrics.d.ts +17 -1
  72. package/dist/policy-metrics.d.ts.map +1 -1
  73. package/dist/policy-metrics.js +164 -1
  74. package/dist/policy-metrics.js.map +1 -1
  75. package/dist/policy.d.ts +34 -18
  76. package/dist/policy.d.ts.map +1 -1
  77. package/dist/policy.js +113 -18
  78. package/dist/policy.js.map +1 -1
  79. package/dist/prompt-assembler.d.ts +5 -3
  80. package/dist/prompt-assembler.d.ts.map +1 -1
  81. package/dist/prompt-assembler.js +126 -77
  82. package/dist/prompt-assembler.js.map +1 -1
  83. package/dist/prompt-policy.d.ts +8 -1
  84. package/dist/prompt-policy.d.ts.map +1 -1
  85. package/dist/prompt-policy.js +26 -8
  86. package/dist/prompt-policy.js.map +1 -1
  87. package/dist/protocol.d.ts +232 -192
  88. package/dist/protocol.d.ts.map +1 -1
  89. package/dist/protocol.js +15 -100
  90. package/dist/protocol.js.map +1 -1
  91. package/dist/replay.d.ts +11 -3
  92. package/dist/replay.d.ts.map +1 -1
  93. package/dist/replay.js +51 -14
  94. package/dist/replay.js.map +1 -1
  95. package/dist/round-commit-store.d.ts +12 -0
  96. package/dist/round-commit-store.d.ts.map +1 -0
  97. package/dist/round-commit-store.js +13 -0
  98. package/dist/round-commit-store.js.map +1 -0
  99. package/dist/round-coordinator.d.ts +25 -18
  100. package/dist/round-coordinator.d.ts.map +1 -1
  101. package/dist/round-coordinator.js +83 -71
  102. package/dist/round-coordinator.js.map +1 -1
  103. package/dist/round-driver.d.ts +14 -10
  104. package/dist/round-driver.d.ts.map +1 -1
  105. package/dist/round-driver.js +9 -13
  106. package/dist/round-driver.js.map +1 -1
  107. package/dist/round-gate.d.ts +31 -0
  108. package/dist/round-gate.d.ts.map +1 -0
  109. package/dist/round-gate.js +259 -0
  110. package/dist/round-gate.js.map +1 -0
  111. package/dist/round-recovery.d.ts +17 -0
  112. package/dist/round-recovery.d.ts.map +1 -0
  113. package/dist/round-recovery.js +117 -0
  114. package/dist/round-recovery.js.map +1 -0
  115. package/dist/round-report.d.ts +16 -0
  116. package/dist/round-report.d.ts.map +1 -0
  117. package/dist/round-report.js +401 -0
  118. package/dist/round-report.js.map +1 -0
  119. package/dist/round-transaction.d.ts +19 -12
  120. package/dist/round-transaction.d.ts.map +1 -1
  121. package/dist/round-transaction.js +102 -88
  122. package/dist/round-transaction.js.map +1 -1
  123. package/dist/runtime-services.d.ts +59 -0
  124. package/dist/runtime-services.d.ts.map +1 -0
  125. package/dist/runtime-services.js +460 -0
  126. package/dist/runtime-services.js.map +1 -0
  127. package/dist/storage.d.ts +3 -9
  128. package/dist/storage.d.ts.map +1 -1
  129. package/dist/storage.js +6 -17
  130. package/dist/storage.js.map +1 -1
  131. package/dist/token-utils.d.ts +9 -8
  132. package/dist/token-utils.d.ts.map +1 -1
  133. package/dist/token-utils.js +50 -10
  134. package/dist/token-utils.js.map +1 -1
  135. package/dist/vault-format.d.ts +4 -0
  136. package/dist/vault-format.d.ts.map +1 -0
  137. package/dist/vault-format.js +4 -0
  138. package/dist/vault-format.js.map +1 -0
  139. package/dist/vault-types.d.ts +24 -0
  140. package/dist/vault-types.d.ts.map +1 -0
  141. package/dist/vault-types.js +2 -0
  142. package/dist/vault-types.js.map +1 -0
  143. package/loop_policy.json +10 -12
  144. package/loopforge-policy.json +340 -0
  145. package/package.json +12 -6
  146. package/skills/perception/SKILL.md +199 -164
  147. package/dist/backends/interface.d.ts +0 -39
  148. package/dist/backends/interface.d.ts.map +0 -1
  149. package/dist/backends/interface.js +0 -6
  150. package/dist/backends/interface.js.map +0 -1
  151. package/dist/enforcement-gate.d.ts +0 -71
  152. package/dist/enforcement-gate.d.ts.map +0 -1
  153. package/dist/enforcement-gate.js +0 -744
  154. package/dist/enforcement-gate.js.map +0 -1
  155. package/dist/engine.d.ts +0 -59
  156. package/dist/engine.d.ts.map +0 -1
  157. package/dist/engine.js +0 -596
  158. package/dist/engine.js.map +0 -1
  159. package/dist/self-eval.d.ts +0 -23
  160. package/dist/self-eval.d.ts.map +0 -1
  161. package/dist/self-eval.js +0 -181
  162. package/dist/self-eval.js.map +0 -1
  163. package/dist/verification-gate.d.ts +0 -56
  164. package/dist/verification-gate.d.ts.map +0 -1
  165. package/dist/verification-gate.js +0 -563
  166. package/dist/verification-gate.js.map +0 -1
package/dist/mcp/tools.js CHANGED
@@ -1,14 +1,106 @@
1
1
  /** LoopForge MCP — Tool definitions and handlers.
2
2
  *
3
- * 9 tools: start, next, status, stop, pause, list, replay, resume, health.
3
+ * Public tools for round execution, recovery, gates, and audit.
4
4
  * Each handler receives SessionManager + parsed input, returns the output object.
5
5
  */
6
- import { buildSelfEvaluation } from "../engine.js";
7
- import { getPolicyMetrics } from "../policy-metrics.js";
8
6
  import { isRecord } from "../token-utils.js";
7
+ import { parseRoundReport } from "../round-report.js";
8
+ import { getPolicy } from "../policy.js";
9
9
  // ═══════════════════════════════════════════════════════════════════════════
10
10
  // Tool schemas (MCP JSON Schema format)
11
11
  // ═══════════════════════════════════════════════════════════════════════════
12
+ const GATE_ACTION_SCHEMA = {
13
+ type: "object",
14
+ properties: {
15
+ description: { type: "string" },
16
+ scope: { type: "array", items: { type: "string" } },
17
+ effects: { type: "array", items: { type: "string", enum: ["workspace_write", "production", "credentials", "data_migration", "public_api", "publish", "payment", "external_communication", "network"] } },
18
+ reversibility: { type: "string", enum: ["reversible", "recoverable", "irreversible", "unknown"] },
19
+ authorization: { type: "string", enum: ["agent_allowed", "user_required", "unknown"] },
20
+ },
21
+ required: ["description", "scope", "effects", "reversibility", "authorization"],
22
+ };
23
+ const ROUND_REPORT_SCHEMA = {
24
+ type: "object",
25
+ required: ["format", "outcome", "continuation", "summary", "violations"],
26
+ properties: {
27
+ format: { type: "string", const: "loopforge.round-report" },
28
+ outcome: { type: "string", enum: ["success", "partial", "failed", "blocked"] },
29
+ continuation: { type: "string", enum: ["continue", "stop"] },
30
+ summary: { type: "string" },
31
+ violations: { type: "array", items: { type: "string" } },
32
+ agentIntent: { type: "string" },
33
+ driftClarification: {
34
+ type: "object",
35
+ properties: {
36
+ explanation: { type: "string", minLength: 1, maxLength: 2000 },
37
+ anchors: { type: "array", maxItems: 8, items: { type: "string", maxLength: 240 } },
38
+ },
39
+ required: ["explanation", "anchors"],
40
+ },
41
+ evidence: {
42
+ type: "object",
43
+ properties: {
44
+ files: { type: "array", items: { type: "string" } },
45
+ checks: { type: "array", items: { type: "object", properties: {
46
+ name: { type: "string" }, status: { type: "string", enum: ["passed", "failed", "not_run"] },
47
+ summary: { type: "string" }, counts: { type: "object", properties: {
48
+ passed: { type: "integer", minimum: 0 }, failed: { type: "integer", minimum: 0 }, skipped: { type: "integer", minimum: 0 },
49
+ } },
50
+ }, required: ["name", "status"] } },
51
+ claims: { type: "array", items: { type: "object", properties: {
52
+ id: { type: "string", pattern: "^claim-[a-f0-9]{12}$" }, targetId: { type: "string" }, evidenceRefs: { type: "array", items: { type: "string" } },
53
+ }, required: ["id", "targetId", "evidenceRefs"] } },
54
+ provenance: {
55
+ type: "object",
56
+ description: "Runtime-owned evidence provenance keyed by evidence reference.",
57
+ additionalProperties: {
58
+ type: "object",
59
+ properties: {
60
+ source: { type: "string", enum: ["agent", "command", "provider", "verification"] },
61
+ observedAt: { type: "string" },
62
+ status: { type: "string", enum: ["verified", "claimed", "contradicted", "unavailable"] },
63
+ detail: { type: "string" },
64
+ },
65
+ required: ["source", "observedAt", "status"],
66
+ },
67
+ },
68
+ noChangeReason: { type: "string" },
69
+ },
70
+ },
71
+ blocker: { type: "object", properties: {
72
+ kind: { type: "string", enum: ["dependency", "external", "needs_human_input", "agent"] },
73
+ reason: { type: "string" }, references: { type: "array", items: { type: "string" } },
74
+ action: GATE_ACTION_SCHEMA,
75
+ }, required: ["kind", "reason"] },
76
+ discoveries: { type: "object", properties: {
77
+ wrongAssumptions: { type: "array", items: { type: "string" } },
78
+ emergedWork: { type: "array", items: { type: "string" } },
79
+ facts: { type: "array", items: { type: "string" } },
80
+ newConstraints: { type: "array", items: { type: "string" } },
81
+ } },
82
+ subGoalEvents: { type: "object", properties: {
83
+ completed: { type: "array", items: { type: "string" } },
84
+ blocked: { type: "array", items: { type: "string" } },
85
+ canceled: { type: "array", items: { type: "string" } },
86
+ } },
87
+ constraintEvents: { type: "object", properties: {
88
+ retired: { type: "array", items: { type: "string" } },
89
+ } },
90
+ criterionRevisions: { type: "array", items: { type: "object", properties: {
91
+ old: { type: "string" }, new: { type: "string" },
92
+ }, required: ["old", "new"] } },
93
+ objectiveRefinement: { type: "string" },
94
+ phaseBoundaryProposal: { type: "object", properties: {
95
+ label: { type: "string" }, reason: { type: "string" },
96
+ }, required: ["label", "reason"] },
97
+ contextRequest: { type: "object", properties: {
98
+ emphasize: { type: "array", items: { type: "string" } },
99
+ expand: { type: "array", items: { type: "string" } },
100
+ confusion_points: { type: "array", items: { type: "string" } },
101
+ } },
102
+ },
103
+ };
12
104
  const TOOL_BASE_SCHEMAS = [
13
105
  {
14
106
  name: "loopforge_start",
@@ -26,7 +118,9 @@ const TOOL_BASE_SCHEMAS = [
26
118
  },
27
119
  maxRounds: {
28
120
  type: "number",
29
- description: "Maximum rounds before auto-stop. Default: 20 from policy.",
121
+ minimum: 1,
122
+ multipleOf: 1,
123
+ description: "Maximum rounds before auto-stop. Default: 100 from policy.",
30
124
  },
31
125
  domain: {
32
126
  type: "string",
@@ -41,258 +135,122 @@ const TOOL_BASE_SCHEMAS = [
41
135
  items: { type: "string" },
42
136
  description: "Hard constraints to enforce across all rounds.",
43
137
  },
138
+ successCriteria: {
139
+ type: "array",
140
+ items: { type: "string" },
141
+ description: "Explicit success criteria to verify across the loop.",
142
+ },
44
143
  },
45
144
  required: ["task"],
46
145
  },
47
146
  },
48
147
  {
49
148
  name: "loopforge_next",
50
- description: "Submit the output from the current round and advance to the next. Returns the next-round prompt, or null with a stopReason when the loop ends. The evaluation parameter provides structured self-assessment — prefer this over embedding a ---loopforge-eval block in the output text.",
149
+ description: "Submit the canonical round report for the current round and advance to the next. Returns the next-round prompt, or null with a stopReason when the loop ends.",
51
150
  inputSchema: {
52
151
  type: "object",
152
+ additionalProperties: false,
53
153
  properties: {
54
154
  sessionId: {
55
155
  type: "string",
56
156
  description: "Session ID returned by loopforge_start.",
57
157
  },
58
- output: {
59
- type: "string",
60
- description: "Optional. The agent's full output from executing the current round's prompt. May be omitted if evaluation parameter is provided.",
61
- },
62
- evaluation: {
63
- type: "object",
64
- description: "Structured self-evaluation for this round. Required for the loop to continue. Preferred over embedding ---loopforge-eval blocks in output text.",
65
- required: ["success", "output_summary", "should_continue", "constraint_violations"],
66
- properties: {
67
- success: {
68
- type: "boolean",
69
- description: "true ONLY if all hard constraints met AND the task goal achieved.",
70
- },
71
- output_summary: {
72
- type: "string",
73
- description: "Specific, actionable summary of what was DONE this round — not what was attempted.",
74
- },
75
- should_continue: {
76
- type: "boolean",
77
- description: "false ONLY when the ENTIRE task is complete. Partial progress = true.",
78
- },
79
- constraint_violations: {
80
- type: "array",
81
- items: { type: "string" },
82
- description: "Constraints the agent actually violated this round. Be honest.",
83
- },
84
- discovered_constraints: {
85
- type: "array",
86
- items: { type: "string" },
87
- description: "Optional. New constraints discovered this round.",
88
- },
89
- objective_refinement: {
90
- type: "string",
91
- description: "Optional. If this round deepened understanding of the task objective.",
92
- },
93
- emerged_subtasks: {
94
- type: "array",
95
- items: { type: "string" },
96
- description: "Optional. Sub-problems that surfaced during execution.",
97
- },
98
- execution_evidence: {
99
- type: "object",
100
- description: "Optional. Structured record of what actually happened this round.",
101
- properties: {
102
- files_changed: {
103
- type: "array",
104
- items: { type: "string" },
105
- description: "Files modified this round.",
106
- },
107
- test_results: {
108
- type: "object",
109
- description: "Test results from this round.",
110
- properties: {
111
- passed: { type: "integer", minimum: 0, description: "Number of passing tests." },
112
- failed: { type: "integer", minimum: 0, description: "Number of failing tests." },
113
- skipped: { type: "integer", minimum: 0, description: "Number of skipped tests." },
114
- },
115
- },
116
- success_criteria_met: {
117
- type: "array",
118
- items: { type: "string" },
119
- description: "Success criteria satisfied this round.",
120
- },
121
- success_criteria_remaining: {
122
- type: "array",
123
- items: { type: "string" },
124
- description: "Success criteria still outstanding.",
125
- },
126
- progress_estimate: {
127
- type: "number",
128
- minimum: 0,
129
- maximum: 1,
130
- description: "Estimated progress toward task completion (0.0–1.0).",
131
- },
132
- },
133
- },
134
- retracted_constraints: {
135
- type: "array",
136
- items: { type: "string" },
137
- description: "Optional. Constraints the agent now believes are wrong.",
138
- },
139
- revised_success_criteria: {
140
- type: "array",
141
- description: "Optional. Success criteria that need reformulation.",
142
- items: {
143
- type: "object",
144
- properties: {
145
- old: { type: "string", description: "Original criterion." },
146
- new: { type: "string", description: "Revised criterion." },
147
- },
148
- },
149
- },
150
- wrong_assumptions: {
151
- type: "array",
152
- items: { type: "string" },
153
- description: "Optional. Assumptions from earlier rounds that were incorrect.",
154
- },
155
- worker_results: {
156
- type: "array",
157
- description: "Optional. Results of sub-agent / Worker delegations this round.",
158
- items: {
159
- type: "object",
160
- properties: {
161
- agentId: { type: "string" },
162
- subAgentType: { type: "string" },
163
- subTask: { type: "string" },
164
- resultSummary: { type: "string" },
165
- success: { type: "boolean" },
166
- discoveredConstraints: {
167
- type: "array",
168
- items: { type: "string" },
169
- },
170
- },
171
- required: ["agentId", "subAgentType", "subTask", "resultSummary", "success"],
172
- },
173
- },
174
- compression_checkpoint: {
175
- type: "boolean",
176
- description: "Optional. Set true at subtask boundaries to trigger a full-state checkpoint (L2 prompt) that survives rolling-window eviction. The checkpoint label is carried into future prompts.",
177
- },
178
- checkpoint_label: {
179
- type: "string",
180
- description: "Optional. Human-readable label for this checkpoint (e.g. 'core-functions-complete'). Only meaningful when compression_checkpoint is true.",
181
- },
182
- next_action: {
183
- type: "string",
184
- description: "Optional. What the agent plans to do in the next round. Helps LoopForge detect task drift early by comparing declared intent with actual work.",
185
- },
186
- completed_subtasks: {
187
- type: "array",
188
- items: { type: "string" },
189
- description: "Optional. Sub-tasks completed this round. Matched to previously emerged sub-tasks by description similarity.",
190
- },
191
- blocked_subtasks: {
192
- type: "array",
193
- items: { type: "string" },
194
- description: "Optional. Sub-tasks that are now blocked. Describe what is blocking and why.",
195
- },
196
- canceled_subtasks: {
197
- type: "array",
198
- items: { type: "string" },
199
- description: "Optional. Sub-tasks no longer needed. Removes them from the active sub-goal list.",
200
- },
201
- stop_reason: {
202
- type: "string",
203
- enum: ["gave_up", "blocked", "needs_human_input"],
204
- description: "Optional. Why the agent is stopping. Only meaningful when should_continue=false and success=false. 'blocked' means work cannot proceed under current constraints.",
205
- },
206
- prompt_requests: {
207
- type: "object",
208
- description: "Optional. Information needs for the next round's prompt. Use when you know what context you'll need to succeed in the next round.",
209
- properties: {
210
- emphasize: {
211
- type: "array",
212
- items: { type: "string" },
213
- description: "Constraints, discoveries, or decisions that need emphasis in the next prompt. Matched by meaning — not exact text. Matched items appear in a 'Critical Context' section. Max 5 entries. Pure reordering — no token overhead.",
214
- },
215
- expand: {
216
- type: "array",
217
- items: {
218
- type: "string",
219
- enum: ["milestones", "sub_goals", "constraint_lifecycle", "agent_trust", "progress", "loop_synthesis"],
220
- },
221
- description: "Sections to expand to full detail in the next prompt. Useful when you need full context on a specific area (e.g. full sub-goal dashboard when replanning). L1 max 1 section. L2 already expanded — redundant. L0 ignored.",
222
- },
223
- confusion_points: {
224
- type: "array",
225
- items: { type: "string" },
226
- description: "Things you're confused about. Rendered at the TOP of the next prompt before the objective. Max 3 entries, each ≤ 200 chars recommended. Be specific — 'I don't understand why milestone 3 is marked complete when phase 2 appears unfinished.'",
227
- },
228
- },
229
- },
230
- },
158
+ report: {
159
+ ...ROUND_REPORT_SCHEMA,
160
+ description: "Canonical round report. This is the only execution report accepted by loopforge_next.",
231
161
  },
232
162
  },
233
- required: ["sessionId", "evaluation"],
163
+ required: ["sessionId", "report"],
234
164
  },
235
165
  },
236
166
  {
237
167
  name: "loopforge_status",
238
- description: "Get the current status of a loop session, including round identity and success trajectory.",
168
+ description: "Read session, loop health, or all persisted loops through one canonical status view.",
239
169
  inputSchema: {
240
170
  type: "object",
241
171
  properties: {
172
+ scope: { type: "string", enum: ["session", "loop", "all"] },
242
173
  sessionId: {
243
174
  type: "string",
244
175
  description: "Session ID returned by loopforge_start.",
245
176
  },
177
+ loopId: { type: "string" },
178
+ detail: { type: "string", enum: ["summary", "full"] },
246
179
  },
247
- required: ["sessionId"],
180
+ required: ["scope"],
248
181
  },
249
182
  },
250
183
  {
251
- name: "loopforge_stop",
252
- description: "Manually stop a loop session and return its final trajectory.",
184
+ name: "loopforge_retry",
185
+ description: "Submit a corrected round report for the current rejected logical round. The round ID and pre-round evidence baseline remain unchanged.",
253
186
  inputSchema: {
254
187
  type: "object",
255
188
  properties: {
256
189
  sessionId: {
257
190
  type: "string",
258
- description: "Session ID to stop.",
191
+ description: "Session ID returned by loopforge_start.",
192
+ },
193
+ report: {
194
+ ...ROUND_REPORT_SCHEMA,
195
+ description: "Corrected canonical report for the rejected attempt.",
259
196
  },
260
197
  },
261
- required: ["sessionId"],
198
+ required: ["sessionId", "report"],
262
199
  },
263
200
  },
264
201
  {
265
- name: "loopforge_pause",
266
- description: "Pause a running loop session. The loop suspends at the next round boundary and its state is persisted to vault. Paused loops can be resumed with loopforge_resume. Use this to interrupt a long-running loop without losing progress.",
202
+ name: "loopforge_backtrack",
203
+ description: "Move an active session to the round after its nearest clean restore point. Returns restore instructions but never changes workspace files.",
267
204
  inputSchema: {
268
205
  type: "object",
269
206
  properties: {
270
207
  sessionId: {
271
208
  type: "string",
272
- description: "Session ID to pause.",
209
+ description: "Session ID returned by loopforge_start.",
273
210
  },
274
211
  },
275
212
  required: ["sessionId"],
276
213
  },
277
214
  },
278
215
  {
279
- name: "loopforge_list",
280
- description: "List all active loop sessions managed by this MCP server.",
216
+ name: "loopforge_history",
217
+ description: "Read one persisted round or a deterministic event timeline.",
281
218
  inputSchema: {
282
219
  type: "object",
283
- properties: {},
284
- required: [],
220
+ properties: {
221
+ loopId: { type: "string" },
222
+ view: { type: "string", enum: ["round", "timeline"] },
223
+ round: { type: "integer", minimum: 1 },
224
+ fromSequence: { type: "integer", minimum: 1 },
225
+ toSequence: { type: "integer", minimum: 1 },
226
+ limit: { type: "integer", minimum: 1, maximum: 200 },
227
+ },
228
+ required: ["loopId", "view"],
285
229
  },
286
230
  },
287
231
  {
288
- name: "loopforge_replay",
289
- description: "Replay a completed or running loop session as an auditable round timeline.",
232
+ name: "loopforge_stop",
233
+ description: "Manually stop a loop session and return its final trajectory.",
290
234
  inputSchema: {
291
235
  type: "object",
292
236
  properties: {
293
237
  sessionId: {
294
238
  type: "string",
295
- description: "Session ID to replay.",
239
+ description: "Session ID to stop.",
240
+ },
241
+ },
242
+ required: ["sessionId"],
243
+ },
244
+ },
245
+ {
246
+ name: "loopforge_pause",
247
+ description: "Pause a running loop session. The loop suspends at the next round boundary and its state is persisted to vault. Paused loops can be resumed with loopforge_resume. Use this to interrupt a long-running loop without losing progress.",
248
+ inputSchema: {
249
+ type: "object",
250
+ properties: {
251
+ sessionId: {
252
+ type: "string",
253
+ description: "Session ID to pause.",
296
254
  },
297
255
  },
298
256
  required: ["sessionId"],
@@ -313,16 +271,37 @@ const TOOL_BASE_SCHEMAS = [
313
271
  },
314
272
  },
315
273
  {
316
- name: "loopforge_health",
317
- description: "Check the health of a loop: goal alignment, constraint integrity, drift detection, strategy stability, and task continuity. Works for both active in-memory sessions and vault-persisted loops.",
274
+ name: "loopforge_gate_check",
275
+ description: "Classify whether an action is an agent-resolvable gate or requires explicit user authority. Read-only.",
318
276
  inputSchema: {
319
277
  type: "object",
320
278
  properties: {
321
- loopId: {
322
- type: "string",
323
- description: "Loop ID to check health for.",
324
- },
279
+ action: GATE_ACTION_SCHEMA,
280
+ },
281
+ required: ["action"],
282
+ },
283
+ },
284
+ {
285
+ name: "loopforge_gate_resolve",
286
+ description: "Record an explicit user decision for a previously classified gate.",
287
+ inputSchema: {
288
+ type: "object",
289
+ properties: {
290
+ sessionId: { type: "string" },
291
+ gateId: { type: "string" },
292
+ action: GATE_ACTION_SCHEMA,
293
+ approved: { type: "boolean" },
294
+ note: { type: "string" },
325
295
  },
296
+ required: ["sessionId", "gateId", "action", "approved", "note"],
297
+ },
298
+ },
299
+ {
300
+ name: "loopforge_audit",
301
+ description: "Run a read-only final audit over accepted vault events, evidence claims, checks, and gate decisions.",
302
+ inputSchema: {
303
+ type: "object",
304
+ properties: { loopId: { type: "string" } },
326
305
  required: ["loopId"],
327
306
  },
328
307
  },
@@ -335,12 +314,18 @@ const ADVANCE_OUTPUT_SCHEMA = {
335
314
  round: { type: "number" },
336
315
  roundId: { type: ["string", "null"] },
337
316
  prompt: { type: ["string", "null"] },
317
+ decision: { enum: ["accepted", "rejected", "backtracked", "stopped", "terminated", "errored"] },
318
+ attempt: { type: "number" },
338
319
  stopReason: { type: "string" },
339
320
  stopDetail: { type: "string", description: "Human-readable context for why the loop stopped. Provides facts without prescribing a specific agent action." },
340
321
  level: { type: "string" },
341
322
  roundSuccess: { type: "boolean" },
342
323
  enforcementAction: { enum: ["accept", "reject", "terminate", "backtrack"] },
343
324
  enforcementReason: { type: "string" },
325
+ projection: { type: "object", additionalProperties: true },
326
+ evidenceSummary: { type: "object", additionalProperties: true },
327
+ stateHash: { type: "string" },
328
+ promptHash: { type: "string" },
344
329
  warnings: { type: "array", items: { type: "string" } },
345
330
  },
346
331
  additionalProperties: true,
@@ -348,7 +333,10 @@ const ADVANCE_OUTPUT_SCHEMA = {
348
333
  const TOOL_OUTPUT_SCHEMAS = {
349
334
  loopforge_start: ADVANCE_OUTPUT_SCHEMA,
350
335
  loopforge_next: ADVANCE_OUTPUT_SCHEMA,
336
+ loopforge_retry: ADVANCE_OUTPUT_SCHEMA,
337
+ loopforge_backtrack: ADVANCE_OUTPUT_SCHEMA,
351
338
  loopforge_resume: ADVANCE_OUTPUT_SCHEMA,
339
+ loopforge_history: { type: "object", additionalProperties: true },
352
340
  loopforge_status: {
353
341
  type: "object",
354
342
  properties: {
@@ -385,43 +373,17 @@ const TOOL_OUTPUT_SCHEMAS = {
385
373
  },
386
374
  additionalProperties: true,
387
375
  },
388
- loopforge_list: {
389
- type: "object",
390
- properties: {
391
- sessions: { type: "array", items: { type: "object", additionalProperties: true } },
392
- },
393
- required: ["sessions"],
394
- additionalProperties: false,
395
- },
396
- loopforge_replay: {
397
- type: "object",
398
- properties: {
399
- error: { type: "string" },
400
- sessionId: { type: "string" },
401
- loopId: { type: "string" },
402
- timeline: { type: "array", items: { type: "object", additionalProperties: true } },
403
- },
404
- additionalProperties: true,
405
- },
406
- loopforge_health: {
407
- type: "object",
408
- properties: {
409
- error: { type: "string" },
410
- loopId: { type: "string" },
411
- goal_alignment: { type: "object", additionalProperties: true },
412
- constraint_integrity: { type: "object", additionalProperties: true },
413
- drift_detected: { type: "boolean" },
414
- strategy_stability: { type: "object", additionalProperties: true },
415
- task_continuity: { type: "object", additionalProperties: true },
416
- policy_metrics: { type: "object", additionalProperties: true },
417
- },
418
- additionalProperties: true,
419
- },
376
+ loopforge_gate_check: { type: "object", additionalProperties: true },
377
+ loopforge_gate_resolve: { type: "object", additionalProperties: true },
378
+ loopforge_audit: { type: "object", additionalProperties: true },
420
379
  };
421
380
  function closeObjectSchemas(schema) {
422
381
  const result = { ...schema };
423
382
  if (result.type === "object") {
424
- result.additionalProperties = false;
383
+ // Preserve explicitly open maps such as EvidenceEnvelope.provenance.
384
+ // Fixed-shape objects remain closed by default.
385
+ if (!("additionalProperties" in result))
386
+ result.additionalProperties = false;
425
387
  if (isRecord(result.properties)) {
426
388
  result.properties = Object.fromEntries(Object.entries(result.properties).map(([key, value]) => [
427
389
  key,
@@ -490,6 +452,13 @@ function validateSchema(value, schema, path) {
490
452
  if (type === "string" && typeof value !== "string") {
491
453
  throw new ToolInputValidationError(`${path} must be a string`);
492
454
  }
455
+ if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => candidate === value)) {
456
+ throw new ToolInputValidationError(`${path} must be one of ${schema.enum.join(", ")}`);
457
+ }
458
+ if (type === "string" && typeof value === "string" &&
459
+ typeof schema.pattern === "string" && !new RegExp(schema.pattern).test(value)) {
460
+ throw new ToolInputValidationError(`${path} must match ${schema.pattern}`);
461
+ }
493
462
  if (type === "boolean" && typeof value !== "boolean") {
494
463
  throw new ToolInputValidationError(`${path} must be a boolean`);
495
464
  }
@@ -513,6 +482,55 @@ export function validateToolInput(name, input) {
513
482
  throw new ToolInputValidationError(`Unknown tool: ${name}`);
514
483
  validateSchema(input, contract.inputSchema, "arguments");
515
484
  }
485
+ function enrichAdvanceResult(mgr, result) {
486
+ const sessionId = typeof result.sessionId === "string" ? result.sessionId : "";
487
+ const session = sessionId ? mgr.get(sessionId) : undefined;
488
+ const projection = sessionId ? mgr.cognitiveProjection(sessionId) : null;
489
+ const audit = session ? mgr.audit(session.loopId) : null;
490
+ const enforcement = String(result.enforcementAction ?? "");
491
+ const stopReason = String(result.stopReason ?? "");
492
+ const decision = enforcement === "reject" ? "rejected"
493
+ : enforcement === "backtrack" ? "backtracked"
494
+ : enforcement === "terminate" ? "terminated"
495
+ : stopReason ? "stopped" : "accepted";
496
+ const artifact = session?.roundSnapshot?.promptArtifact;
497
+ const criteria = isRecord(audit?.criteria) ? audit.criteria : {};
498
+ const checks = isRecord(audit?.checks) ? audit.checks : {};
499
+ return {
500
+ ...result,
501
+ decision,
502
+ attempt: session?.roundSnapshot?.attempt ?? result.attempt,
503
+ projection: projection ? {
504
+ phase: Array.isArray(projection.phaseHistory)
505
+ ? projection.phaseHistory.at(-1)?.label ?? ""
506
+ : "",
507
+ focus: projection.currentFocus ?? "",
508
+ todo: projection.todoProjection ?? null,
509
+ gate: projection.activeGate ?? null,
510
+ } : undefined,
511
+ evidenceSummary: {
512
+ verified: Array.isArray(criteria.evidenced) ? criteria.evidenced : [],
513
+ contradicted: Array.isArray(checks.failed) ? checks.failed : [],
514
+ unavailable: Array.isArray(checks.unavailable) ? checks.unavailable : [],
515
+ },
516
+ stateHash: artifact?.stateHash,
517
+ promptHash: artifact?.promptHash,
518
+ };
519
+ }
520
+ /** Uniform decision enrichment for advance-family tools.
521
+ *
522
+ * Handler-level `{error}` responses and session-level lease conflicts are
523
+ * not loop decisions — without this guard the enforcement-only derivation
524
+ * above would label them "stopped" (any stopReason) or even "accepted"
525
+ * (a bare error with no enforcement fields). */
526
+ function finalizeAdvanceOutput(mgr, result) {
527
+ const stopReason = String(result.stopReason ?? "");
528
+ if ((result.error !== undefined && result.error !== null) ||
529
+ stopReason.startsWith("session_owned_elsewhere")) {
530
+ return { ...result, decision: "errored" };
531
+ }
532
+ return enrichAdvanceResult(mgr, result);
533
+ }
516
534
  export const TOOL_HANDLERS = {
517
535
  async loopforge_start(mgr, input) {
518
536
  const startInput = {
@@ -524,37 +542,92 @@ export const TOOL_HANDLERS = {
524
542
  constraints: Array.isArray(input.constraints)
525
543
  ? input.constraints
526
544
  : undefined,
545
+ successCriteria: Array.isArray(input.successCriteria)
546
+ ? input.successCriteria
547
+ : undefined,
527
548
  };
528
549
  if (!startInput.task.trim()) {
529
- return { error: "task is required and must be non-empty" };
550
+ return { error: "task is required and must be non-empty", decision: "errored" };
551
+ }
552
+ const policyMaxRounds = getPolicy().engine.max_rounds;
553
+ if (startInput.maxRounds !== undefined &&
554
+ (!Number.isInteger(startInput.maxRounds) || startInput.maxRounds < 1 || startInput.maxRounds > policyMaxRounds)) {
555
+ return { error: `maxRounds must be an integer between 1 and ${policyMaxRounds}`, decision: "errored" };
530
556
  }
531
557
  const result = await mgr.create(startInput);
532
- return { ...result };
558
+ const enriched = finalizeAdvanceOutput(mgr, { ...result });
559
+ const session = typeof result.sessionId === "string" ? mgr.get(result.sessionId) : undefined;
560
+ return {
561
+ ...enriched,
562
+ loopId: session?.loopId,
563
+ goalId: session?.runtime.state?.task_id ?? session?.loopId,
564
+ };
533
565
  },
534
566
  async loopforge_next(mgr, input) {
535
567
  const sessionId = String(input.sessionId ?? "");
536
- const output = String(input.output ?? "");
537
- const rawEval = input.evaluation;
538
568
  if (!sessionId)
539
- return { error: "sessionId is required" };
540
- // Build SelfEvaluation from structured evaluation parameter
541
- const preExtractedEval = rawEval ? buildSelfEvaluation(rawEval) : undefined;
542
- // Require at least one of: evaluation parameter or output with embedded eval block
543
- if (!preExtractedEval && !output.trim()) {
544
- return { error: "Either evaluation parameter or output with ---loopforge-eval block is required" };
545
- }
546
- const result = await mgr.advance(sessionId, output, preExtractedEval);
547
- return { ...result };
569
+ return { error: "sessionId is required", decision: "errored" };
570
+ const report = parseRoundReport(input.report);
571
+ if (!report)
572
+ return { error: "report is not a valid RoundReport", decision: "errored" };
573
+ const result = await mgr.advance(sessionId, report);
574
+ return finalizeAdvanceOutput(mgr, { ...result });
575
+ },
576
+ async loopforge_retry(mgr, input) {
577
+ const sessionId = String(input.sessionId ?? "");
578
+ if (!sessionId)
579
+ return { error: "sessionId is required", decision: "errored" };
580
+ const report = parseRoundReport(input.report);
581
+ if (!report)
582
+ return { error: "report is not a valid RoundReport", decision: "errored" };
583
+ return finalizeAdvanceOutput(mgr, { ...await mgr.retry(sessionId, report) });
584
+ },
585
+ async loopforge_backtrack(mgr, input) {
586
+ const sessionId = String(input.sessionId ?? "");
587
+ if (!sessionId)
588
+ return { error: "sessionId is required", decision: "errored" };
589
+ // Backtrack is an enforcement action: enrich it like the other advance
590
+ // tools so the output honors the shared advance contract.
591
+ return finalizeAdvanceOutput(mgr, { ...await mgr.backtrack(sessionId) });
548
592
  },
549
593
  async loopforge_status(mgr, input) {
594
+ const scope = String(input.scope ?? "");
595
+ const hasSessionId = typeof input.sessionId === "string" && input.sessionId.trim().length > 0;
596
+ const hasLoopId = typeof input.loopId === "string" && input.loopId.trim().length > 0;
597
+ if (scope === "all") {
598
+ if (hasSessionId || hasLoopId)
599
+ return { error: "scope=all does not accept sessionId or loopId" };
600
+ return { scope, sessions: mgr.list() };
601
+ }
602
+ if (scope === "loop") {
603
+ if (hasSessionId)
604
+ return { error: "scope=loop does not accept sessionId" };
605
+ const loopId = String(input.loopId ?? "").trim();
606
+ if (!loopId)
607
+ return { error: "loopId is required for loop scope" };
608
+ const health = mgr.getHealth(loopId);
609
+ if (!health)
610
+ return { error: `no data found for loop "${loopId}"` };
611
+ const audit = mgr.audit(loopId);
612
+ return input.detail === "full"
613
+ ? { scope, loopId, health, audit }
614
+ : { scope, loopId, health };
615
+ }
616
+ if (scope !== "session")
617
+ return { error: "scope must be session, loop, or all" };
618
+ if (hasLoopId)
619
+ return { error: "scope=session does not accept loopId" };
550
620
  const sessionId = String(input.sessionId ?? "");
551
621
  if (!sessionId)
552
622
  return { error: "sessionId is required" };
553
623
  const session = mgr.get(sessionId);
554
624
  if (!session)
555
625
  return { error: `session not found: ${sessionId}` };
556
- const metrics = session.engine.getMetrics();
557
- return {
626
+ const metrics = session.runtime.getMetrics();
627
+ const state = mgr.cognitiveProjection(sessionId);
628
+ const artifact = session.roundSnapshot?.promptArtifact;
629
+ const audit = mgr.audit(session.loopId);
630
+ const summary = {
558
631
  sessionId: session.sessionId,
559
632
  loopId: session.loopId,
560
633
  round: session.currentRound,
@@ -563,9 +636,40 @@ export const TOOL_HANDLERS = {
563
636
  status: session.status,
564
637
  successTrajectory: session.successTrajectory,
565
638
  lease: mgr.getLeaseStatus(session.loopId),
639
+ };
640
+ if (input.detail !== "full")
641
+ return summary;
642
+ return {
643
+ ...summary,
644
+ cognition: state ? {
645
+ phaseHistory: state.phaseHistory ?? [],
646
+ currentFocus: state.currentFocus ?? "",
647
+ todoProjection: state.todoProjection ?? null,
648
+ suggestedNextAction: state.suggestedNextAction ?? "",
649
+ activeGate: state.activeGate ?? null,
650
+ handoff: state.handoff ?? null,
651
+ stateFreshness: state.stateFreshness ?? "missing",
652
+ regressionObligations: state.regressionObligations ?? [],
653
+ } : null,
654
+ promptDiagnostics: artifact ? {
655
+ level: artifact.level,
656
+ reasons: artifact.levelReasons,
657
+ includedSections: artifact.includedSections,
658
+ omittedSections: artifact.omittedSections,
659
+ stateHash: artifact.stateHash,
660
+ promptHash: artifact.promptHash,
661
+ lastL2Round: artifact.lastL2Round,
662
+ lastMeaningfulProgressRound: artifact.lastMeaningfulProgressRound,
663
+ stateAgeRounds: artifact.stateAgeRounds,
664
+ } : null,
665
+ audit: {
666
+ verdict: audit.verdict,
667
+ missingCriteria: isRecord(audit.criteria) ? audit.criteria.missing : [],
668
+ unresolvedUserGates: audit.unresolvedUserGates ?? [],
669
+ },
566
670
  metrics: {
567
671
  vaultWriteErrors: metrics.vaultWriteErrors,
568
- policy: getPolicyMetrics(session.loopId),
672
+ policy: mgr.getLoopPolicyMetrics(session.loopId),
569
673
  },
570
674
  };
571
675
  },
@@ -579,7 +683,17 @@ export const TOOL_HANDLERS = {
579
683
  const roundsCompleted = session.currentRound;
580
684
  const successTrajectory = [...session.successTrajectory];
581
685
  mgr.delete(sessionId);
582
- return { success: true, roundsCompleted, successTrajectory };
686
+ const audit = mgr.audit(session.loopId);
687
+ return {
688
+ success: true,
689
+ sessionId,
690
+ loopId: session.loopId,
691
+ stopReason: "cancelled",
692
+ stopDetail: "The loop was manually stopped. Vault state was retained for audit and inspection.",
693
+ roundsCompleted,
694
+ successTrajectory,
695
+ audit,
696
+ };
583
697
  },
584
698
  async loopforge_pause(mgr, input) {
585
699
  const sessionId = String(input.sessionId ?? "");
@@ -588,42 +702,53 @@ export const TOOL_HANDLERS = {
588
702
  const result = mgr.pause(sessionId);
589
703
  return { ...result };
590
704
  },
591
- async loopforge_list(mgr, _input) {
592
- const sessions = mgr.list();
593
- return { sessions };
594
- },
595
- async loopforge_replay(mgr, input) {
596
- const sessionId = String(input.sessionId ?? "");
597
- if (!sessionId)
598
- return { error: "sessionId is required" };
599
- const session = mgr.get(sessionId);
600
- if (!session)
601
- return { error: `session not found: ${sessionId}` };
602
- const timeline = mgr.replayTimeline(sessionId) ?? [];
603
- return { sessionId, loopId: session.loopId, timeline };
705
+ async loopforge_history(mgr, input) {
706
+ const loopId = String(input.loopId ?? "").trim();
707
+ const view = String(input.view ?? "");
708
+ if (!loopId)
709
+ return { error: "loopId is required" };
710
+ if (view !== "round" && view !== "timeline")
711
+ return { error: "view must be round or timeline" };
712
+ if (view === "round") {
713
+ const round = Number(input.round);
714
+ if (!Number.isInteger(round) || round < 1)
715
+ return { error: "round must be a positive integer" };
716
+ const inspected = mgr.inspect(loopId, round);
717
+ return inspected ?? { error: `round not found: ${loopId}#${round}` };
718
+ }
719
+ const fromSequence = typeof input.fromSequence === "number" ? input.fromSequence : undefined;
720
+ const toSequence = typeof input.toSequence === "number" ? input.toSequence : undefined;
721
+ const limit = typeof input.limit === "number" ? input.limit : 50;
722
+ const result = mgr.replayTimelineByLoop(loopId, fromSequence, toSequence, limit);
723
+ return { loopId, view, timeline: result.timeline, nextSequence: result.nextSequence };
604
724
  },
605
725
  async loopforge_resume(mgr, input) {
606
726
  const loopId = String(input.loopId ?? "");
607
727
  if (!loopId)
608
- return { error: "loopId is required" };
728
+ return { error: "loopId is required", decision: "errored" };
609
729
  // Paused recovery must run first so the persisted status is atomically
610
730
  // changed back to running before a prompt is returned.
611
731
  let result = await mgr.unpause(loopId);
612
732
  if (!result) {
613
- result = mgr.resume(loopId);
733
+ result = await mgr.resume(loopId);
614
734
  }
615
735
  if (!result)
616
- return { error: `no saved session found for loop "${loopId}"` };
617
- return { ...result };
736
+ return { error: `no saved session found for loop "${loopId}"`, decision: "errored" };
737
+ return finalizeAdvanceOutput(mgr, { ...result });
618
738
  },
619
- async loopforge_health(mgr, input) {
620
- const loopId = String(input.loopId ?? "");
739
+ async loopforge_gate_check(mgr, input) {
740
+ if (!isRecord(input.action))
741
+ return { error: "action descriptor is required" };
742
+ return mgr.checkGate(input.action);
743
+ },
744
+ async loopforge_gate_resolve(mgr, input) {
745
+ return mgr.resolveGate(String(input.sessionId ?? ""), String(input.gateId ?? ""), isRecord(input.action) ? input.action : {}, input.approved === true, String(input.note ?? ""));
746
+ },
747
+ async loopforge_audit(mgr, input) {
748
+ const loopId = String(input.loopId ?? "").trim();
621
749
  if (!loopId)
622
750
  return { error: "loopId is required" };
623
- const health = mgr.getHealth(loopId);
624
- if (!health)
625
- return { error: `no data found for loop "${loopId}"` };
626
- return health;
751
+ return mgr.audit(loopId);
627
752
  },
628
753
  };
629
754
  //# sourceMappingURL=tools.js.map