loopforge 3.7.1 → 3.8.2
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.
- package/README.md +84 -130
- package/dist/audit.d.ts +53 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +106 -22
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +52 -67
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +134 -147
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +103 -4
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +13 -0
- package/dist/cognitive-facts.d.ts.map +1 -1
- package/dist/cognitive-facts.js +28 -13
- package/dist/cognitive-facts.js.map +1 -1
- package/dist/committed-round.d.ts +103 -21
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +188 -45
- package/dist/committed-round.js.map +1 -1
- package/dist/contract-items.d.ts +55 -0
- package/dist/contract-items.d.ts.map +1 -0
- package/dist/contract-items.js +180 -0
- package/dist/contract-items.js.map +1 -0
- package/dist/enforcement-gate.d.ts +20 -2
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +197 -386
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +53 -38
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts +4 -4
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +16 -14
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +85 -89
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +328 -184
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +76 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +127 -0
- package/dist/explain.js.map +1 -0
- package/dist/generate-schema.js +152 -16
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +40 -53
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +229 -553
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +0 -1
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +0 -4
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts.map +1 -1
- package/dist/loop-projection.js +3 -1
- package/dist/loop-projection.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +30 -21
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +47 -103
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +51 -7
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +39 -23
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +148 -163
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +89 -0
- package/dist/mcp/submission-boundary.d.ts.map +1 -0
- package/dist/mcp/submission-boundary.js +203 -0
- package/dist/mcp/submission-boundary.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +249 -128
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.js +2 -2
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +55 -93
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +149 -38
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +1 -32
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +282 -435
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -4
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +6 -7
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +365 -142
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +21 -66
- package/dist/protocol.js.map +1 -1
- package/dist/replay.js +2 -2
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +69 -57
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +104 -64
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +36 -16
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +166 -102
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +3 -5
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +0 -1
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +96 -0
- package/dist/round-facts.d.ts.map +1 -0
- package/dist/round-facts.js +216 -0
- package/dist/round-facts.js.map +1 -0
- package/dist/round-transaction.d.ts +19 -9
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +90 -22
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +87 -9
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +396 -27
- package/dist/self-eval.js.map +1 -1
- package/dist/subgoal-state.d.ts +77 -0
- package/dist/subgoal-state.d.ts.map +1 -0
- package/dist/subgoal-state.js +198 -0
- package/dist/subgoal-state.js.map +1 -0
- package/dist/token-utils.d.ts +52 -19
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +83 -72
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +14 -31
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +178 -496
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace.d.ts +9 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +17 -0
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +59 -83
- package/package.json +67 -67
- package/skills/perception/SKILL.md +218 -204
package/dist/mcp/tools.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* status is the unified inspection tool (view=session|loop|all|audit).
|
|
6
6
|
* Each handler receives SessionManager + parsed input, returns the output object.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import { parseSubGoalUpdates, validateCoreSelfEvaluation, validateSubGoalUpdatesShape, } from "../self-eval.js";
|
|
8
|
+
import { validateCoreSelfEvaluation } from "../self-eval.js";
|
|
10
9
|
import { isRecord } from "../token-utils.js";
|
|
11
10
|
import { getPolicy, validateLoopId } from "../policy.js";
|
|
11
|
+
import { deriveEvidenceCapability } from "../evidence-provider.js";
|
|
12
12
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
13
|
// Tool schemas (MCP JSON Schema format)
|
|
14
14
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -104,7 +104,7 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
104
104
|
description: "Optional. Sub-problems that surfaced during execution. The only " +
|
|
105
105
|
"sub-goal creation channel — sg-XXXXXXXX ID literals are rejected here.",
|
|
106
106
|
},
|
|
107
|
-
|
|
107
|
+
execution_report: {
|
|
108
108
|
type: "object",
|
|
109
109
|
description: "Optional. Structured record of what actually happened this round.",
|
|
110
110
|
properties: {
|
|
@@ -113,7 +113,7 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
113
113
|
items: { type: "string" },
|
|
114
114
|
description: "Files modified this round.",
|
|
115
115
|
},
|
|
116
|
-
|
|
116
|
+
tests_reported: {
|
|
117
117
|
type: "object",
|
|
118
118
|
description: "Test results from this round.",
|
|
119
119
|
properties: {
|
|
@@ -122,15 +122,31 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
122
122
|
skipped: { type: "integer", minimum: 0, description: "Number of skipped tests." },
|
|
123
123
|
},
|
|
124
124
|
},
|
|
125
|
-
|
|
125
|
+
criterion_claims: {
|
|
126
126
|
type: "array",
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
description: "v3.8: advisory claims about objective criteria. LENIENT — unknown or malformed ids are dropped with a warning, never a rejection.",
|
|
128
|
+
items: {
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: {
|
|
131
|
+
criterion_id: { type: "string", description: "cr-XXXXXXXX criterion id (free text allowed)." },
|
|
132
|
+
outcome: { type: "string", enum: ["met", "remaining"] },
|
|
133
|
+
},
|
|
134
|
+
required: ["criterion_id", "outcome"],
|
|
135
|
+
additionalProperties: false,
|
|
136
|
+
},
|
|
129
137
|
},
|
|
130
|
-
|
|
138
|
+
contract_item_claims: {
|
|
131
139
|
type: "array",
|
|
132
|
-
|
|
133
|
-
|
|
140
|
+
description: "Claims about items of the ACTIVE contract, cited by the rci-XXXXXXXX id the prompt rendered. LENIENT at declaration: an unknown/malformed id is reported as contract_invalid so you can correct the payload and resubmit the same roundId. A claim never verifies anything by itself — the runtime only marks an item verified when its bound commands are observed passing.",
|
|
141
|
+
items: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
item_id: { type: "string", description: "rci-XXXXXXXX id of an item of the ACTIVE contract." },
|
|
145
|
+
outcome: { type: "string", enum: ["met", "remaining"], description: "Your claim for that item this round." },
|
|
146
|
+
},
|
|
147
|
+
required: ["item_id", "outcome"],
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
},
|
|
134
150
|
},
|
|
135
151
|
progress_estimate: {
|
|
136
152
|
type: "number",
|
|
@@ -194,10 +210,6 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
194
210
|
type: "string",
|
|
195
211
|
description: "Optional. Human-readable label for this checkpoint (e.g. 'core-functions-complete'). Only meaningful when compression_checkpoint is true.",
|
|
196
212
|
},
|
|
197
|
-
next_action: {
|
|
198
|
-
type: "string",
|
|
199
|
-
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.",
|
|
200
|
-
},
|
|
201
213
|
subgoal_updates: {
|
|
202
214
|
type: "array",
|
|
203
215
|
maxItems: 20,
|
|
@@ -265,10 +277,6 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
265
277
|
},
|
|
266
278
|
description: "Optional. Claims that a PRIOR round satisfied a criterion. The runtime verifies them against the prior round's git evidence. Max 20 entries.",
|
|
267
279
|
},
|
|
268
|
-
drift_clarification: {
|
|
269
|
-
type: "string",
|
|
270
|
-
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.",
|
|
271
|
-
},
|
|
272
280
|
prompt_requests: {
|
|
273
281
|
type: "object",
|
|
274
282
|
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.",
|
|
@@ -276,7 +284,7 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
276
284
|
emphasize: {
|
|
277
285
|
type: "array",
|
|
278
286
|
items: { type: "string" },
|
|
279
|
-
description: "Constraints, discoveries, or decisions that need emphasis in the next prompt.
|
|
287
|
+
description: "Constraints, discoveries, or decisions that need emphasis in the next prompt. Each entry is matched against active state items by STABLE ID (c-/cr-/sg-XXXXXXXX) or by exact text after normalization — a paraphrase matches nothing and renders nothing. Matched items appear in a 'Critical Context' section. Max 5 entries. Pure reordering — no token overhead.",
|
|
280
288
|
},
|
|
281
289
|
confusion_points: {
|
|
282
290
|
type: "array",
|
|
@@ -287,29 +295,49 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
287
295
|
},
|
|
288
296
|
round_contract: {
|
|
289
297
|
type: "object",
|
|
290
|
-
description: "Optional. Proposal for the NEXT round's contract (v3.
|
|
298
|
+
description: "Optional. Proposal for the NEXT round's contract (v3.8 item model). Declared here, it becomes the ACTIVE contract (rendered as the Current Task) only after this round commits. It closes when EVERY item is machine-verified (its bound commands observed passing, untampered, with the declared config), or when a round reports outcome=blocked. While it is open, restate it unchanged here — a different proposal is ignored. DECLARATION IS STRICT: an item with no bound command, one naming an unknown/disabled/non-after-capable command, a scope entry leaving the workspace, a malformed reference, or a count over the declared limits returns contract_invalid and the round may be retried with the same roundId.",
|
|
291
299
|
properties: {
|
|
292
300
|
work_item: {
|
|
293
301
|
type: "string",
|
|
294
302
|
description: "Optional. Round focus, one line. Rendered as the active contract's Current Task first line. Max 200 chars.",
|
|
295
303
|
},
|
|
296
|
-
|
|
297
|
-
type: "array",
|
|
298
|
-
items: { type: "string" },
|
|
299
|
-
description: "Completion conditions: cr-XXXXXXXX criterion IDs or free text. Satisfied = list in success_criteria_met; still open = list in success_criteria_remaining. Max 20 items.",
|
|
300
|
-
},
|
|
301
|
-
verification_plan: {
|
|
304
|
+
items: {
|
|
302
305
|
type: "array",
|
|
303
|
-
|
|
304
|
-
|
|
306
|
+
description: "Required, at least one (max 20). Each item is a verification unit bound to evidence commands. The runtime derives a stable rci-XXXXXXXX id per item and renders it in the prompt — cite that id in execution_report.contract_item_claims.",
|
|
307
|
+
items: {
|
|
308
|
+
type: "object",
|
|
309
|
+
properties: {
|
|
310
|
+
description: {
|
|
311
|
+
type: "string",
|
|
312
|
+
description: "What must be true for this item. Max 200 chars.",
|
|
313
|
+
},
|
|
314
|
+
criterion_refs: {
|
|
315
|
+
type: "array",
|
|
316
|
+
items: { type: "string" },
|
|
317
|
+
description: "Optional. cr-XXXXXXXX criterion ids this item is evidence for (free text allowed). Max 20.",
|
|
318
|
+
},
|
|
319
|
+
subgoal_refs: {
|
|
320
|
+
type: "array",
|
|
321
|
+
items: { type: "string" },
|
|
322
|
+
description: "Optional. sg-XXXXXXXX sub-goals THIS ITEM is evidence for. Must name sub-goals that exist (or that this same submission creates via emerged_subtasks). Max 20 ids. A machine-verified item produces the derived verified-subgoal fact for exactly the ids it names here.",
|
|
323
|
+
},
|
|
324
|
+
verify_with: {
|
|
325
|
+
type: "array",
|
|
326
|
+
items: { type: "string" },
|
|
327
|
+
description: "Required, at least one. Names of configured, enabled, after-capable evidence.commands (loop_policy.json) that verify this item. Unknown or disabled names are rejected.",
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
required: ["description", "verify_with"],
|
|
331
|
+
additionalProperties: false,
|
|
332
|
+
},
|
|
305
333
|
},
|
|
306
334
|
scope: {
|
|
307
335
|
type: "array",
|
|
308
336
|
items: { type: "string" },
|
|
309
|
-
description: "Files/directories this round may touch (workspace-relative; './' and trailing slashes accepted). Out-of-scope git changes trigger round_scope_drift. Max 50 items.",
|
|
337
|
+
description: "Files/directories this round may touch (workspace-relative; './' and trailing slashes accepted; must stay inside the workspace). Out-of-scope git changes trigger round_scope_drift, which is a machine fact and is not waivable by explanation. Max 50 items.",
|
|
310
338
|
},
|
|
311
339
|
},
|
|
312
|
-
required: ["
|
|
340
|
+
required: ["items", "scope"],
|
|
313
341
|
},
|
|
314
342
|
},
|
|
315
343
|
},
|
|
@@ -333,8 +361,12 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
333
361
|
},
|
|
334
362
|
view: {
|
|
335
363
|
type: "string",
|
|
336
|
-
enum: ["session", "loop", "all", "audit"],
|
|
337
|
-
description: "Which view to return. Defaults to session.",
|
|
364
|
+
enum: ["session", "loop", "all", "audit", "explain"],
|
|
365
|
+
description: "Which view to return. Defaults to session. explain (v3.8) is the read-only per-round \"why\" view over committed facts.",
|
|
366
|
+
},
|
|
367
|
+
round: {
|
|
368
|
+
type: "number",
|
|
369
|
+
description: "view=explain only: return that single round's \"why\" view. Omit for the whole timeline.",
|
|
338
370
|
},
|
|
339
371
|
},
|
|
340
372
|
required: [],
|
|
@@ -493,7 +525,8 @@ const TOOL_BASE_SCHEMAS = [
|
|
|
493
525
|
const ADVANCE_OUTPUT_SCHEMA = {
|
|
494
526
|
type: "object",
|
|
495
527
|
properties: {
|
|
496
|
-
|
|
528
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
529
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
497
530
|
details: { type: "object", additionalProperties: true },
|
|
498
531
|
sessionId: { type: "string" },
|
|
499
532
|
round: { type: "number" },
|
|
@@ -525,7 +558,8 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
525
558
|
loopforge_status: {
|
|
526
559
|
type: "object",
|
|
527
560
|
properties: {
|
|
528
|
-
|
|
561
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
562
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
529
563
|
sessionId: { type: "string" },
|
|
530
564
|
loopId: { type: "string" },
|
|
531
565
|
round: { type: "number" },
|
|
@@ -545,7 +579,8 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
545
579
|
loopforge_stop: {
|
|
546
580
|
type: "object",
|
|
547
581
|
properties: {
|
|
548
|
-
|
|
582
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
583
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
549
584
|
success: { type: "boolean" },
|
|
550
585
|
roundsCompleted: { type: "number" },
|
|
551
586
|
successTrajectory: { type: "array", items: { type: "boolean" } },
|
|
@@ -555,7 +590,8 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
555
590
|
loopforge_pause: {
|
|
556
591
|
type: "object",
|
|
557
592
|
properties: {
|
|
558
|
-
|
|
593
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
594
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
559
595
|
sessionId: { type: "string" },
|
|
560
596
|
round: { type: "number" },
|
|
561
597
|
status: { type: "string" },
|
|
@@ -565,7 +601,8 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
565
601
|
loopforge_replay: {
|
|
566
602
|
type: "object",
|
|
567
603
|
properties: {
|
|
568
|
-
|
|
604
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
605
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
569
606
|
sessionId: { type: "string" },
|
|
570
607
|
loopId: { type: "string" },
|
|
571
608
|
timeline: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
@@ -575,7 +612,8 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
575
612
|
loopforge_gate_check: {
|
|
576
613
|
type: "object",
|
|
577
614
|
properties: {
|
|
578
|
-
|
|
615
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
616
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
579
617
|
sessionId: { type: "string" },
|
|
580
618
|
gateId: { type: "string" },
|
|
581
619
|
risk: { type: "string", enum: ["low", "high", "unknown"] },
|
|
@@ -591,7 +629,8 @@ export const TOOL_OUTPUT_SCHEMAS = {
|
|
|
591
629
|
loopforge_gate_resolve: {
|
|
592
630
|
type: "object",
|
|
593
631
|
properties: {
|
|
594
|
-
|
|
632
|
+
ok: { type: "boolean", description: "v3.8: uniform result envelope — false means the error object describes a rejected call." },
|
|
633
|
+
error: { type: "object", additionalProperties: true, description: "v3.8 ToolError: { code, message, retryable, sessionId?, roundId?, details? }" },
|
|
595
634
|
sessionId: { type: "string" },
|
|
596
635
|
loopId: { type: "string" },
|
|
597
636
|
gateId: { type: "string" },
|
|
@@ -772,6 +811,16 @@ export function validateToolOutput(name, output) {
|
|
|
772
811
|
return;
|
|
773
812
|
validateSchema(output, schema, `output.${name}`);
|
|
774
813
|
}
|
|
814
|
+
/** v3.8: capability warnings for the current policy, merged with the compile's
|
|
815
|
+
* own warnings. Derived through the SAME function `RoundDriver.prepare()`
|
|
816
|
+
* uses, so the start/resume banner and the prepared round cannot tell two
|
|
817
|
+
* different stories about what the runtime can observe. Never persisted. */
|
|
818
|
+
function capabilityWarningList(existing) {
|
|
819
|
+
return [...new Set([
|
|
820
|
+
...(existing ?? []),
|
|
821
|
+
...deriveEvidenceCapability(getPolicy()).warnings,
|
|
822
|
+
])];
|
|
823
|
+
}
|
|
775
824
|
export const TOOL_HANDLERS = {
|
|
776
825
|
async loopforge_start(mgr, input) {
|
|
777
826
|
const startInput = {
|
|
@@ -785,7 +834,10 @@ export const TOOL_HANDLERS = {
|
|
|
785
834
|
: undefined,
|
|
786
835
|
};
|
|
787
836
|
if (!startInput.task.trim()) {
|
|
788
|
-
return {
|
|
837
|
+
return {
|
|
838
|
+
error: "invalid_argument",
|
|
839
|
+
errorMessage: "task is required and must be non-empty",
|
|
840
|
+
};
|
|
789
841
|
}
|
|
790
842
|
// v2.14: entry-point validation — a bad maxRounds previously passed
|
|
791
843
|
// through and stopped the loop on the first round; an invalid loopId
|
|
@@ -793,92 +845,86 @@ export const TOOL_HANDLERS = {
|
|
|
793
845
|
// argument error.
|
|
794
846
|
if (startInput.maxRounds !== undefined &&
|
|
795
847
|
(!Number.isInteger(startInput.maxRounds) || startInput.maxRounds < 1)) {
|
|
796
|
-
return {
|
|
848
|
+
return {
|
|
849
|
+
error: "invalid_argument",
|
|
850
|
+
errorMessage: "maxRounds must be a positive integer",
|
|
851
|
+
};
|
|
797
852
|
}
|
|
798
853
|
if (startInput.loopId !== undefined) {
|
|
799
854
|
try {
|
|
800
855
|
validateLoopId(startInput.loopId);
|
|
801
856
|
}
|
|
802
857
|
catch (error) {
|
|
803
|
-
return {
|
|
858
|
+
return {
|
|
859
|
+
error: "invalid_argument",
|
|
860
|
+
errorMessage: `invalid loopId: ${error.message}`,
|
|
861
|
+
};
|
|
804
862
|
}
|
|
805
863
|
}
|
|
806
|
-
|
|
864
|
+
// A malformed loop_policy.json is a configuration defect, not a bad
|
|
865
|
+
// argument — it must not escape as an opaque JSON-RPC internal error.
|
|
866
|
+
let result;
|
|
867
|
+
try {
|
|
868
|
+
result = await mgr.create(startInput);
|
|
869
|
+
}
|
|
870
|
+
catch (error) {
|
|
871
|
+
return {
|
|
872
|
+
error: "policy_invalid",
|
|
873
|
+
errorMessage: `the runtime policy could not be loaded: ${error.message}`,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
807
876
|
// v2.14: a same-process duplicate loopId surfaces as a clean argument
|
|
808
877
|
// error instead of a prompt-less "success" the client cannot act on.
|
|
809
878
|
if (typeof result.stopReason === "string" &&
|
|
810
879
|
result.stopReason.startsWith("loop_already_running:")) {
|
|
811
|
-
return {
|
|
880
|
+
return {
|
|
881
|
+
error: "loop_already_running",
|
|
882
|
+
errorMessage: result.stopDetail ?? "loop already running",
|
|
883
|
+
};
|
|
812
884
|
}
|
|
813
|
-
|
|
885
|
+
// v3.8: the static capability warning — a loop with no provider or no
|
|
886
|
+
// enabled command can still run; its success claims are recorded as
|
|
887
|
+
// insufficient instead of verified, and the agent is told so up front.
|
|
888
|
+
const capabilityWarnings = capabilityWarningList(result.warnings);
|
|
889
|
+
return capabilityWarnings.length > 0
|
|
890
|
+
? { ...result, warnings: capabilityWarnings }
|
|
891
|
+
: { ...result };
|
|
814
892
|
},
|
|
815
893
|
async loopforge_next(mgr, input) {
|
|
816
894
|
const sessionId = String(input.sessionId ?? "");
|
|
817
895
|
const output = String(input.output ?? "");
|
|
818
896
|
const rawEval = input.evaluation;
|
|
819
897
|
const roundId = input.roundId === undefined ? undefined : String(input.roundId);
|
|
820
|
-
if (!sessionId)
|
|
821
|
-
return { error: "sessionId is required" };
|
|
898
|
+
if (!sessionId) {
|
|
899
|
+
return { error: "invalid_argument", errorMessage: "sessionId is required" };
|
|
900
|
+
}
|
|
822
901
|
// v3.0.1: every submission is anchored to the round it reports on. A
|
|
823
902
|
// missing roundId would silently re-process a stale submission against a
|
|
824
903
|
// later round — reject it here so the contract is enforced even when the
|
|
825
904
|
// handler is invoked directly (bypassing schema validation).
|
|
826
905
|
if (!roundId || !roundId.trim()) {
|
|
827
|
-
return { error: "roundId is required — pass the roundId from the most recent loopforge_start / loopforge_next / loopforge_resume / loopforge_status response" };
|
|
828
|
-
}
|
|
829
|
-
if (!rawEval || typeof rawEval !== "object" || Array.isArray(rawEval)) {
|
|
830
906
|
return {
|
|
831
|
-
error: "
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
invalid: [{ field: "evaluation", expected: "object" }],
|
|
835
|
-
},
|
|
836
|
-
sessionId,
|
|
837
|
-
roundId,
|
|
907
|
+
error: "round_id_required",
|
|
908
|
+
errorMessage: "roundId is required — pass the roundId from the most recent " +
|
|
909
|
+
"loopforge_start / loopforge_next / loopforge_resume / loopforge_status response",
|
|
838
910
|
};
|
|
839
911
|
}
|
|
840
|
-
|
|
841
|
-
|
|
912
|
+
// v3.8.1: the submission boundary belongs to the RUNTIME, not to this
|
|
913
|
+
// transport. `mgr.advance` applies all of it — object shape, core fields,
|
|
914
|
+
// subgoal_updates structure and references, and the strict Round Contract
|
|
915
|
+
// declaration/claims boundary — before touching any state, and reports a
|
|
916
|
+
// refusal through `submissionError`. This handler only maps that onto the
|
|
917
|
+
// tool envelope, so a non-MCP caller gets exactly the same strictness.
|
|
918
|
+
const result = await mgr.advance(sessionId, output, rawEval, roundId);
|
|
919
|
+
if (result.submissionError) {
|
|
842
920
|
return {
|
|
843
|
-
error:
|
|
844
|
-
|
|
921
|
+
error: result.submissionError.code,
|
|
922
|
+
errorMessage: result.stopDetail ?? "the submission was rejected",
|
|
923
|
+
details: result.submissionError.details,
|
|
845
924
|
sessionId,
|
|
846
925
|
roundId,
|
|
847
926
|
};
|
|
848
927
|
}
|
|
849
|
-
// v3.7.1: subgoal_updates is a strict structural boundary. Shape errors
|
|
850
|
-
// (bad entries, sg- literals in the creation channel) and referential
|
|
851
|
-
// errors (unknown / terminal / illegal transitions, checked against the
|
|
852
|
-
// compiled sub-goal set) return evaluation_invalid BEFORE advance — no
|
|
853
|
-
// state change, no gates, no rejection counters; retry same roundId.
|
|
854
|
-
const subgoalErrors = validateSubGoalUpdatesShape(rawEval);
|
|
855
|
-
if (subgoalErrors.length > 0) {
|
|
856
|
-
return {
|
|
857
|
-
error: "evaluation_invalid",
|
|
858
|
-
details: { missing: [], invalid: [], subgoal_errors: subgoalErrors },
|
|
859
|
-
sessionId,
|
|
860
|
-
roundId,
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
const updates = parseSubGoalUpdates(rawEval.subgoal_updates);
|
|
864
|
-
if (updates.length > 0) {
|
|
865
|
-
const emerged = Array.isArray(rawEval.emerged_subtasks)
|
|
866
|
-
? rawEval.emerged_subtasks
|
|
867
|
-
.filter((v) => typeof v === "string")
|
|
868
|
-
.map((s) => s.slice(0, 500))
|
|
869
|
-
: [];
|
|
870
|
-
const referentialErrors = mgr.preflightSubGoalUpdates(sessionId, roundId, updates, emerged);
|
|
871
|
-
if (referentialErrors.length > 0) {
|
|
872
|
-
return {
|
|
873
|
-
error: "evaluation_invalid",
|
|
874
|
-
details: { missing: [], invalid: [], subgoal_errors: referentialErrors },
|
|
875
|
-
sessionId,
|
|
876
|
-
roundId,
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
const preExtractedEval = buildSelfEvaluation(rawEval);
|
|
881
|
-
const result = await mgr.advance(sessionId, output, preExtractedEval, roundId);
|
|
882
928
|
// v2.12: Typed projection — runtime facts for the main agent, attached
|
|
883
929
|
// on every accepted/continued round (not on errors).
|
|
884
930
|
let projection = null;
|
|
@@ -889,7 +935,8 @@ export const TOOL_HANDLERS = {
|
|
|
889
935
|
return projection ? { ...result, projection } : { ...result };
|
|
890
936
|
},
|
|
891
937
|
async loopforge_status(mgr, input) {
|
|
892
|
-
const view = input.view === "loop" || input.view === "all" ||
|
|
938
|
+
const view = input.view === "loop" || input.view === "all" ||
|
|
939
|
+
input.view === "audit" || input.view === "explain"
|
|
893
940
|
? input.view
|
|
894
941
|
: "session";
|
|
895
942
|
const sessionId = String(input.sessionId ?? "");
|
|
@@ -900,25 +947,55 @@ export const TOOL_HANDLERS = {
|
|
|
900
947
|
}
|
|
901
948
|
// v2.12: view=audit — read-only verification audit.
|
|
902
949
|
if (view === "audit") {
|
|
903
|
-
if (!loopId)
|
|
904
|
-
return { error: "loopId is required for view=audit" };
|
|
950
|
+
if (!loopId) {
|
|
951
|
+
return { error: "invalid_argument", errorMessage: "loopId is required for view=audit" };
|
|
952
|
+
}
|
|
905
953
|
const audit = mgr.getAudit(loopId);
|
|
906
|
-
if (!audit)
|
|
907
|
-
return {
|
|
954
|
+
if (!audit) {
|
|
955
|
+
return {
|
|
956
|
+
error: "state_unavailable",
|
|
957
|
+
errorMessage: `no audit data found for loop "${loopId}"`,
|
|
958
|
+
sessionId,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
908
961
|
return audit;
|
|
909
962
|
}
|
|
963
|
+
// v3.8: view=explain — the read-only per-round "why" view.
|
|
964
|
+
if (view === "explain") {
|
|
965
|
+
if (!loopId) {
|
|
966
|
+
return { error: "invalid_argument", errorMessage: "loopId is required for view=explain" };
|
|
967
|
+
}
|
|
968
|
+
const roundValue = input.round;
|
|
969
|
+
const round = typeof roundValue === "number" && Number.isInteger(roundValue) && roundValue >= 1
|
|
970
|
+
? roundValue
|
|
971
|
+
: undefined;
|
|
972
|
+
return mgr.getExplain(loopId, round);
|
|
973
|
+
}
|
|
910
974
|
// v2.12: view=loop — loop health (formerly loopforge_health).
|
|
911
975
|
if (view === "loop") {
|
|
912
|
-
if (!loopId)
|
|
913
|
-
return { error: "loopId is required for view=loop" };
|
|
976
|
+
if (!loopId) {
|
|
977
|
+
return { error: "invalid_argument", errorMessage: "loopId is required for view=loop" };
|
|
978
|
+
}
|
|
914
979
|
const health = mgr.getHealth(loopId);
|
|
915
|
-
return health ?? {
|
|
980
|
+
return health ?? {
|
|
981
|
+
error: "state_unavailable",
|
|
982
|
+
errorMessage: `no health data for loop "${loopId}"`,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
if (!sessionId) {
|
|
986
|
+
return {
|
|
987
|
+
error: "invalid_argument",
|
|
988
|
+
errorMessage: "sessionId is required for view=session",
|
|
989
|
+
};
|
|
916
990
|
}
|
|
917
|
-
if (!sessionId)
|
|
918
|
-
return { error: "sessionId is required for view=session" };
|
|
919
991
|
const session = mgr.get(sessionId);
|
|
920
|
-
if (!session)
|
|
921
|
-
return {
|
|
992
|
+
if (!session) {
|
|
993
|
+
return {
|
|
994
|
+
error: "session_not_found",
|
|
995
|
+
errorMessage: `session not found: ${sessionId} (process restarted?) — pass loopId for view=audit/loop, or loopforge_resume`,
|
|
996
|
+
sessionId,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
922
999
|
const metrics = session.engine.getMetrics();
|
|
923
1000
|
return {
|
|
924
1001
|
sessionId: session.sessionId,
|
|
@@ -933,6 +1010,11 @@ export const TOOL_HANDLERS = {
|
|
|
933
1010
|
// verification gates derive the same value from the same committed
|
|
934
1011
|
// evals via the shared round-contract walker).
|
|
935
1012
|
activeContract: mgr.getActiveContract(sessionId),
|
|
1013
|
+
// v3.8: the ONE capability fact — the static half the compile hashes
|
|
1014
|
+
// (what verification is possible) plus the live half rendered from the
|
|
1015
|
+
// last baseline. `available` is provider-registry state: reported here
|
|
1016
|
+
// and by `doctor`, never hashed.
|
|
1017
|
+
capability: deriveEvidenceCapability(getPolicy(), session.evidenceBaseline ?? []),
|
|
936
1018
|
projection: mgr.getProjection(sessionId),
|
|
937
1019
|
metrics: {
|
|
938
1020
|
vaultWriteErrors: metrics.vaultWriteErrors,
|
|
@@ -947,11 +1029,17 @@ export const TOOL_HANDLERS = {
|
|
|
947
1029
|
},
|
|
948
1030
|
async loopforge_stop(mgr, input) {
|
|
949
1031
|
const sessionId = String(input.sessionId ?? "");
|
|
950
|
-
if (!sessionId)
|
|
951
|
-
return { error: "sessionId is required" };
|
|
1032
|
+
if (!sessionId) {
|
|
1033
|
+
return { error: "invalid_argument", errorMessage: "sessionId is required" };
|
|
1034
|
+
}
|
|
952
1035
|
const session = mgr.get(sessionId);
|
|
953
|
-
if (!session)
|
|
954
|
-
return {
|
|
1036
|
+
if (!session) {
|
|
1037
|
+
return {
|
|
1038
|
+
error: "session_not_found",
|
|
1039
|
+
errorMessage: `session not found: ${sessionId}`,
|
|
1040
|
+
sessionId,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
955
1043
|
const roundsCompleted = session.currentRound;
|
|
956
1044
|
const successTrajectory = [...session.successTrajectory];
|
|
957
1045
|
mgr.delete(sessionId);
|
|
@@ -959,8 +1047,9 @@ export const TOOL_HANDLERS = {
|
|
|
959
1047
|
},
|
|
960
1048
|
async loopforge_pause(mgr, input) {
|
|
961
1049
|
const sessionId = String(input.sessionId ?? "");
|
|
962
|
-
if (!sessionId)
|
|
963
|
-
return { error: "sessionId is required" };
|
|
1050
|
+
if (!sessionId) {
|
|
1051
|
+
return { error: "invalid_argument", errorMessage: "sessionId is required" };
|
|
1052
|
+
}
|
|
964
1053
|
const result = mgr.pause(sessionId);
|
|
965
1054
|
return { ...result };
|
|
966
1055
|
},
|
|
@@ -968,7 +1057,11 @@ export const TOOL_HANDLERS = {
|
|
|
968
1057
|
const sessionId = String(input.sessionId ?? "");
|
|
969
1058
|
const loopId = String(input.loopId ?? "");
|
|
970
1059
|
if (!sessionId && !loopId) {
|
|
971
|
-
return {
|
|
1060
|
+
return {
|
|
1061
|
+
error: "invalid_argument",
|
|
1062
|
+
errorMessage: "sessionId or loopId is required — pass loopId to replay a loop " +
|
|
1063
|
+
"after a restart (no live session)",
|
|
1064
|
+
};
|
|
972
1065
|
}
|
|
973
1066
|
// Live session first: it disambiguates re-used loopIds and the session
|
|
974
1067
|
// registry is the cheapest lookup.
|
|
@@ -981,27 +1074,39 @@ export const TOOL_HANDLERS = {
|
|
|
981
1074
|
// v3.3.1: session not in memory (restart). Fall through to the vault
|
|
982
1075
|
// path when a loopId was also provided; otherwise say so plainly.
|
|
983
1076
|
if (!loopId) {
|
|
984
|
-
return {
|
|
1077
|
+
return {
|
|
1078
|
+
error: "session_not_found",
|
|
1079
|
+
errorMessage: `session not found: ${sessionId} (process restarted?) — ` +
|
|
1080
|
+
"pass loopId to replay from the vault",
|
|
1081
|
+
sessionId,
|
|
1082
|
+
};
|
|
985
1083
|
}
|
|
986
1084
|
}
|
|
987
1085
|
// Vault-direct replay — the timeline lives in the committed round
|
|
988
1086
|
// documents, not in the session registry.
|
|
989
1087
|
const timeline = mgr.replayByLoop(loopId);
|
|
990
1088
|
if (!timeline) {
|
|
991
|
-
return {
|
|
1089
|
+
return {
|
|
1090
|
+
error: "state_unavailable",
|
|
1091
|
+
errorMessage: `no committed rounds found for loop "${loopId}"`,
|
|
1092
|
+
};
|
|
992
1093
|
}
|
|
993
1094
|
// sessionId omitted on the vault path (its output schema is string-only).
|
|
994
1095
|
return { ...(sessionId ? { sessionId } : {}), loopId, timeline };
|
|
995
1096
|
},
|
|
996
1097
|
async loopforge_resume(mgr, input) {
|
|
997
1098
|
const loopId = String(input.loopId ?? "");
|
|
998
|
-
if (!loopId)
|
|
999
|
-
return { error: "loopId is required" };
|
|
1099
|
+
if (!loopId) {
|
|
1100
|
+
return { error: "invalid_argument", errorMessage: "loopId is required" };
|
|
1101
|
+
}
|
|
1000
1102
|
try {
|
|
1001
1103
|
validateLoopId(loopId);
|
|
1002
1104
|
}
|
|
1003
1105
|
catch (error) {
|
|
1004
|
-
return {
|
|
1106
|
+
return {
|
|
1107
|
+
error: "invalid_argument",
|
|
1108
|
+
errorMessage: `invalid loopId: ${error.message}`,
|
|
1109
|
+
};
|
|
1005
1110
|
}
|
|
1006
1111
|
// Paused recovery must run first so the persisted status is atomically
|
|
1007
1112
|
// changed back to running before a prompt is returned.
|
|
@@ -1009,8 +1114,12 @@ export const TOOL_HANDLERS = {
|
|
|
1009
1114
|
if (!result) {
|
|
1010
1115
|
result = await mgr.resume(loopId);
|
|
1011
1116
|
}
|
|
1012
|
-
if (!result)
|
|
1013
|
-
return {
|
|
1117
|
+
if (!result) {
|
|
1118
|
+
return {
|
|
1119
|
+
error: "state_unavailable",
|
|
1120
|
+
errorMessage: `no saved session found for loop "${loopId}"`,
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1014
1123
|
return { ...result };
|
|
1015
1124
|
},
|
|
1016
1125
|
async loopforge_gate_check(mgr, input) {
|
|
@@ -1020,12 +1129,19 @@ export const TOOL_HANDLERS = {
|
|
|
1020
1129
|
const sessionId = String(input.sessionId ?? "");
|
|
1021
1130
|
const roundId = String(input.roundId ?? "");
|
|
1022
1131
|
const action = input.action;
|
|
1023
|
-
if (!sessionId)
|
|
1024
|
-
return { error: "sessionId is required" };
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1132
|
+
if (!sessionId) {
|
|
1133
|
+
return { error: "invalid_argument", errorMessage: "sessionId is required" };
|
|
1134
|
+
}
|
|
1135
|
+
if (!roundId) {
|
|
1136
|
+
return {
|
|
1137
|
+
error: "round_id_required",
|
|
1138
|
+
errorMessage: "roundId is required — pass the roundId from the latest response",
|
|
1139
|
+
sessionId,
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
if (!action || !isRecord(action)) {
|
|
1143
|
+
return { error: "invalid_argument", errorMessage: "action is required", sessionId, roundId };
|
|
1144
|
+
}
|
|
1029
1145
|
const descriptor = {
|
|
1030
1146
|
description: String(action.description ?? ""),
|
|
1031
1147
|
scope: Array.isArray(action.scope)
|
|
@@ -1033,7 +1149,7 @@ export const TOOL_HANDLERS = {
|
|
|
1033
1149
|
: [],
|
|
1034
1150
|
effects: Array.isArray(action.effects)
|
|
1035
1151
|
? action.effects.filter((v) => typeof v === "string")
|
|
1036
|
-
.filter((
|
|
1152
|
+
.filter((_v) => true)
|
|
1037
1153
|
: [],
|
|
1038
1154
|
reversibility: (["reversible", "recoverable", "irreversible", "unknown"]
|
|
1039
1155
|
.includes(String(action.reversibility))
|
|
@@ -1045,7 +1161,12 @@ export const TOOL_HANDLERS = {
|
|
|
1045
1161
|
: "unknown"),
|
|
1046
1162
|
};
|
|
1047
1163
|
if (!descriptor.description.trim()) {
|
|
1048
|
-
return {
|
|
1164
|
+
return {
|
|
1165
|
+
error: "invalid_argument",
|
|
1166
|
+
errorMessage: "action.description is required",
|
|
1167
|
+
sessionId,
|
|
1168
|
+
roundId,
|
|
1169
|
+
};
|
|
1049
1170
|
}
|
|
1050
1171
|
return mgr.checkGate(sessionId, roundId, descriptor);
|
|
1051
1172
|
},
|