loopforge 3.0.0 → 3.2.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 (178) hide show
  1. package/README.md +128 -145
  2. package/dist/audit.d.ts +50 -0
  3. package/dist/audit.d.ts.map +1 -0
  4. package/dist/audit.js +158 -0
  5. package/dist/audit.js.map +1 -0
  6. package/dist/canonical-state.d.ts +28 -20
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +82 -173
  9. package/dist/canonical-state.js.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +25 -16
  12. package/dist/cli.js.map +1 -1
  13. package/dist/cognitive-governance.d.ts +25 -33
  14. package/dist/cognitive-governance.d.ts.map +1 -1
  15. package/dist/cognitive-governance.js +38 -138
  16. package/dist/cognitive-governance.js.map +1 -1
  17. package/dist/enforcement-gate.d.ts +76 -0
  18. package/dist/enforcement-gate.d.ts.map +1 -0
  19. package/dist/enforcement-gate.js +1071 -0
  20. package/dist/enforcement-gate.js.map +1 -0
  21. package/dist/engine.d.ts +99 -0
  22. package/dist/engine.d.ts.map +1 -0
  23. package/dist/engine.js +839 -0
  24. package/dist/engine.js.map +1 -0
  25. package/dist/evidence-claims.d.ts +50 -0
  26. package/dist/evidence-claims.d.ts.map +1 -0
  27. package/dist/evidence-claims.js +134 -0
  28. package/dist/evidence-claims.js.map +1 -0
  29. package/dist/evidence-provider.d.ts +21 -23
  30. package/dist/evidence-provider.d.ts.map +1 -1
  31. package/dist/evidence-provider.js +74 -34
  32. package/dist/evidence-provider.js.map +1 -1
  33. package/dist/generate-schema.js +1 -84
  34. package/dist/generate-schema.js.map +1 -1
  35. package/dist/index.d.ts +19 -22
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +16 -15
  38. package/dist/index.js.map +1 -1
  39. package/dist/loop-compiler.d.ts +41 -3
  40. package/dist/loop-compiler.d.ts.map +1 -1
  41. package/dist/loop-compiler.js +632 -273
  42. package/dist/loop-compiler.js.map +1 -1
  43. package/dist/loop-extras-parser.d.ts +2 -2
  44. package/dist/loop-extras-parser.d.ts.map +1 -1
  45. package/dist/loop-extras-parser.js +2 -2
  46. package/dist/loop-extras-parser.js.map +1 -1
  47. package/dist/loop-projection.d.ts +26 -0
  48. package/dist/loop-projection.d.ts.map +1 -0
  49. package/dist/loop-projection.js +142 -0
  50. package/dist/loop-projection.js.map +1 -0
  51. package/dist/loop-store.d.ts +112 -24
  52. package/dist/loop-store.d.ts.map +1 -1
  53. package/dist/loop-store.js +365 -218
  54. package/dist/loop-store.js.map +1 -1
  55. package/dist/mcp/round-lifecycle.d.ts +216 -0
  56. package/dist/mcp/round-lifecycle.d.ts.map +1 -0
  57. package/dist/mcp/round-lifecycle.js +926 -0
  58. package/dist/mcp/round-lifecycle.js.map +1 -0
  59. package/dist/mcp/server.d.ts +2 -1
  60. package/dist/mcp/server.d.ts.map +1 -1
  61. package/dist/mcp/server.js +28 -42
  62. package/dist/mcp/server.js.map +1 -1
  63. package/dist/mcp/session.d.ts +53 -101
  64. package/dist/mcp/session.d.ts.map +1 -1
  65. package/dist/mcp/session.js +306 -1105
  66. package/dist/mcp/session.js.map +1 -1
  67. package/dist/mcp/tools.d.ts +13 -16
  68. package/dist/mcp/tools.d.ts.map +1 -1
  69. package/dist/mcp/tools.js +479 -413
  70. package/dist/mcp/tools.js.map +1 -1
  71. package/dist/policy-metrics.d.ts +12 -17
  72. package/dist/policy-metrics.d.ts.map +1 -1
  73. package/dist/policy-metrics.js +95 -161
  74. package/dist/policy-metrics.js.map +1 -1
  75. package/dist/policy.d.ts +44 -35
  76. package/dist/policy.d.ts.map +1 -1
  77. package/dist/policy.js +31 -115
  78. package/dist/policy.js.map +1 -1
  79. package/dist/prompt-assembler.d.ts +37 -6
  80. package/dist/prompt-assembler.d.ts.map +1 -1
  81. package/dist/prompt-assembler.js +389 -160
  82. package/dist/prompt-assembler.js.map +1 -1
  83. package/dist/prompt-policy.d.ts +1 -8
  84. package/dist/prompt-policy.d.ts.map +1 -1
  85. package/dist/prompt-policy.js +8 -26
  86. package/dist/prompt-policy.js.map +1 -1
  87. package/dist/protocol.d.ts +357 -229
  88. package/dist/protocol.d.ts.map +1 -1
  89. package/dist/protocol.js +139 -15
  90. package/dist/protocol.js.map +1 -1
  91. package/dist/replay.d.ts +5 -13
  92. package/dist/replay.d.ts.map +1 -1
  93. package/dist/replay.js +19 -55
  94. package/dist/replay.js.map +1 -1
  95. package/dist/round-coordinator.d.ts +20 -24
  96. package/dist/round-coordinator.d.ts.map +1 -1
  97. package/dist/round-coordinator.js +99 -92
  98. package/dist/round-coordinator.js.map +1 -1
  99. package/dist/round-driver.d.ts +14 -13
  100. package/dist/round-driver.d.ts.map +1 -1
  101. package/dist/round-driver.js +26 -14
  102. package/dist/round-driver.js.map +1 -1
  103. package/dist/round-transaction.d.ts +15 -18
  104. package/dist/round-transaction.d.ts.map +1 -1
  105. package/dist/round-transaction.js +38 -47
  106. package/dist/round-transaction.js.map +1 -1
  107. package/dist/self-eval.d.ts +53 -0
  108. package/dist/self-eval.d.ts.map +1 -0
  109. package/dist/self-eval.js +284 -0
  110. package/dist/self-eval.js.map +1 -0
  111. package/dist/storage.d.ts +9 -4
  112. package/dist/storage.d.ts.map +1 -1
  113. package/dist/storage.js +18 -6
  114. package/dist/storage.js.map +1 -1
  115. package/dist/token-utils.d.ts +15 -3
  116. package/dist/token-utils.d.ts.map +1 -1
  117. package/dist/token-utils.js +24 -5
  118. package/dist/token-utils.js.map +1 -1
  119. package/dist/verification-gate.d.ts +108 -0
  120. package/dist/verification-gate.d.ts.map +1 -0
  121. package/dist/verification-gate.js +971 -0
  122. package/dist/verification-gate.js.map +1 -0
  123. package/loop_policy.json +44 -15
  124. package/package.json +5 -10
  125. package/skills/perception/SKILL.md +182 -199
  126. package/dist/canonical-event-writer.d.ts +0 -20
  127. package/dist/canonical-event-writer.d.ts.map +0 -1
  128. package/dist/canonical-event-writer.js +0 -131
  129. package/dist/canonical-event-writer.js.map +0 -1
  130. package/dist/compiler-subgoals.d.ts +0 -7
  131. package/dist/compiler-subgoals.d.ts.map +0 -1
  132. package/dist/compiler-subgoals.js +0 -93
  133. package/dist/compiler-subgoals.js.map +0 -1
  134. package/dist/derive-id.d.ts +0 -12
  135. package/dist/derive-id.d.ts.map +0 -1
  136. package/dist/derive-id.js +0 -31
  137. package/dist/derive-id.js.map +0 -1
  138. package/dist/mcp/session-projection.d.ts +0 -5
  139. package/dist/mcp/session-projection.d.ts.map +0 -1
  140. package/dist/mcp/session-projection.js +0 -18
  141. package/dist/mcp/session-projection.js.map +0 -1
  142. package/dist/mcp/session-queue.d.ts +0 -3
  143. package/dist/mcp/session-queue.d.ts.map +0 -1
  144. package/dist/mcp/session-queue.js +0 -18
  145. package/dist/mcp/session-queue.js.map +0 -1
  146. package/dist/mcp/session-types.d.ts +0 -72
  147. package/dist/mcp/session-types.d.ts.map +0 -1
  148. package/dist/mcp/session-types.js +0 -2
  149. package/dist/mcp/session-types.js.map +0 -1
  150. package/dist/round-commit-store.d.ts +0 -12
  151. package/dist/round-commit-store.d.ts.map +0 -1
  152. package/dist/round-commit-store.js +0 -13
  153. package/dist/round-commit-store.js.map +0 -1
  154. package/dist/round-gate.d.ts +0 -31
  155. package/dist/round-gate.d.ts.map +0 -1
  156. package/dist/round-gate.js +0 -259
  157. package/dist/round-gate.js.map +0 -1
  158. package/dist/round-recovery.d.ts +0 -17
  159. package/dist/round-recovery.d.ts.map +0 -1
  160. package/dist/round-recovery.js +0 -117
  161. package/dist/round-recovery.js.map +0 -1
  162. package/dist/round-report.d.ts +0 -16
  163. package/dist/round-report.d.ts.map +0 -1
  164. package/dist/round-report.js +0 -401
  165. package/dist/round-report.js.map +0 -1
  166. package/dist/runtime-services.d.ts +0 -59
  167. package/dist/runtime-services.d.ts.map +0 -1
  168. package/dist/runtime-services.js +0 -460
  169. package/dist/runtime-services.js.map +0 -1
  170. package/dist/vault-format.d.ts +0 -4
  171. package/dist/vault-format.d.ts.map +0 -1
  172. package/dist/vault-format.js +0 -4
  173. package/dist/vault-format.js.map +0 -1
  174. package/dist/vault-types.d.ts +0 -24
  175. package/dist/vault-types.d.ts.map +0 -1
  176. package/dist/vault-types.js +0 -2
  177. package/dist/vault-types.js.map +0 -1
  178. package/loopforge-policy.json +0 -340
package/dist/mcp/tools.js CHANGED
@@ -1,106 +1,17 @@
1
1
  /** LoopForge MCP — Tool definitions and handlers.
2
2
  *
3
- * Public tools for round execution, recovery, gates, and audit.
3
+ * 9 tools: start, next, status, stop, pause, resume, replay,
4
+ * gate_check, gate_resolve.
5
+ * status is the unified inspection tool (view=session|loop|all|audit).
4
6
  * Each handler receives SessionManager + parsed input, returns the output object.
5
7
  */
8
+ import { buildSelfEvaluation } from "../engine.js";
9
+ import { collectSelfEvalGaps } from "../self-eval.js";
6
10
  import { isRecord } from "../token-utils.js";
7
- import { parseRoundReport } from "../round-report.js";
8
- import { getPolicy } from "../policy.js";
11
+ import { validateLoopId } from "../policy.js";
9
12
  // ═══════════════════════════════════════════════════════════════════════════
10
13
  // Tool schemas (MCP JSON Schema format)
11
14
  // ═══════════════════════════════════════════════════════════════════════════
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
- };
104
15
  const TOOL_BASE_SCHEMAS = [
105
16
  {
106
17
  name: "loopforge_start",
@@ -117,10 +28,9 @@ const TOOL_BASE_SCHEMAS = [
117
28
  description: "Optional loop ID. Generated automatically if not provided.",
118
29
  },
119
30
  maxRounds: {
120
- type: "number",
31
+ type: "integer",
121
32
  minimum: 1,
122
- multipleOf: 1,
123
- description: "Maximum rounds before auto-stop. Default: 100 from policy.",
33
+ description: "Maximum rounds before auto-stop. Default: 20 from policy.",
124
34
  },
125
35
  domain: {
126
36
  type: "string",
@@ -135,122 +45,301 @@ const TOOL_BASE_SCHEMAS = [
135
45
  items: { type: "string" },
136
46
  description: "Hard constraints to enforce across all rounds.",
137
47
  },
138
- successCriteria: {
139
- type: "array",
140
- items: { type: "string" },
141
- description: "Explicit success criteria to verify across the loop.",
142
- },
143
48
  },
144
49
  required: ["task"],
145
50
  },
146
51
  },
147
52
  {
148
53
  name: "loopforge_next",
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.",
54
+ 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.",
150
55
  inputSchema: {
151
56
  type: "object",
152
- additionalProperties: false,
153
57
  properties: {
154
58
  sessionId: {
155
59
  type: "string",
156
60
  description: "Session ID returned by loopforge_start.",
157
61
  },
158
- report: {
159
- ...ROUND_REPORT_SCHEMA,
160
- description: "Canonical round report. This is the only execution report accepted by loopforge_next.",
62
+ roundId: {
63
+ type: "string",
64
+ description: "The roundId from the most recent loopforge_start / loopforge_next / loopforge_resume / loopforge_status response. Anchors this submission to the exact round it reports on: a submission whose roundId no longer matches the current round (the round already committed) is not processed — the current held prompt is returned instead, so lost responses can be recovered without skipping or double-committing a round.",
65
+ },
66
+ output: {
67
+ type: "string",
68
+ description: "Optional. The agent's full output from executing the current round's prompt. May be omitted if evaluation parameter is provided.",
69
+ },
70
+ evaluation: {
71
+ type: "object",
72
+ description: "Structured self-evaluation for this round. Either this or an output containing a ---loopforge-eval block is required for the loop to continue. Preferred over embedding ---loopforge-eval blocks in output text.",
73
+ required: ["success", "output_summary", "should_continue", "constraint_violations"],
74
+ properties: {
75
+ success: {
76
+ type: "boolean",
77
+ description: "true ONLY if all hard constraints met AND the task goal achieved.",
78
+ },
79
+ output_summary: {
80
+ type: "string",
81
+ description: "Specific, actionable summary of what was DONE this round — not what was attempted.",
82
+ },
83
+ should_continue: {
84
+ type: "boolean",
85
+ description: "false ONLY when the ENTIRE task is complete. Partial progress = true.",
86
+ },
87
+ constraint_violations: {
88
+ type: "array",
89
+ items: { type: "string" },
90
+ description: "Constraints the agent actually violated this round. Be honest.",
91
+ },
92
+ discovered_constraints: {
93
+ type: "array",
94
+ items: { type: "string" },
95
+ description: "Optional. New constraints discovered this round.",
96
+ },
97
+ objective_refinement: {
98
+ type: "string",
99
+ description: "Optional. If this round deepened understanding of the task objective.",
100
+ },
101
+ emerged_subtasks: {
102
+ type: "array",
103
+ items: { type: "string" },
104
+ description: "Optional. Sub-problems that surfaced during execution.",
105
+ },
106
+ execution_evidence: {
107
+ type: "object",
108
+ description: "Optional. Structured record of what actually happened this round.",
109
+ properties: {
110
+ files_changed: {
111
+ type: "array",
112
+ items: { type: "string" },
113
+ description: "Files modified this round.",
114
+ },
115
+ test_results: {
116
+ type: "object",
117
+ description: "Test results from this round.",
118
+ properties: {
119
+ passed: { type: "integer", minimum: 0, description: "Number of passing tests." },
120
+ failed: { type: "integer", minimum: 0, description: "Number of failing tests." },
121
+ skipped: { type: "integer", minimum: 0, description: "Number of skipped tests." },
122
+ },
123
+ },
124
+ success_criteria_met: {
125
+ type: "array",
126
+ items: { type: "string" },
127
+ description: "Success criteria satisfied this round.",
128
+ },
129
+ success_criteria_remaining: {
130
+ type: "array",
131
+ items: { type: "string" },
132
+ description: "Success criteria still outstanding.",
133
+ },
134
+ progress_estimate: {
135
+ type: "number",
136
+ minimum: 0,
137
+ maximum: 1,
138
+ description: "Estimated progress toward task completion (0.0–1.0).",
139
+ },
140
+ },
141
+ },
142
+ retracted_constraints: {
143
+ type: "array",
144
+ items: { type: "string" },
145
+ description: "Optional. Constraints the agent now believes are wrong.",
146
+ },
147
+ revised_success_criteria: {
148
+ type: "array",
149
+ description: "Optional. Success criteria that need reformulation.",
150
+ items: {
151
+ type: "object",
152
+ properties: {
153
+ old: { type: "string", description: "Original criterion." },
154
+ new: { type: "string", description: "Revised criterion." },
155
+ },
156
+ },
157
+ },
158
+ wrong_assumptions: {
159
+ type: "array",
160
+ items: { type: "string" },
161
+ description: "Optional. Assumptions from earlier rounds that were incorrect.",
162
+ },
163
+ worker_results: {
164
+ type: "array",
165
+ description: "Optional. Results of sub-agent / Worker delegations this round.",
166
+ items: {
167
+ type: "object",
168
+ properties: {
169
+ agentId: { type: "string" },
170
+ subAgentType: { type: "string" },
171
+ subTask: { type: "string" },
172
+ resultSummary: { type: "string" },
173
+ success: { type: "boolean" },
174
+ outcome: {
175
+ type: "string",
176
+ enum: ["success", "partial", "failed"],
177
+ description: "Optional. Worker outcome; derived from success when absent.",
178
+ },
179
+ discoveredConstraints: {
180
+ type: "array",
181
+ items: { type: "string" },
182
+ },
183
+ },
184
+ required: ["agentId", "subAgentType", "subTask", "resultSummary", "success"],
185
+ },
186
+ },
187
+ compression_checkpoint: {
188
+ type: "boolean",
189
+ 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.",
190
+ },
191
+ checkpoint_label: {
192
+ type: "string",
193
+ description: "Optional. Human-readable label for this checkpoint (e.g. 'core-functions-complete'). Only meaningful when compression_checkpoint is true.",
194
+ },
195
+ next_action: {
196
+ type: "string",
197
+ 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.",
198
+ },
199
+ completed_subtasks: {
200
+ type: "array",
201
+ items: { type: "string" },
202
+ description: "Optional. Sub-tasks completed this round. Matched to previously emerged sub-tasks by description similarity.",
203
+ },
204
+ blocked_subtasks: {
205
+ type: "array",
206
+ items: { type: "string" },
207
+ description: "Optional. Sub-tasks that are now blocked. Describe what is blocking and why.",
208
+ },
209
+ canceled_subtasks: {
210
+ type: "array",
211
+ items: { type: "string" },
212
+ description: "Optional. Sub-tasks no longer needed. Removes them from the active sub-goal list.",
213
+ },
214
+ stop_reason: {
215
+ type: "string",
216
+ enum: ["gave_up", "blocked", "needs_human_input"],
217
+ description: "Optional. Why the agent is stopping. Only meaningful when should_continue=false and success=false. 'blocked' means work cannot proceed under current constraints.",
218
+ },
219
+ no_change_reason: {
220
+ type: "string",
221
+ description: "Optional. Declared reason for claiming success with no machine-verifiable evidence (e.g. documentation-only round). Downgrades the success_without_verified_evidence check from error to info. Max 200 chars.",
222
+ },
223
+ outcome: {
224
+ type: "string",
225
+ enum: ["success", "partial", "failed", "blocked"],
226
+ description: "Optional. Tri-state outcome. When absent, derived from success (true→success, false→failed). 'partial' and 'blocked' must be declared explicitly. A declared non-success outcome suppresses the success-class checks even when success=true.",
227
+ },
228
+ blocker: {
229
+ type: "string",
230
+ description: "Optional. Flat blocker description, required only when outcome=blocked. Also feeds the user/agent gate classification. Max 500 chars.",
231
+ },
232
+ retroactiveClaims: {
233
+ type: "array",
234
+ items: {
235
+ type: "object",
236
+ properties: {
237
+ round: { type: "integer", minimum: 1 },
238
+ claim: { type: "string" },
239
+ },
240
+ required: ["round", "claim"],
241
+ additionalProperties: false,
242
+ },
243
+ description: "Optional. Claims that a PRIOR round satisfied a criterion. The runtime verifies them against the prior round's git evidence. Max 20 entries.",
244
+ },
245
+ drift_clarification: {
246
+ type: "string",
247
+ description: "Optional. Explain why this round's actions diverged from your previous round's next_action. Only required when the previous round was flagged for intent_drift or subgoal_drift. Mention concrete anchors: sub-goal IDs (sg-XXXXXXXX), constraint IDs (c-XXXXXXXX), criterion IDs (cr-XXXXXXXX), or file paths.",
248
+ },
249
+ prompt_requests: {
250
+ type: "object",
251
+ 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.",
252
+ properties: {
253
+ emphasize: {
254
+ type: "array",
255
+ items: { type: "string" },
256
+ 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.",
257
+ },
258
+ expand: {
259
+ type: "array",
260
+ items: {
261
+ type: "string",
262
+ enum: ["milestones", "sub_goals", "constraint_lifecycle", "agent_trust", "progress", "loop_synthesis"],
263
+ },
264
+ 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.",
265
+ },
266
+ confusion_points: {
267
+ type: "array",
268
+ items: { type: "string" },
269
+ 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.'",
270
+ },
271
+ },
272
+ },
273
+ },
161
274
  },
162
275
  },
163
- required: ["sessionId", "report"],
276
+ // v3.2.1: evaluation is optional at the schema level — the handler
277
+ // requires EITHER the evaluation parameter OR output containing a
278
+ // ---loopforge-eval block. Requiring it here made the embedded-block
279
+ // path dead code (input validation rejected it before the handler).
280
+ required: ["sessionId", "roundId"],
164
281
  },
165
282
  },
166
283
  {
167
284
  name: "loopforge_status",
168
- description: "Read session, loop health, or all persisted loops through one canonical status view.",
285
+ description: "Inspect loop state. view=session (default): session status, round identity, success trajectory and typed projection. view=loop: loop health (goal alignment, constraints, drift). view=all: summary of every persisted loop. view=audit: read-only end-of-loop verification audit (claims, gates, verdict, sequence integrity).",
169
286
  inputSchema: {
170
287
  type: "object",
171
288
  properties: {
172
- scope: { type: "string", enum: ["session", "loop", "all"] },
173
289
  sessionId: {
174
290
  type: "string",
175
- description: "Session ID returned by loopforge_start.",
291
+ description: "Session ID returned by loopforge_start (views session/audit).",
176
292
  },
177
- loopId: { type: "string" },
178
- detail: { type: "string", enum: ["summary", "full"] },
179
- },
180
- required: ["scope"],
181
- },
182
- },
183
- {
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.",
186
- inputSchema: {
187
- type: "object",
188
- properties: {
189
- sessionId: {
293
+ loopId: {
190
294
  type: "string",
191
- description: "Session ID returned by loopforge_start.",
295
+ description: "Loop ID (views loop/audit).",
192
296
  },
193
- report: {
194
- ...ROUND_REPORT_SCHEMA,
195
- description: "Corrected canonical report for the rejected attempt.",
297
+ view: {
298
+ type: "string",
299
+ enum: ["session", "loop", "all", "audit"],
300
+ description: "Which view to return. Defaults to session.",
196
301
  },
197
302
  },
198
- required: ["sessionId", "report"],
303
+ required: [],
199
304
  },
200
305
  },
201
306
  {
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.",
307
+ name: "loopforge_stop",
308
+ description: "Manually stop a loop session and return its final trajectory.",
204
309
  inputSchema: {
205
310
  type: "object",
206
311
  properties: {
207
312
  sessionId: {
208
313
  type: "string",
209
- description: "Session ID returned by loopforge_start.",
314
+ description: "Session ID to stop.",
210
315
  },
211
316
  },
212
317
  required: ["sessionId"],
213
318
  },
214
319
  },
215
320
  {
216
- name: "loopforge_history",
217
- description: "Read one persisted round or a deterministic event timeline.",
218
- inputSchema: {
219
- type: "object",
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"],
229
- },
230
- },
231
- {
232
- name: "loopforge_stop",
233
- description: "Manually stop a loop session and return its final trajectory.",
321
+ name: "loopforge_pause",
322
+ 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.",
234
323
  inputSchema: {
235
324
  type: "object",
236
325
  properties: {
237
326
  sessionId: {
238
327
  type: "string",
239
- description: "Session ID to stop.",
328
+ description: "Session ID to pause.",
240
329
  },
241
330
  },
242
331
  required: ["sessionId"],
243
332
  },
244
333
  },
245
334
  {
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.",
335
+ name: "loopforge_replay",
336
+ description: "Replay a completed or running loop session as an auditable round timeline.",
248
337
  inputSchema: {
249
338
  type: "object",
250
339
  properties: {
251
340
  sessionId: {
252
341
  type: "string",
253
- description: "Session ID to pause.",
342
+ description: "Session ID to replay.",
254
343
  },
255
344
  },
256
345
  required: ["sessionId"],
@@ -272,37 +361,42 @@ const TOOL_BASE_SCHEMAS = [
272
361
  },
273
362
  {
274
363
  name: "loopforge_gate_check",
275
- description: "Classify whether an action is an agent-resolvable gate or requires explicit user authority. Read-only.",
364
+ description: "v2.12: Classify a high-risk action or blocker text as a user gate (needs human authorization) or an agent gate (resolvable with evidence). Read-only — nothing is recorded. Use the returned gate id with loopforge_gate_resolve after a human decision.",
276
365
  inputSchema: {
277
366
  type: "object",
278
367
  properties: {
279
- action: GATE_ACTION_SCHEMA,
368
+ gateText: {
369
+ type: "string",
370
+ description: "The action or blocker text to classify, e.g. 'Deploy to production'.",
371
+ },
280
372
  },
281
- required: ["action"],
373
+ required: ["gateText"],
282
374
  },
283
375
  },
284
376
  {
285
377
  name: "loopforge_gate_resolve",
286
- description: "Record an explicit user decision for a previously classified gate.",
378
+ description: "v2.12: Record a human decision for a previously opened user gate. The gate id embeds the action hash — if the action text changed, the approval expires automatically. Read-only for agent gates (they are resolved by submitting evidence).",
287
379
  inputSchema: {
288
380
  type: "object",
289
381
  properties: {
290
- sessionId: { type: "string" },
291
- gateId: { type: "string" },
292
- action: GATE_ACTION_SCHEMA,
293
- approved: { type: "boolean" },
294
- note: { type: "string" },
382
+ sessionId: {
383
+ type: "string",
384
+ description: "Session ID whose loop owns the gate.",
385
+ },
386
+ gateId: {
387
+ type: "string",
388
+ description: "Gate id returned by loopforge_gate_check or recorded from a blocked round.",
389
+ },
390
+ approved: {
391
+ type: "boolean",
392
+ description: "Whether the human authorizes the action.",
393
+ },
394
+ note: {
395
+ type: "string",
396
+ description: "Optional human note recorded with the decision.",
397
+ },
295
398
  },
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" } },
305
- required: ["loopId"],
399
+ required: ["sessionId", "gateId", "approved"],
306
400
  },
307
401
  },
308
402
  ];
@@ -314,29 +408,34 @@ const ADVANCE_OUTPUT_SCHEMA = {
314
408
  round: { type: "number" },
315
409
  roundId: { type: ["string", "null"] },
316
410
  prompt: { type: ["string", "null"] },
317
- decision: { enum: ["accepted", "rejected", "backtracked", "stopped", "terminated", "errored"] },
318
- attempt: { type: "number" },
319
- stopReason: { type: "string" },
320
- stopDetail: { type: "string", description: "Human-readable context for why the loop stopped. Provides facts without prescribing a specific agent action." },
321
- level: { type: "string" },
322
- roundSuccess: { type: "boolean" },
411
+ stopReason: { type: ["string", "null"] },
412
+ stopDetail: { type: ["string", "null"], description: "Human-readable context for why the loop stopped. Provides facts without prescribing a specific agent action." },
413
+ level: { type: ["string", "null"] },
414
+ roundSuccess: { type: ["boolean", "null"] },
323
415
  enforcementAction: { enum: ["accept", "reject", "terminate", "backtrack"] },
324
- enforcementReason: { type: "string" },
325
- projection: { type: "object", additionalProperties: true },
326
- evidenceSummary: { type: "object", additionalProperties: true },
327
- stateHash: { type: "string" },
328
- promptHash: { type: "string" },
416
+ enforcementReason: { type: ["string", "null"] },
329
417
  warnings: { type: "array", items: { type: "string" } },
418
+ parseGaps: {
419
+ type: "array",
420
+ items: { type: "object", additionalProperties: true },
421
+ description: "v2.12: Field-level format diagnostics for tolerated evaluation issues.",
422
+ },
423
+ projection: {
424
+ type: "object",
425
+ additionalProperties: true,
426
+ description: "v2.12: Typed cognitive state projection (focus/todo/phase/delegation/handoff).",
427
+ },
330
428
  },
331
429
  additionalProperties: true,
332
430
  };
333
- const TOOL_OUTPUT_SCHEMAS = {
431
+ /** Declared structured-output contracts for every tool. The MCP server
432
+ * validates each handler's output against these before returning it —
433
+ * a mismatch is a contract bug, surfaced as a JSON-RPC error instead of
434
+ * silently shipping an output that violates the declared schema. */
435
+ export const TOOL_OUTPUT_SCHEMAS = {
334
436
  loopforge_start: ADVANCE_OUTPUT_SCHEMA,
335
437
  loopforge_next: ADVANCE_OUTPUT_SCHEMA,
336
- loopforge_retry: ADVANCE_OUTPUT_SCHEMA,
337
- loopforge_backtrack: ADVANCE_OUTPUT_SCHEMA,
338
438
  loopforge_resume: ADVANCE_OUTPUT_SCHEMA,
339
- loopforge_history: { type: "object", additionalProperties: true },
340
439
  loopforge_status: {
341
440
  type: "object",
342
441
  properties: {
@@ -373,17 +472,47 @@ const TOOL_OUTPUT_SCHEMAS = {
373
472
  },
374
473
  additionalProperties: true,
375
474
  },
376
- loopforge_gate_check: { type: "object", additionalProperties: true },
377
- loopforge_gate_resolve: { type: "object", additionalProperties: true },
378
- loopforge_audit: { type: "object", additionalProperties: true },
475
+ loopforge_replay: {
476
+ type: "object",
477
+ properties: {
478
+ error: { type: "string" },
479
+ sessionId: { type: "string" },
480
+ loopId: { type: "string" },
481
+ timeline: { type: "array", items: { type: "object", additionalProperties: true } },
482
+ },
483
+ additionalProperties: true,
484
+ },
485
+ loopforge_gate_check: {
486
+ type: "object",
487
+ properties: {
488
+ error: { type: "string" },
489
+ id: { type: "string" },
490
+ kind: { type: "string" },
491
+ question: { type: "string" },
492
+ blockedScope: { type: "array", items: { type: "string" } },
493
+ allowedWork: { type: "array", items: { type: "string" } },
494
+ problem: { type: "string" },
495
+ requiredEvidence: { type: "array", items: { type: "string" } },
496
+ suggestedResolution: { type: "string" },
497
+ },
498
+ additionalProperties: true,
499
+ },
500
+ loopforge_gate_resolve: {
501
+ type: "object",
502
+ properties: {
503
+ error: { type: "string" },
504
+ sessionId: { type: "string" },
505
+ loopId: { type: "string" },
506
+ gateId: { type: "string" },
507
+ approved: { type: "boolean" },
508
+ },
509
+ additionalProperties: true,
510
+ },
379
511
  };
380
512
  function closeObjectSchemas(schema) {
381
513
  const result = { ...schema };
382
514
  if (result.type === "object") {
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;
515
+ result.additionalProperties = false;
387
516
  if (isRecord(result.properties)) {
388
517
  result.properties = Object.fromEntries(Object.entries(result.properties).map(([key, value]) => [
389
518
  key,
@@ -411,12 +540,28 @@ export class ToolInputValidationError extends Error {
411
540
  this.name = "ToolInputValidationError";
412
541
  }
413
542
  }
543
+ function typeMatches(value, type) {
544
+ switch (type) {
545
+ case "object": return isRecord(value);
546
+ case "array": return Array.isArray(value);
547
+ case "string": return typeof value === "string";
548
+ case "boolean": return typeof value === "boolean";
549
+ case "number": return typeof value === "number" && Number.isFinite(value);
550
+ case "integer": return typeof value === "number" && Number.isInteger(value);
551
+ case "null": return value === null;
552
+ default: return true; // undeclared type — no constraint
553
+ }
554
+ }
414
555
  function validateSchema(value, schema, path) {
415
- const type = schema.type;
416
- if (type === "object") {
417
- if (!isRecord(value)) {
418
- throw new ToolInputValidationError(`${path} must be an object`);
419
- }
556
+ // v2.14: an explicitly-undefined optional field is the same as absent —
557
+ // output objects carry `key: undefined` after spreads of optional fields.
558
+ if (value === undefined)
559
+ return;
560
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
561
+ if (!types.some((type) => typeMatches(value, type))) {
562
+ throw new ToolInputValidationError(`${path} must be ${types.join(" or ")}`);
563
+ }
564
+ if (types.some((type) => type === "object") && isRecord(value)) {
420
565
  const properties = isRecord(schema.properties) ? schema.properties : {};
421
566
  const required = Array.isArray(schema.required)
422
567
  ? schema.required.filter((item) => typeof item === "string")
@@ -440,34 +585,23 @@ function validateSchema(value, schema, path) {
440
585
  }
441
586
  return;
442
587
  }
443
- if (type === "array") {
444
- if (!Array.isArray(value)) {
445
- throw new ToolInputValidationError(`${path} must be an array`);
446
- }
588
+ if (types.some((type) => type === "array") && Array.isArray(value)) {
447
589
  if (isRecord(schema.items)) {
448
590
  value.forEach((item, index) => validateSchema(item, schema.items, `${path}[${index}]`));
449
591
  }
450
592
  return;
451
593
  }
452
- if (type === "string" && typeof value !== "string") {
453
- throw new ToolInputValidationError(`${path} must be a string`);
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
- }
462
- if (type === "boolean" && typeof value !== "boolean") {
463
- throw new ToolInputValidationError(`${path} must be a boolean`);
464
- }
465
- if (type === "number" || type === "integer") {
466
- if (typeof value !== "number" ||
467
- !Number.isFinite(value) ||
468
- (type === "integer" && !Number.isInteger(value))) {
469
- throw new ToolInputValidationError(`${path} must be ${type === "integer" ? "an integer" : "a number"}`);
594
+ if (types.some((type) => type === "string") && typeof value === "string") {
595
+ // Enforce declared enums — the schemas advertise them, so validation
596
+ // must too (v2.12: view/gateText enums were previously unchecked).
597
+ if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
598
+ throw new ToolInputValidationError(`${path} must be one of: ${schema.enum.join(", ")}`);
470
599
  }
600
+ return;
601
+ }
602
+ if ((types.some((type) => type === "number") ||
603
+ types.some((type) => type === "integer")) &&
604
+ typeof value === "number") {
471
605
  if (typeof schema.minimum === "number" && value < schema.minimum) {
472
606
  throw new ToolInputValidationError(`${path} must be >= ${schema.minimum}`);
473
607
  }
@@ -482,54 +616,15 @@ export function validateToolInput(name, input) {
482
616
  throw new ToolInputValidationError(`Unknown tool: ${name}`);
483
617
  validateSchema(input, contract.inputSchema, "arguments");
484
618
  }
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);
619
+ /** Validate a handler's output against the tool's declared output schema.
620
+ * v2.14: the schemas were advertised in tools/list but never enforced —
621
+ * a mismatched output silently violated the declared contract. Throws
622
+ * ToolInputValidationError on mismatch. */
623
+ export function validateToolOutput(name, output) {
624
+ const schema = TOOL_OUTPUT_SCHEMAS[name];
625
+ if (!schema)
626
+ return;
627
+ validateSchema(output, schema, `output.${name}`);
533
628
  }
534
629
  export const TOOL_HANDLERS = {
535
630
  async loopforge_start(mgr, input) {
@@ -542,92 +637,112 @@ export const TOOL_HANDLERS = {
542
637
  constraints: Array.isArray(input.constraints)
543
638
  ? input.constraints
544
639
  : undefined,
545
- successCriteria: Array.isArray(input.successCriteria)
546
- ? input.successCriteria
547
- : undefined,
548
640
  };
549
641
  if (!startInput.task.trim()) {
550
- return { error: "task is required and must be non-empty", decision: "errored" };
642
+ return { error: "task is required and must be non-empty" };
551
643
  }
552
- const policyMaxRounds = getPolicy().engine.max_rounds;
644
+ // v2.14: entry-point validation — a bad maxRounds previously passed
645
+ // through and stopped the loop on the first round; an invalid loopId
646
+ // exploded later as an internal storage error instead of a clean
647
+ // argument error.
553
648
  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" };
649
+ (!Number.isInteger(startInput.maxRounds) || startInput.maxRounds < 1)) {
650
+ return { error: "maxRounds must be a positive integer" };
651
+ }
652
+ if (startInput.loopId !== undefined) {
653
+ try {
654
+ validateLoopId(startInput.loopId);
655
+ }
656
+ catch (error) {
657
+ return { error: `invalid loopId: ${error.message}` };
658
+ }
556
659
  }
557
660
  const result = await mgr.create(startInput);
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
- };
661
+ // v2.14: a same-process duplicate loopId surfaces as a clean argument
662
+ // error instead of a prompt-less "success" the client cannot act on.
663
+ if (typeof result.stopReason === "string" &&
664
+ result.stopReason.startsWith("loop_already_running:")) {
665
+ return { error: result.stopDetail ?? "loop already running" };
666
+ }
667
+ return { ...result };
565
668
  },
566
669
  async loopforge_next(mgr, input) {
567
670
  const sessionId = String(input.sessionId ?? "");
671
+ const output = String(input.output ?? "");
672
+ const rawEval = input.evaluation;
673
+ const roundId = input.roundId === undefined ? undefined : String(input.roundId);
568
674
  if (!sessionId)
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) });
675
+ return { error: "sessionId is required" };
676
+ // v3.0.1: every submission is anchored to the round it reports on. A
677
+ // missing roundId would silently re-process a stale submission against a
678
+ // later round — reject it here so the contract is enforced even when the
679
+ // handler is invoked directly (bypassing schema validation).
680
+ if (!roundId || !roundId.trim()) {
681
+ return { error: "roundId is required — pass the roundId from the most recent loopforge_start / loopforge_next / loopforge_resume / loopforge_status response" };
682
+ }
683
+ // Build SelfEvaluation from structured evaluation parameter
684
+ const preExtractedEval = rawEval ? buildSelfEvaluation(rawEval) : undefined;
685
+ // Require at least one of: evaluation parameter or output with embedded eval block
686
+ if (!preExtractedEval && !output.trim()) {
687
+ return { error: "Either evaluation parameter or output with ---loopforge-eval block is required" };
688
+ }
689
+ const result = await mgr.advance(sessionId, output, preExtractedEval, roundId);
690
+ // v2.12: Typed projection — runtime facts for the main agent, attached
691
+ // on every accepted/continued round (not on errors).
692
+ let projection = null;
693
+ if (result.enforcementAction !== "reject" && result.enforcementAction !== "terminate" &&
694
+ result.stopReason !== "session_not_found" && result.stopReason !== "stalled") {
695
+ projection = mgr.getProjection(sessionId);
696
+ }
697
+ // v2.12: Field-level diagnostics for tolerated format issues. The
698
+ // lenient parser still accepts the evaluation — the gaps are surfaced
699
+ // as structured warnings so the agent can correct its template.
700
+ if (rawEval) {
701
+ const gaps = collectSelfEvalGaps(rawEval);
702
+ if (gaps.length > 0) {
703
+ return {
704
+ ...result,
705
+ projection,
706
+ warnings: [...(result.warnings ?? []), ...gaps.map((g) => `[${g.field}] ${g.issue}`)],
707
+ parseGaps: gaps,
708
+ };
709
+ }
710
+ }
711
+ return projection ? { ...result, projection } : { ...result };
592
712
  },
593
713
  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() };
714
+ const view = input.view === "loop" || input.view === "all" || input.view === "audit"
715
+ ? input.view
716
+ : "session";
717
+ const sessionId = String(input.sessionId ?? "");
718
+ const loopId = String(input.loopId ?? "");
719
+ // v2.12: view=all — every persisted loop (formerly loopforge_list).
720
+ if (view === "all") {
721
+ return { sessions: mgr.list() };
601
722
  }
602
- if (scope === "loop") {
603
- if (hasSessionId)
604
- return { error: "scope=loop does not accept sessionId" };
605
- const loopId = String(input.loopId ?? "").trim();
723
+ // v2.12: view=audit — read-only verification audit.
724
+ if (view === "audit") {
606
725
  if (!loopId)
607
- return { error: "loopId is required for loop scope" };
726
+ return { error: "loopId is required for view=audit" };
727
+ const audit = mgr.getAudit(loopId);
728
+ if (!audit)
729
+ return { error: `no audit data found for loop "${loopId}"` };
730
+ return audit;
731
+ }
732
+ // v2.12: view=loop — loop health (formerly loopforge_health).
733
+ if (view === "loop") {
734
+ if (!loopId)
735
+ return { error: "loopId is required for view=loop" };
608
736
  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 };
737
+ return health ?? { error: `no health data for loop "${loopId}"` };
615
738
  }
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" };
620
- const sessionId = String(input.sessionId ?? "");
621
739
  if (!sessionId)
622
- return { error: "sessionId is required" };
740
+ return { error: "sessionId is required for view=session" };
623
741
  const session = mgr.get(sessionId);
624
742
  if (!session)
625
743
  return { error: `session not found: ${sessionId}` };
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 = {
744
+ const metrics = session.engine.getMetrics();
745
+ return {
631
746
  sessionId: session.sessionId,
632
747
  loopId: session.loopId,
633
748
  round: session.currentRound,
@@ -636,40 +751,10 @@ export const TOOL_HANDLERS = {
636
751
  status: session.status,
637
752
  successTrajectory: session.successTrajectory,
638
753
  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
- },
754
+ projection: mgr.getProjection(sessionId),
670
755
  metrics: {
671
756
  vaultWriteErrors: metrics.vaultWriteErrors,
672
- policy: mgr.getLoopPolicyMetrics(session.loopId),
757
+ policy: mgr.getPolicyMetrics(session.loopId),
673
758
  },
674
759
  };
675
760
  },
@@ -683,17 +768,7 @@ export const TOOL_HANDLERS = {
683
768
  const roundsCompleted = session.currentRound;
684
769
  const successTrajectory = [...session.successTrajectory];
685
770
  mgr.delete(sessionId);
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
- };
771
+ return { success: true, roundsCompleted, successTrajectory };
697
772
  },
698
773
  async loopforge_pause(mgr, input) {
699
774
  const sessionId = String(input.sessionId ?? "");
@@ -702,53 +777,44 @@ export const TOOL_HANDLERS = {
702
777
  const result = mgr.pause(sessionId);
703
778
  return { ...result };
704
779
  },
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 };
780
+ async loopforge_replay(mgr, input) {
781
+ const sessionId = String(input.sessionId ?? "");
782
+ if (!sessionId)
783
+ return { error: "sessionId is required" };
784
+ const session = mgr.get(sessionId);
785
+ if (!session)
786
+ return { error: `session not found: ${sessionId}` };
787
+ const timeline = mgr.replayTimeline(sessionId) ?? [];
788
+ return { sessionId, loopId: session.loopId, timeline };
724
789
  },
725
790
  async loopforge_resume(mgr, input) {
726
791
  const loopId = String(input.loopId ?? "");
727
792
  if (!loopId)
728
- return { error: "loopId is required", decision: "errored" };
793
+ return { error: "loopId is required" };
794
+ try {
795
+ validateLoopId(loopId);
796
+ }
797
+ catch (error) {
798
+ return { error: `invalid loopId: ${error.message}` };
799
+ }
729
800
  // Paused recovery must run first so the persisted status is atomically
730
801
  // changed back to running before a prompt is returned.
731
802
  let result = await mgr.unpause(loopId);
732
803
  if (!result) {
733
- result = await mgr.resume(loopId);
804
+ result = mgr.resume(loopId);
734
805
  }
735
806
  if (!result)
736
- return { error: `no saved session found for loop "${loopId}"`, decision: "errored" };
737
- return finalizeAdvanceOutput(mgr, { ...result });
807
+ return { error: `no saved session found for loop "${loopId}"` };
808
+ return { ...result };
738
809
  },
739
810
  async loopforge_gate_check(mgr, input) {
740
- if (!isRecord(input.action))
741
- return { error: "action descriptor is required" };
742
- return mgr.checkGate(input.action);
811
+ const gateText = String(input.gateText ?? "");
812
+ if (!gateText.trim())
813
+ return { error: "gateText is required" };
814
+ return mgr.checkGate(gateText);
743
815
  },
744
816
  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();
749
- if (!loopId)
750
- return { error: "loopId is required" };
751
- return mgr.audit(loopId);
817
+ return mgr.resolveGate(String(input.sessionId ?? ""), String(input.gateId ?? ""), input.approved === true, input.note === undefined ? undefined : String(input.note));
752
818
  },
753
819
  };
754
820
  //# sourceMappingURL=tools.js.map