loopforge 2.7.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 (173) hide show
  1. package/README.md +136 -182
  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 +211 -64
  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 +26 -26
  26. package/dist/evidence-provider.d.ts.map +1 -1
  27. package/dist/evidence-provider.js +45 -71
  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 +23 -29
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +16 -26
  34. package/dist/index.js.map +1 -1
  35. package/dist/loop-compiler.d.ts +2 -2
  36. package/dist/loop-compiler.d.ts.map +1 -1
  37. package/dist/loop-compiler.js +321 -315
  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 -311
  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 +123 -19
  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 +65 -84
  64. package/dist/mcp/session.d.ts.map +1 -1
  65. package/dist/mcp/session.js +667 -224
  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 +401 -254
  70. package/dist/mcp/tools.js.map +1 -1
  71. package/dist/policy-metrics.d.ts +24 -0
  72. package/dist/policy-metrics.d.ts.map +1 -1
  73. package/dist/policy-metrics.js +174 -0
  74. package/dist/policy-metrics.js.map +1 -1
  75. package/dist/policy.d.ts +74 -12
  76. package/dist/policy.d.ts.map +1 -1
  77. package/dist/policy.js +124 -19
  78. package/dist/policy.js.map +1 -1
  79. package/dist/prompt-assembler.d.ts +8 -4
  80. package/dist/prompt-assembler.d.ts.map +1 -1
  81. package/dist/prompt-assembler.js +533 -86
  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 +259 -173
  88. package/dist/protocol.d.ts.map +1 -1
  89. package/dist/protocol.js +17 -96
  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 +53 -22
  100. package/dist/round-coordinator.d.ts.map +1 -1
  101. package/dist/round-coordinator.js +140 -44
  102. package/dist/round-coordinator.js.map +1 -1
  103. package/dist/round-driver.d.ts +18 -10
  104. package/dist/round-driver.d.ts.map +1 -1
  105. package/dist/round-driver.js +11 -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 +23 -12
  120. package/dist/round-transaction.d.ts.map +1 -1
  121. package/dist/round-transaction.js +104 -91
  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/{engine.js → runtime-services.js} +157 -275
  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 +12 -8
  132. package/dist/token-utils.d.ts.map +1 -1
  133. package/dist/token-utils.js +56 -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 +13 -7
  146. package/skills/perception/SKILL.md +200 -90
  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 -50
  152. package/dist/enforcement-gate.d.ts.map +0 -1
  153. package/dist/enforcement-gate.js +0 -501
  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.map +0 -1
  158. package/dist/interop.d.ts +0 -35
  159. package/dist/interop.d.ts.map +0 -1
  160. package/dist/interop.js +0 -34
  161. package/dist/interop.js.map +0 -1
  162. package/dist/run.d.ts +0 -51
  163. package/dist/run.d.ts.map +0 -1
  164. package/dist/run.js +0 -200
  165. package/dist/run.js.map +0 -1
  166. package/dist/self-eval.d.ts +0 -23
  167. package/dist/self-eval.d.ts.map +0 -1
  168. package/dist/self-eval.js +0 -151
  169. package/dist/self-eval.js.map +0 -1
  170. package/dist/verification-gate.d.ts +0 -52
  171. package/dist/verification-gate.d.ts.map +0 -1
  172. package/dist/verification-gate.js +0 -527
  173. package/dist/verification-gate.js.map +0 -1
package/dist/mcp/tools.js CHANGED
@@ -1,13 +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";
6
+ import { isRecord } from "../token-utils.js";
7
+ import { parseRoundReport } from "../round-report.js";
8
+ import { getPolicy } from "../policy.js";
8
9
  // ═══════════════════════════════════════════════════════════════════════════
9
10
  // Tool schemas (MCP JSON Schema format)
10
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
+ };
11
104
  const TOOL_BASE_SCHEMAS = [
12
105
  {
13
106
  name: "loopforge_start",
@@ -25,7 +118,9 @@ const TOOL_BASE_SCHEMAS = [
25
118
  },
26
119
  maxRounds: {
27
120
  type: "number",
28
- 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.",
29
124
  },
30
125
  domain: {
31
126
  type: "string",
@@ -40,234 +135,122 @@ const TOOL_BASE_SCHEMAS = [
40
135
  items: { type: "string" },
41
136
  description: "Hard constraints to enforce across all rounds.",
42
137
  },
138
+ successCriteria: {
139
+ type: "array",
140
+ items: { type: "string" },
141
+ description: "Explicit success criteria to verify across the loop.",
142
+ },
43
143
  },
44
144
  required: ["task"],
45
145
  },
46
146
  },
47
147
  {
48
148
  name: "loopforge_next",
49
- 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.",
50
150
  inputSchema: {
51
151
  type: "object",
152
+ additionalProperties: false,
52
153
  properties: {
53
154
  sessionId: {
54
155
  type: "string",
55
156
  description: "Session ID returned by loopforge_start.",
56
157
  },
57
- output: {
58
- type: "string",
59
- description: "Optional. The agent's full output from executing the current round's prompt. May be omitted if evaluation parameter is provided.",
60
- },
61
- evaluation: {
62
- type: "object",
63
- description: "Structured self-evaluation for this round. Required for the loop to continue. Preferred over embedding ---loopforge-eval blocks in output text.",
64
- required: ["success", "output_summary", "should_continue", "constraint_violations"],
65
- properties: {
66
- success: {
67
- type: "boolean",
68
- description: "true ONLY if all hard constraints met AND the task goal achieved.",
69
- },
70
- output_summary: {
71
- type: "string",
72
- description: "Specific, actionable summary of what was DONE this round — not what was attempted.",
73
- },
74
- should_continue: {
75
- type: "boolean",
76
- description: "false ONLY when the ENTIRE task is complete. Partial progress = true.",
77
- },
78
- constraint_violations: {
79
- type: "array",
80
- items: { type: "string" },
81
- description: "Constraints the agent actually violated this round. Be honest.",
82
- },
83
- discovered_constraints: {
84
- type: "array",
85
- items: { type: "string" },
86
- description: "Optional. New constraints discovered this round.",
87
- },
88
- objective_refinement: {
89
- type: "string",
90
- description: "Optional. If this round deepened understanding of the task objective.",
91
- },
92
- emerged_subtasks: {
93
- type: "array",
94
- items: { type: "string" },
95
- description: "Optional. Sub-problems that surfaced during execution.",
96
- },
97
- execution_evidence: {
98
- type: "object",
99
- description: "Optional. Structured record of what actually happened this round.",
100
- properties: {
101
- files_changed: {
102
- type: "array",
103
- items: { type: "string" },
104
- description: "Files modified this round.",
105
- },
106
- test_results: {
107
- type: "object",
108
- description: "Test results from this round.",
109
- properties: {
110
- passed: { type: "integer", minimum: 0, description: "Number of passing tests." },
111
- failed: { type: "integer", minimum: 0, description: "Number of failing tests." },
112
- skipped: { type: "integer", minimum: 0, description: "Number of skipped tests." },
113
- },
114
- },
115
- success_criteria_met: {
116
- type: "array",
117
- items: { type: "string" },
118
- description: "Success criteria satisfied this round.",
119
- },
120
- success_criteria_remaining: {
121
- type: "array",
122
- items: { type: "string" },
123
- description: "Success criteria still outstanding.",
124
- },
125
- progress_estimate: {
126
- type: "number",
127
- minimum: 0,
128
- maximum: 1,
129
- description: "Estimated progress toward task completion (0.0–1.0).",
130
- },
131
- },
132
- },
133
- retracted_constraints: {
134
- type: "array",
135
- items: { type: "string" },
136
- description: "Optional. Constraints the agent now believes are wrong.",
137
- },
138
- revised_success_criteria: {
139
- type: "array",
140
- description: "Optional. Success criteria that need reformulation.",
141
- items: {
142
- type: "object",
143
- properties: {
144
- old: { type: "string", description: "Original criterion." },
145
- new: { type: "string", description: "Revised criterion." },
146
- },
147
- },
148
- },
149
- wrong_assumptions: {
150
- type: "array",
151
- items: { type: "string" },
152
- description: "Optional. Assumptions from earlier rounds that were incorrect.",
153
- },
154
- worker_results: {
155
- type: "array",
156
- description: "Optional. Results of sub-agent / Worker delegations this round.",
157
- items: {
158
- type: "object",
159
- properties: {
160
- agentId: { type: "string" },
161
- subAgentType: { type: "string" },
162
- subTask: { type: "string" },
163
- resultSummary: { type: "string" },
164
- success: { type: "boolean" },
165
- discoveredConstraints: {
166
- type: "array",
167
- items: { type: "string" },
168
- },
169
- },
170
- required: ["agentId", "subAgentType", "subTask", "resultSummary", "success"],
171
- },
172
- },
173
- compression_checkpoint: {
174
- type: "boolean",
175
- 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.",
176
- },
177
- checkpoint_label: {
178
- type: "string",
179
- description: "Optional. Human-readable label for this checkpoint (e.g. 'core-functions-complete'). Only meaningful when compression_checkpoint is true.",
180
- },
181
- next_action: {
182
- type: "string",
183
- 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.",
184
- },
185
- completed_subtasks: {
186
- type: "array",
187
- items: { type: "string" },
188
- description: "Optional. Sub-tasks completed this round. Matched to previously emerged sub-tasks by description similarity.",
189
- },
190
- blocked_subtasks: {
191
- type: "array",
192
- items: { type: "string" },
193
- description: "Optional. Sub-tasks that are now blocked. Describe what is blocking and why.",
194
- },
195
- canceled_subtasks: {
196
- type: "array",
197
- items: { type: "string" },
198
- description: "Optional. Sub-tasks no longer needed. Removes them from the active sub-goal list.",
199
- },
200
- stop_reason: {
201
- type: "string",
202
- enum: ["gave_up", "blocked", "needs_human_input"],
203
- description: "Optional. Why the agent is stopping. Only meaningful when should_continue=false and success=false. 'blocked' means work cannot proceed under current constraints.",
204
- },
205
- },
158
+ report: {
159
+ ...ROUND_REPORT_SCHEMA,
160
+ description: "Canonical round report. This is the only execution report accepted by loopforge_next.",
206
161
  },
207
162
  },
208
- required: ["sessionId", "evaluation"],
163
+ required: ["sessionId", "report"],
209
164
  },
210
165
  },
211
166
  {
212
167
  name: "loopforge_status",
213
- 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.",
214
169
  inputSchema: {
215
170
  type: "object",
216
171
  properties: {
172
+ scope: { type: "string", enum: ["session", "loop", "all"] },
217
173
  sessionId: {
218
174
  type: "string",
219
175
  description: "Session ID returned by loopforge_start.",
220
176
  },
177
+ loopId: { type: "string" },
178
+ detail: { type: "string", enum: ["summary", "full"] },
221
179
  },
222
- required: ["sessionId"],
180
+ required: ["scope"],
223
181
  },
224
182
  },
225
183
  {
226
- name: "loopforge_stop",
227
- 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.",
228
186
  inputSchema: {
229
187
  type: "object",
230
188
  properties: {
231
189
  sessionId: {
232
190
  type: "string",
233
- 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.",
234
196
  },
235
197
  },
236
- required: ["sessionId"],
198
+ required: ["sessionId", "report"],
237
199
  },
238
200
  },
239
201
  {
240
- name: "loopforge_pause",
241
- 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.",
242
204
  inputSchema: {
243
205
  type: "object",
244
206
  properties: {
245
207
  sessionId: {
246
208
  type: "string",
247
- description: "Session ID to pause.",
209
+ description: "Session ID returned by loopforge_start.",
248
210
  },
249
211
  },
250
212
  required: ["sessionId"],
251
213
  },
252
214
  },
253
215
  {
254
- name: "loopforge_list",
255
- 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.",
256
218
  inputSchema: {
257
219
  type: "object",
258
- properties: {},
259
- 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"],
260
229
  },
261
230
  },
262
231
  {
263
- name: "loopforge_replay",
264
- 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.",
265
234
  inputSchema: {
266
235
  type: "object",
267
236
  properties: {
268
237
  sessionId: {
269
238
  type: "string",
270
- 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.",
271
254
  },
272
255
  },
273
256
  required: ["sessionId"],
@@ -288,16 +271,37 @@ const TOOL_BASE_SCHEMAS = [
288
271
  },
289
272
  },
290
273
  {
291
- name: "loopforge_health",
292
- 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.",
293
276
  inputSchema: {
294
277
  type: "object",
295
278
  properties: {
296
- loopId: {
297
- type: "string",
298
- description: "Loop ID to check health for.",
299
- },
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" },
300
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" } },
301
305
  required: ["loopId"],
302
306
  },
303
307
  },
@@ -310,12 +314,18 @@ const ADVANCE_OUTPUT_SCHEMA = {
310
314
  round: { type: "number" },
311
315
  roundId: { type: ["string", "null"] },
312
316
  prompt: { type: ["string", "null"] },
317
+ decision: { enum: ["accepted", "rejected", "backtracked", "stopped", "terminated", "errored"] },
318
+ attempt: { type: "number" },
313
319
  stopReason: { type: "string" },
314
320
  stopDetail: { type: "string", description: "Human-readable context for why the loop stopped. Provides facts without prescribing a specific agent action." },
315
321
  level: { type: "string" },
316
322
  roundSuccess: { type: "boolean" },
317
- enforcementAction: { enum: ["accept", "reject", "terminate"] },
323
+ enforcementAction: { enum: ["accept", "reject", "terminate", "backtrack"] },
318
324
  enforcementReason: { type: "string" },
325
+ projection: { type: "object", additionalProperties: true },
326
+ evidenceSummary: { type: "object", additionalProperties: true },
327
+ stateHash: { type: "string" },
328
+ promptHash: { type: "string" },
319
329
  warnings: { type: "array", items: { type: "string" } },
320
330
  },
321
331
  additionalProperties: true,
@@ -323,7 +333,10 @@ const ADVANCE_OUTPUT_SCHEMA = {
323
333
  const TOOL_OUTPUT_SCHEMAS = {
324
334
  loopforge_start: ADVANCE_OUTPUT_SCHEMA,
325
335
  loopforge_next: ADVANCE_OUTPUT_SCHEMA,
336
+ loopforge_retry: ADVANCE_OUTPUT_SCHEMA,
337
+ loopforge_backtrack: ADVANCE_OUTPUT_SCHEMA,
326
338
  loopforge_resume: ADVANCE_OUTPUT_SCHEMA,
339
+ loopforge_history: { type: "object", additionalProperties: true },
327
340
  loopforge_status: {
328
341
  type: "object",
329
342
  properties: {
@@ -360,46 +373,17 @@ const TOOL_OUTPUT_SCHEMAS = {
360
373
  },
361
374
  additionalProperties: true,
362
375
  },
363
- loopforge_list: {
364
- type: "object",
365
- properties: {
366
- sessions: { type: "array", items: { type: "object", additionalProperties: true } },
367
- },
368
- required: ["sessions"],
369
- additionalProperties: false,
370
- },
371
- loopforge_replay: {
372
- type: "object",
373
- properties: {
374
- error: { type: "string" },
375
- sessionId: { type: "string" },
376
- loopId: { type: "string" },
377
- timeline: { type: "array", items: { type: "object", additionalProperties: true } },
378
- },
379
- additionalProperties: true,
380
- },
381
- loopforge_health: {
382
- type: "object",
383
- properties: {
384
- error: { type: "string" },
385
- loopId: { type: "string" },
386
- goal_alignment: { type: "object", additionalProperties: true },
387
- constraint_integrity: { type: "object", additionalProperties: true },
388
- drift_detected: { type: "boolean" },
389
- strategy_stability: { type: "object", additionalProperties: true },
390
- task_continuity: { type: "object", additionalProperties: true },
391
- policy_metrics: { type: "object", additionalProperties: true },
392
- },
393
- additionalProperties: true,
394
- },
376
+ loopforge_gate_check: { type: "object", additionalProperties: true },
377
+ loopforge_gate_resolve: { type: "object", additionalProperties: true },
378
+ loopforge_audit: { type: "object", additionalProperties: true },
395
379
  };
396
- function isRecord(value) {
397
- return value !== null && typeof value === "object" && !Array.isArray(value);
398
- }
399
380
  function closeObjectSchemas(schema) {
400
381
  const result = { ...schema };
401
382
  if (result.type === "object") {
402
- 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;
403
387
  if (isRecord(result.properties)) {
404
388
  result.properties = Object.fromEntries(Object.entries(result.properties).map(([key, value]) => [
405
389
  key,
@@ -468,6 +452,13 @@ function validateSchema(value, schema, path) {
468
452
  if (type === "string" && typeof value !== "string") {
469
453
  throw new ToolInputValidationError(`${path} must be a string`);
470
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
+ }
471
462
  if (type === "boolean" && typeof value !== "boolean") {
472
463
  throw new ToolInputValidationError(`${path} must be a boolean`);
473
464
  }
@@ -491,6 +482,55 @@ export function validateToolInput(name, input) {
491
482
  throw new ToolInputValidationError(`Unknown tool: ${name}`);
492
483
  validateSchema(input, contract.inputSchema, "arguments");
493
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
+ }
494
534
  export const TOOL_HANDLERS = {
495
535
  async loopforge_start(mgr, input) {
496
536
  const startInput = {
@@ -502,37 +542,92 @@ export const TOOL_HANDLERS = {
502
542
  constraints: Array.isArray(input.constraints)
503
543
  ? input.constraints
504
544
  : undefined,
545
+ successCriteria: Array.isArray(input.successCriteria)
546
+ ? input.successCriteria
547
+ : undefined,
505
548
  };
506
549
  if (!startInput.task.trim()) {
507
- 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" };
508
556
  }
509
557
  const result = await mgr.create(startInput);
510
- 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
+ };
511
565
  },
512
566
  async loopforge_next(mgr, input) {
513
567
  const sessionId = String(input.sessionId ?? "");
514
- const output = String(input.output ?? "");
515
- const rawEval = input.evaluation;
516
568
  if (!sessionId)
517
- return { error: "sessionId is required" };
518
- // Build SelfEvaluation from structured evaluation parameter
519
- const preExtractedEval = rawEval ? buildSelfEvaluation(rawEval) : undefined;
520
- // Require at least one of: evaluation parameter or output with embedded eval block
521
- if (!preExtractedEval && !output.trim()) {
522
- return { error: "Either evaluation parameter or output with ---loopforge-eval block is required" };
523
- }
524
- const result = await mgr.advance(sessionId, output, preExtractedEval);
525
- 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) });
526
592
  },
527
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" };
528
620
  const sessionId = String(input.sessionId ?? "");
529
621
  if (!sessionId)
530
622
  return { error: "sessionId is required" };
531
623
  const session = mgr.get(sessionId);
532
624
  if (!session)
533
625
  return { error: `session not found: ${sessionId}` };
534
- const metrics = session.engine.getMetrics();
535
- 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 = {
536
631
  sessionId: session.sessionId,
537
632
  loopId: session.loopId,
538
633
  round: session.currentRound,
@@ -541,9 +636,40 @@ export const TOOL_HANDLERS = {
541
636
  status: session.status,
542
637
  successTrajectory: session.successTrajectory,
543
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
+ },
544
670
  metrics: {
545
671
  vaultWriteErrors: metrics.vaultWriteErrors,
546
- policy: getPolicyMetrics(session.loopId),
672
+ policy: mgr.getLoopPolicyMetrics(session.loopId),
547
673
  },
548
674
  };
549
675
  },
@@ -557,7 +683,17 @@ export const TOOL_HANDLERS = {
557
683
  const roundsCompleted = session.currentRound;
558
684
  const successTrajectory = [...session.successTrajectory];
559
685
  mgr.delete(sessionId);
560
- 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
+ };
561
697
  },
562
698
  async loopforge_pause(mgr, input) {
563
699
  const sessionId = String(input.sessionId ?? "");
@@ -566,42 +702,53 @@ export const TOOL_HANDLERS = {
566
702
  const result = mgr.pause(sessionId);
567
703
  return { ...result };
568
704
  },
569
- async loopforge_list(mgr, _input) {
570
- const sessions = mgr.list();
571
- return { sessions };
572
- },
573
- async loopforge_replay(mgr, input) {
574
- const sessionId = String(input.sessionId ?? "");
575
- if (!sessionId)
576
- return { error: "sessionId is required" };
577
- const session = mgr.get(sessionId);
578
- if (!session)
579
- return { error: `session not found: ${sessionId}` };
580
- const timeline = mgr.replayTimeline(sessionId) ?? [];
581
- 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 };
582
724
  },
583
725
  async loopforge_resume(mgr, input) {
584
726
  const loopId = String(input.loopId ?? "");
585
727
  if (!loopId)
586
- return { error: "loopId is required" };
728
+ return { error: "loopId is required", decision: "errored" };
587
729
  // Paused recovery must run first so the persisted status is atomically
588
730
  // changed back to running before a prompt is returned.
589
731
  let result = await mgr.unpause(loopId);
590
732
  if (!result) {
591
- result = mgr.resume(loopId);
733
+ result = await mgr.resume(loopId);
592
734
  }
593
735
  if (!result)
594
- return { error: `no saved session found for loop "${loopId}"` };
595
- return { ...result };
736
+ return { error: `no saved session found for loop "${loopId}"`, decision: "errored" };
737
+ return finalizeAdvanceOutput(mgr, { ...result });
596
738
  },
597
- async loopforge_health(mgr, input) {
598
- 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();
599
749
  if (!loopId)
600
750
  return { error: "loopId is required" };
601
- const health = mgr.getHealth(loopId);
602
- if (!health)
603
- return { error: `no data found for loop "${loopId}"` };
604
- return health;
751
+ return mgr.audit(loopId);
605
752
  },
606
753
  };
607
754
  //# sourceMappingURL=tools.js.map