loopforge 3.7.0 → 3.8.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.
- package/LICENSE +21 -21
- package/README.md +171 -157
- 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 +75 -69
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +276 -168
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +111 -12
- 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 +30 -16
- package/dist/cognitive-facts.js.map +1 -1
- package/dist/cognitive-governance.d.ts +30 -6
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +74 -18
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +109 -24
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +235 -52
- 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 +43 -20
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +269 -378
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +11 -6
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +102 -67
- 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 +91 -90
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +349 -160
- 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 +250 -53
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +41 -40
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +275 -515
- 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 +40 -21
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +173 -120
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +63 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +53 -7
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +240 -139
- 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 +377 -125
- 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 +65 -95
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +151 -39
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +7 -33
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +313 -473
- 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 +411 -168
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +23 -69
- 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 +55 -16
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +179 -57
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +7 -5
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +2 -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 +23 -9
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +99 -22
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +127 -11
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +517 -36
- 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 +27 -46
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +308 -557
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- 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 +17 -25
- package/package.json +7 -7
- package/skills/perception/SKILL.md +266 -64
package/dist/self-eval.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Structured self-evaluation parsing and normalization — pure functions.
|
|
2
2
|
* The MCP boundary validates required fields before these helpers run.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { makeExecutionReport, makeSelfEvaluation, } from "./protocol.js";
|
|
5
5
|
// ── Raw parsing helpers ───────────────────────────────────────────────────
|
|
6
6
|
function boundedString(value, maxChars) {
|
|
7
7
|
return typeof value === "string" ? value.slice(0, maxChars) : undefined;
|
|
@@ -14,6 +14,19 @@ function boundedStringArray(value, maxItems, maxChars) {
|
|
|
14
14
|
.slice(0, maxItems)
|
|
15
15
|
: [];
|
|
16
16
|
}
|
|
17
|
+
/** v3.8.1: the emerged-subtask intake bound — ONE constant, because the
|
|
18
|
+
* sub-goal ids are derived by ORDINAL over this list. The strict submission
|
|
19
|
+
* boundary builds the reference space the agent may transition from, and the
|
|
20
|
+
* lenient normalizer builds the committed list every prompt compiles from;
|
|
21
|
+
* bounding them differently let the boundary accept a transition to a
|
|
22
|
+
* sub-goal the prompt could never carry, and the migration was then dropped
|
|
23
|
+
* by the compile path without a word. */
|
|
24
|
+
export const EMERGED_LIMITS = { items: 50, chars: 500 };
|
|
25
|
+
/** THE emerged-list normalization. Both the boundary and the committed
|
|
26
|
+
* evaluation call this, so the two sets cannot disagree. */
|
|
27
|
+
export function boundedEmergedSubtasks(value) {
|
|
28
|
+
return boundedStringArray(value, EMERGED_LIMITS.items, EMERGED_LIMITS.chars);
|
|
29
|
+
}
|
|
17
30
|
function nonNegativeInteger(value) {
|
|
18
31
|
return typeof value === "number" && Number.isFinite(value)
|
|
19
32
|
? Math.max(0, Math.trunc(value))
|
|
@@ -40,17 +53,17 @@ function parseRetroactiveClaims(raw) {
|
|
|
40
53
|
}
|
|
41
54
|
return claims;
|
|
42
55
|
}
|
|
43
|
-
/** Parse
|
|
44
|
-
export function
|
|
56
|
+
/** Parse ExecutionReport from a raw JSON object. */
|
|
57
|
+
export function parseExecutionReport(raw) {
|
|
45
58
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
46
59
|
return undefined;
|
|
47
|
-
const testResults = raw.
|
|
48
|
-
!Array.isArray(raw.
|
|
49
|
-
? raw.
|
|
60
|
+
const testResults = raw.tests_reported && typeof raw.tests_reported === "object" &&
|
|
61
|
+
!Array.isArray(raw.tests_reported)
|
|
62
|
+
? raw.tests_reported
|
|
50
63
|
: undefined;
|
|
51
|
-
return
|
|
64
|
+
return makeExecutionReport({
|
|
52
65
|
files_changed: boundedStringArray(raw.files_changed, 200, 500),
|
|
53
|
-
|
|
66
|
+
tests_reported: testResults && typeof testResults.passed === "number" &&
|
|
54
67
|
Number.isFinite(testResults.passed)
|
|
55
68
|
? {
|
|
56
69
|
passed: nonNegativeInteger(testResults.passed),
|
|
@@ -58,14 +71,74 @@ export function parseExecutionEvidence(raw) {
|
|
|
58
71
|
skipped: nonNegativeInteger(testResults.skipped),
|
|
59
72
|
}
|
|
60
73
|
: null,
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
criterion_claims: parseCriterionClaims(raw.criterion_claims),
|
|
75
|
+
contract_item_claims: parseContractItemClaims(raw.contract_item_claims),
|
|
63
76
|
progress_estimate: typeof raw.progress_estimate === "number" &&
|
|
64
77
|
Number.isFinite(raw.progress_estimate)
|
|
65
78
|
? Math.max(0, Math.min(1, raw.progress_estimate))
|
|
66
79
|
: 0.0,
|
|
67
80
|
});
|
|
68
81
|
}
|
|
82
|
+
/** v3.8: The criterion ids the agent claims met this round. */
|
|
83
|
+
export function claimedMetCriteria(report) {
|
|
84
|
+
return (report?.criterion_claims ?? [])
|
|
85
|
+
.filter((claim) => claim.outcome === "met")
|
|
86
|
+
.map((claim) => claim.criterion_id);
|
|
87
|
+
}
|
|
88
|
+
/** v3.8: The criterion ids the agent declares still outstanding. */
|
|
89
|
+
export function claimedRemainingCriteria(report) {
|
|
90
|
+
return (report?.criterion_claims ?? [])
|
|
91
|
+
.filter((claim) => claim.outcome === "remaining")
|
|
92
|
+
.map((claim) => claim.criterion_id);
|
|
93
|
+
}
|
|
94
|
+
/** v3.8: LENIENT parse of advisory criterion claims — unknown or malformed
|
|
95
|
+
* entries are dropped, never a rejection (the contract item layer is the
|
|
96
|
+
* strict one). */
|
|
97
|
+
function parseCriterionClaims(raw) {
|
|
98
|
+
if (!Array.isArray(raw))
|
|
99
|
+
return [];
|
|
100
|
+
const claims = [];
|
|
101
|
+
for (const item of raw) {
|
|
102
|
+
if (claims.length >= 100)
|
|
103
|
+
break;
|
|
104
|
+
if (typeof item !== "object" || item === null || Array.isArray(item))
|
|
105
|
+
continue;
|
|
106
|
+
const entry = item;
|
|
107
|
+
if (typeof entry.criterion_id !== "string" || entry.criterion_id.trim().length === 0)
|
|
108
|
+
continue;
|
|
109
|
+
if (entry.outcome !== "met" && entry.outcome !== "remaining")
|
|
110
|
+
continue;
|
|
111
|
+
claims.push({
|
|
112
|
+
criterion_id: entry.criterion_id.trim().slice(0, 500),
|
|
113
|
+
outcome: entry.outcome,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return claims;
|
|
117
|
+
}
|
|
118
|
+
/** v3.8: LENIENT parse of contract item claims. Referential validity is the
|
|
119
|
+
* strict pre-advance boundary in validateContractShape — this parser only
|
|
120
|
+
* normalizes what reaches the read model. */
|
|
121
|
+
function parseContractItemClaims(raw) {
|
|
122
|
+
if (!Array.isArray(raw))
|
|
123
|
+
return [];
|
|
124
|
+
const claims = [];
|
|
125
|
+
for (const item of raw) {
|
|
126
|
+
if (claims.length >= 20)
|
|
127
|
+
break;
|
|
128
|
+
if (typeof item !== "object" || item === null || Array.isArray(item))
|
|
129
|
+
continue;
|
|
130
|
+
const entry = item;
|
|
131
|
+
if (typeof entry.item_id !== "string")
|
|
132
|
+
continue;
|
|
133
|
+
if (entry.outcome !== "met" && entry.outcome !== "remaining")
|
|
134
|
+
continue;
|
|
135
|
+
claims.push({
|
|
136
|
+
item_id: entry.item_id.trim().slice(0, 64),
|
|
137
|
+
outcome: entry.outcome,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return claims;
|
|
141
|
+
}
|
|
69
142
|
/** Parse CriterionRevision[] from a raw JSON array. */
|
|
70
143
|
export function parseCriterionRevisions(raw) {
|
|
71
144
|
if (!Array.isArray(raw))
|
|
@@ -80,15 +153,51 @@ export function parseCriterionRevisions(raw) {
|
|
|
80
153
|
return { old: r.old.slice(0, 500), new: r.new.slice(0, 500) };
|
|
81
154
|
});
|
|
82
155
|
}
|
|
83
|
-
/** Parse
|
|
84
|
-
|
|
156
|
+
/** Parse SubGoalUpdate[] from a raw JSON array.
|
|
157
|
+
* v3.7.1: lenient per-entry filtering (id/status shape) with fixed caps
|
|
158
|
+
* (20 entries, id ≤ 64, note ≤ 300). Referential validity — unknown IDs
|
|
159
|
+
* and illegal migrations — is NOT checked here: that needs the committed
|
|
160
|
+
* sub-goal set and runs as a pre-advance validation (evaluation_invalid). */
|
|
161
|
+
export function parseSubGoalUpdates(raw) {
|
|
85
162
|
if (!Array.isArray(raw))
|
|
86
163
|
return [];
|
|
87
164
|
return raw
|
|
88
165
|
.filter((v) => typeof v === "object" && v !== null &&
|
|
89
|
-
typeof v.
|
|
90
|
-
|
|
91
|
-
|
|
166
|
+
typeof v.id === "string" &&
|
|
167
|
+
(v.status === "in_progress" ||
|
|
168
|
+
v.status === "done" ||
|
|
169
|
+
v.status === "blocked" ||
|
|
170
|
+
v.status === "canceled"))
|
|
171
|
+
.slice(0, 20)
|
|
172
|
+
.map((u) => {
|
|
173
|
+
const update = {
|
|
174
|
+
id: u.id.trim().slice(0, 64),
|
|
175
|
+
status: u.status,
|
|
176
|
+
};
|
|
177
|
+
if (typeof u.note === "string" && u.note.trim().length > 0) {
|
|
178
|
+
update.note = u.note.trim().slice(0, 300);
|
|
179
|
+
}
|
|
180
|
+
return update;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/** Parse WorkerResult[] from a raw JSON array.
|
|
184
|
+
* v3.7.1: `outcome` is the single fact. An entry without a valid outcome
|
|
185
|
+
* is dropped (not derived, not defaulted) — the array stays informational:
|
|
186
|
+
* absent/empty arrays are accepted unchanged, so a round without any
|
|
187
|
+
* delegation can never be rejected over worker fields. */
|
|
188
|
+
export function parseWorkerResults(raw) {
|
|
189
|
+
if (!Array.isArray(raw))
|
|
190
|
+
return [];
|
|
191
|
+
return raw
|
|
192
|
+
.filter((v) => {
|
|
193
|
+
if (typeof v !== "object" || v === null)
|
|
194
|
+
return false;
|
|
195
|
+
const w = v;
|
|
196
|
+
return typeof w.agentId === "string" &&
|
|
197
|
+
typeof w.subTask === "string" &&
|
|
198
|
+
typeof w.resultSummary === "string" &&
|
|
199
|
+
(w.outcome === "success" || w.outcome === "partial" || w.outcome === "failed");
|
|
200
|
+
})
|
|
92
201
|
.slice(0, 20)
|
|
93
202
|
.map((v) => {
|
|
94
203
|
const w = v;
|
|
@@ -99,11 +208,7 @@ export function parseWorkerResults(raw) {
|
|
|
99
208
|
: "general-purpose",
|
|
100
209
|
subTask: w.subTask.slice(0, 500),
|
|
101
210
|
resultSummary: w.resultSummary.slice(0, 1000),
|
|
102
|
-
|
|
103
|
-
// v2.12: Declared outcome wins; derived from success when absent.
|
|
104
|
-
outcome: w.outcome === "success" || w.outcome === "partial" || w.outcome === "failed"
|
|
105
|
-
? w.outcome
|
|
106
|
-
: undefined,
|
|
211
|
+
outcome: w.outcome,
|
|
107
212
|
discoveredConstraints: boundedStringArray(w.discoveredConstraints, 50, 500),
|
|
108
213
|
};
|
|
109
214
|
});
|
|
@@ -148,10 +253,366 @@ export function validateCoreSelfEvaluation(raw) {
|
|
|
148
253
|
}
|
|
149
254
|
return { missing, invalid };
|
|
150
255
|
}
|
|
256
|
+
/** v3.7.1: Strict structural checks for the sub-goal protocol fields
|
|
257
|
+
* (subgoal_updates / emerged_subtasks). Runs in the same pre-advance
|
|
258
|
+
* evaluation_invalid boundary as the four core fields: an error here is a
|
|
259
|
+
* payload defect, never a work-quality rejection — no session state, no
|
|
260
|
+
* gates, no rejection counters, retry with the same roundId.
|
|
261
|
+
*
|
|
262
|
+
* Shape-only (no vault): entry shape, id pattern, status enum, caps, and
|
|
263
|
+
* the sg-XXXXXXXX creation prohibition on emerged_subtasks. Referential
|
|
264
|
+
* validity (unknown IDs / terminal references / illegal migrations) needs
|
|
265
|
+
* the committed sub-goal set and is checked against the compiled
|
|
266
|
+
* sub_goals right before advance (validateSubGoalUpdates in
|
|
267
|
+
* loop-compiler.ts). */
|
|
268
|
+
export function validateSubGoalUpdatesShape(raw) {
|
|
269
|
+
const errors = [];
|
|
270
|
+
const emerged = raw.emerged_subtasks;
|
|
271
|
+
if (Array.isArray(emerged)) {
|
|
272
|
+
for (const item of emerged) {
|
|
273
|
+
if (typeof item === "string" && /^sg-[a-f0-9]{8}$/.test(item.trim())) {
|
|
274
|
+
errors.push({
|
|
275
|
+
field: "emerged_subtasks",
|
|
276
|
+
reason: "id_in_creation",
|
|
277
|
+
detail: `"${item.trim()}" matches the sg-XXXXXXXX ID pattern — the creation channel never accepts ID references`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const updates = raw.subgoal_updates;
|
|
283
|
+
if (updates === undefined)
|
|
284
|
+
return errors;
|
|
285
|
+
if (!Array.isArray(updates)) {
|
|
286
|
+
errors.push({
|
|
287
|
+
field: "subgoal_updates",
|
|
288
|
+
reason: "not_array",
|
|
289
|
+
detail: "subgoal_updates must be an array of { id, status, note? } entries",
|
|
290
|
+
});
|
|
291
|
+
return errors;
|
|
292
|
+
}
|
|
293
|
+
if (updates.length > 20) {
|
|
294
|
+
errors.push({
|
|
295
|
+
field: "subgoal_updates",
|
|
296
|
+
reason: "too_many",
|
|
297
|
+
detail: "at most 20 subgoal_updates entries per round",
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
updates.forEach((entry, index) => {
|
|
301
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
302
|
+
errors.push({
|
|
303
|
+
field: "subgoal_updates",
|
|
304
|
+
reason: "invalid_entry",
|
|
305
|
+
detail: `entry ${index} is not an object`,
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const u = entry;
|
|
310
|
+
const id = u.id;
|
|
311
|
+
if (typeof id !== "string" || !/^sg-[a-f0-9]{8}$/.test(id.trim())) {
|
|
312
|
+
errors.push({
|
|
313
|
+
field: "subgoal_updates",
|
|
314
|
+
reason: "invalid_id",
|
|
315
|
+
detail: `entry ${index} must reference an sg-XXXXXXXX sub-goal ID`,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const status = u.status;
|
|
319
|
+
if (status !== "in_progress" && status !== "done" &&
|
|
320
|
+
status !== "blocked" && status !== "canceled") {
|
|
321
|
+
errors.push({
|
|
322
|
+
field: "subgoal_updates",
|
|
323
|
+
reason: "invalid_status",
|
|
324
|
+
detail: `entry ${index} status must be one of in_progress | done | blocked | canceled`,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// v3.8.1: `note` is deliberately NOT validated. protocol.ts documents it
|
|
328
|
+
// as "lenient — never validated" free text, and `parseSubGoalUpdates`
|
|
329
|
+
// drops a non-string note. Rejecting the WHOLE submission over a
|
|
330
|
+
// reporting annotation contradicted that boundary: it was the only
|
|
331
|
+
// lenient field that hard-rejected a round.
|
|
332
|
+
});
|
|
333
|
+
return errors;
|
|
334
|
+
}
|
|
335
|
+
const SUBGOAL_ID_RE = /^sg-[a-f0-9]{8}$/;
|
|
336
|
+
/** v3.8: `scope` is the round's declared file boundary, so containment is a
|
|
337
|
+
* trust boundary like every other workspace path — a declaration that names
|
|
338
|
+
* `/etc` or `../outside` is rejected, not silently normalized into a string
|
|
339
|
+
* the drift check can never match. */
|
|
340
|
+
function validateContractScope(scope, checkScopeEntry, errors) {
|
|
341
|
+
if (scope === undefined)
|
|
342
|
+
return;
|
|
343
|
+
if (!Array.isArray(scope)) {
|
|
344
|
+
errors.push({
|
|
345
|
+
field: "round_contract",
|
|
346
|
+
reason: "invalid_scope",
|
|
347
|
+
detail: "scope must be an array of workspace-relative paths",
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (scope.length > CONTRACT_LIMITS.scope) {
|
|
352
|
+
errors.push({
|
|
353
|
+
field: "round_contract",
|
|
354
|
+
reason: "too_many_scope_entries",
|
|
355
|
+
detail: `scope may list at most ${CONTRACT_LIMITS.scope} entries (got ${scope.length})`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
scope.forEach((entry, index) => {
|
|
359
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
360
|
+
errors.push({
|
|
361
|
+
field: "round_contract",
|
|
362
|
+
reason: "invalid_scope_entry",
|
|
363
|
+
detail: `scope entry ${index} must be a non-empty string`,
|
|
364
|
+
});
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const detail = checkScopeEntry(entry.trim());
|
|
368
|
+
if (detail) {
|
|
369
|
+
errors.push({
|
|
370
|
+
field: "round_contract",
|
|
371
|
+
reason: "scope_outside_workspace",
|
|
372
|
+
detail: `scope entry ${index} "${entry.trim()}" ${detail}`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
function validateContractItem(item, index, context, errors) {
|
|
378
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
379
|
+
errors.push({
|
|
380
|
+
field: "round_contract",
|
|
381
|
+
reason: "invalid_item",
|
|
382
|
+
detail: `item ${index} is not an object`,
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const entry = item;
|
|
387
|
+
if (typeof entry.description !== "string" || entry.description.trim().length === 0) {
|
|
388
|
+
errors.push({
|
|
389
|
+
field: "round_contract",
|
|
390
|
+
reason: "empty_item_description",
|
|
391
|
+
detail: `item ${index} needs a non-empty description`,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const verifyWith = entry.verify_with;
|
|
395
|
+
if (!Array.isArray(verifyWith) || verifyWith.length === 0) {
|
|
396
|
+
errors.push({
|
|
397
|
+
field: "round_contract",
|
|
398
|
+
reason: "no_verify_with",
|
|
399
|
+
detail: `item ${index} must bind at least one evidence command in verify_with`,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
if (verifyWith.length > CONTRACT_LIMITS.verifyWith) {
|
|
404
|
+
errors.push({
|
|
405
|
+
field: "round_contract",
|
|
406
|
+
reason: "too_many_verify_with",
|
|
407
|
+
detail: `item ${index} verify_with may name at most ${CONTRACT_LIMITS.verifyWith} commands ` +
|
|
408
|
+
`(got ${verifyWith.length})`,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
for (const name of verifyWith) {
|
|
412
|
+
if (typeof name !== "string" || !context.isConfiguredCommand(name)) {
|
|
413
|
+
errors.push({
|
|
414
|
+
field: "round_contract",
|
|
415
|
+
reason: "unknown_command",
|
|
416
|
+
detail: `item ${index} verify_with names "${String(name)}", which is not a ` +
|
|
417
|
+
"configured, enabled, after-capable evidence command",
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const criterionRefs = entry.criterion_refs;
|
|
423
|
+
if (criterionRefs !== undefined) {
|
|
424
|
+
if (!Array.isArray(criterionRefs)) {
|
|
425
|
+
errors.push({
|
|
426
|
+
field: "round_contract",
|
|
427
|
+
reason: "invalid_criterion_refs",
|
|
428
|
+
detail: `item ${index} criterion_refs must be an array of strings`,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
if (criterionRefs.length > CONTRACT_LIMITS.criterionRefs) {
|
|
433
|
+
errors.push({
|
|
434
|
+
field: "round_contract",
|
|
435
|
+
reason: "too_many_criterion_refs",
|
|
436
|
+
detail: `item ${index} criterion_refs may list at most ` +
|
|
437
|
+
`${CONTRACT_LIMITS.criterionRefs} ids (got ${criterionRefs.length})`,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
criterionRefs.forEach((ref, refIndex) => {
|
|
441
|
+
if (typeof ref !== "string" || ref.trim().length === 0) {
|
|
442
|
+
errors.push({
|
|
443
|
+
field: "round_contract",
|
|
444
|
+
reason: "invalid_criterion_ref",
|
|
445
|
+
detail: `item ${index} criterion_refs entry ${refIndex} must be a non-empty string`,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const subgoalRefs = entry.subgoal_refs;
|
|
452
|
+
if (subgoalRefs !== undefined) {
|
|
453
|
+
if (!Array.isArray(subgoalRefs)) {
|
|
454
|
+
errors.push({
|
|
455
|
+
field: "round_contract",
|
|
456
|
+
reason: "invalid_subgoal_refs",
|
|
457
|
+
detail: `item ${index} subgoal_refs must be an array of sg-XXXXXXXX ids`,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
if (subgoalRefs.length > CONTRACT_LIMITS.subgoalRefs) {
|
|
462
|
+
errors.push({
|
|
463
|
+
field: "round_contract",
|
|
464
|
+
reason: "too_many_subgoal_refs",
|
|
465
|
+
detail: `item ${index} subgoal_refs may list at most ` +
|
|
466
|
+
`${CONTRACT_LIMITS.subgoalRefs} ids (got ${subgoalRefs.length})`,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
const seen = new Set();
|
|
470
|
+
for (const ref of subgoalRefs) {
|
|
471
|
+
if (typeof ref !== "string" || !SUBGOAL_ID_RE.test(ref.trim())) {
|
|
472
|
+
errors.push({
|
|
473
|
+
field: "round_contract",
|
|
474
|
+
reason: "invalid_subgoal_ref",
|
|
475
|
+
detail: `item ${index} subgoal_refs entry "${String(ref)}" must be an sg-XXXXXXXX id`,
|
|
476
|
+
});
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const id = ref.trim();
|
|
480
|
+
if (seen.has(id)) {
|
|
481
|
+
errors.push({
|
|
482
|
+
field: "round_contract",
|
|
483
|
+
reason: "duplicate_subgoal_ref",
|
|
484
|
+
detail: `item ${index} subgoal_refs repeats ${id}`,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
seen.add(id);
|
|
488
|
+
if (context.knownSubGoalIds && !context.knownSubGoalIds.has(id)) {
|
|
489
|
+
errors.push({
|
|
490
|
+
field: "round_contract",
|
|
491
|
+
reason: "unknown_subgoal_ref",
|
|
492
|
+
detail: `item ${index} references ${id}, which is not a sub-goal of this loop`,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/** v3.8: Strict STRUCTURAL boundary for the Round Contract declaration and
|
|
500
|
+
* the contract item claims — the same pre-advance contract as
|
|
501
|
+
* subgoal_updates: an error here is a payload defect, never a work-quality
|
|
502
|
+
* rejection (no session state, no gates, no rejection counters, retry with
|
|
503
|
+
* the same roundId as `contract_invalid`).
|
|
504
|
+
*
|
|
505
|
+
* Declaration strictness: items must exist and stay within CONTRACT_LIMITS;
|
|
506
|
+
* every item must bind at least one configured, enabled, after-capable
|
|
507
|
+
* command; scope entries must stay inside the workspace; criterion and
|
|
508
|
+
* sub-goal references must be well-formed, and sub-goal references must name
|
|
509
|
+
* a sub-goal that actually exists. The limits are enforced HERE rather than
|
|
510
|
+
* by silently truncating in the lenient parser — a declaration is a
|
|
511
|
+
* boundary, not a suggestion.
|
|
512
|
+
*
|
|
513
|
+
* Claim strictness: item ids must be well formed, unique, and — when the
|
|
514
|
+
* active contract is known — reference an item of the ACTIVE contract. */
|
|
515
|
+
export function validateContractShape(raw, context) {
|
|
516
|
+
const errors = [];
|
|
517
|
+
const contract = raw.round_contract;
|
|
518
|
+
if (contract !== undefined) {
|
|
519
|
+
if (typeof contract !== "object" || contract === null || Array.isArray(contract)) {
|
|
520
|
+
errors.push({
|
|
521
|
+
field: "round_contract",
|
|
522
|
+
reason: "not_object",
|
|
523
|
+
detail: "round_contract must be an object with an items array",
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
const obj = contract;
|
|
528
|
+
validateContractScope(obj.scope, context.checkScopeEntry, errors);
|
|
529
|
+
const items = obj.items;
|
|
530
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
531
|
+
errors.push({
|
|
532
|
+
field: "round_contract",
|
|
533
|
+
reason: "no_items",
|
|
534
|
+
detail: "a contract must declare at least one item (description + verify_with)",
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
else if (items.length > CONTRACT_LIMITS.items) {
|
|
538
|
+
errors.push({
|
|
539
|
+
field: "round_contract",
|
|
540
|
+
reason: "too_many_items",
|
|
541
|
+
detail: `a contract may declare at most ${CONTRACT_LIMITS.items} items (got ${items.length})`,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
items.forEach((item, index) => {
|
|
546
|
+
validateContractItem(item, index, context, errors);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const report = raw.execution_report;
|
|
552
|
+
if (typeof report === "object" && report !== null && !Array.isArray(report)) {
|
|
553
|
+
const claims = report.contract_item_claims;
|
|
554
|
+
if (claims !== undefined) {
|
|
555
|
+
if (!Array.isArray(claims)) {
|
|
556
|
+
errors.push({
|
|
557
|
+
field: "execution_report.contract_item_claims",
|
|
558
|
+
reason: "not_array",
|
|
559
|
+
detail: "contract_item_claims must be an array of { item_id, outcome } entries",
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
const seen = new Set();
|
|
564
|
+
claims.forEach((claim, index) => {
|
|
565
|
+
if (typeof claim !== "object" || claim === null || Array.isArray(claim)) {
|
|
566
|
+
errors.push({
|
|
567
|
+
field: "execution_report.contract_item_claims",
|
|
568
|
+
reason: "invalid_entry",
|
|
569
|
+
detail: `entry ${index} is not an object`,
|
|
570
|
+
});
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const entry = claim;
|
|
574
|
+
const id = typeof entry.item_id === "string" ? entry.item_id.trim() : "";
|
|
575
|
+
if (!/^rci-[a-f0-9]{8}$/.test(id)) {
|
|
576
|
+
errors.push({
|
|
577
|
+
field: "execution_report.contract_item_claims",
|
|
578
|
+
reason: "invalid_item_id",
|
|
579
|
+
detail: `entry ${index} must reference an rci-XXXXXXXX contract item id`,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
if (seen.has(id)) {
|
|
584
|
+
errors.push({
|
|
585
|
+
field: "execution_report.contract_item_claims",
|
|
586
|
+
reason: "duplicate_item_id",
|
|
587
|
+
detail: `entry ${index} repeats item id ${id}`,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
seen.add(id);
|
|
591
|
+
if (context.activeItemIds && !context.activeItemIds.has(id)) {
|
|
592
|
+
errors.push({
|
|
593
|
+
field: "execution_report.contract_item_claims",
|
|
594
|
+
reason: "unknown_item_id",
|
|
595
|
+
detail: `entry ${index} references ${id}, which is not an item of the ACTIVE contract`,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (entry.outcome !== "met" && entry.outcome !== "remaining") {
|
|
600
|
+
errors.push({
|
|
601
|
+
field: "execution_report.contract_item_claims",
|
|
602
|
+
reason: "invalid_outcome",
|
|
603
|
+
detail: `entry ${index} outcome must be "met" or "remaining"`,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return errors;
|
|
611
|
+
}
|
|
151
612
|
/** Build a SelfEvaluation from a parsed JSON object.
|
|
152
613
|
* Lenient parsing: missing optional fields get sensible defaults. */
|
|
153
614
|
export function buildSelfEvaluation(raw) {
|
|
154
|
-
const
|
|
615
|
+
const executionReport = parseExecutionReport(raw.execution_report);
|
|
155
616
|
const retractedConstraints = boundedStringArray(raw.retracted_constraints, 50, 500);
|
|
156
617
|
const revisedCriteria = parseCriterionRevisions(raw.revised_success_criteria);
|
|
157
618
|
const wrongAssumptions = boundedStringArray(raw.wrong_assumptions, 50, 500);
|
|
@@ -165,18 +626,15 @@ export function buildSelfEvaluation(raw) {
|
|
|
165
626
|
should_continue: typeof raw.should_continue === "boolean" ? raw.should_continue : true,
|
|
166
627
|
discovered_constraints: boundedStringArray(raw.discovered_constraints, 50, 500),
|
|
167
628
|
objective_refinement: boundedString(raw.objective_refinement, 1000) ?? "",
|
|
168
|
-
emerged_subtasks:
|
|
169
|
-
|
|
629
|
+
emerged_subtasks: boundedEmergedSubtasks(raw.emerged_subtasks),
|
|
630
|
+
execution_report: executionReport,
|
|
170
631
|
retracted_constraints: retractedConstraints,
|
|
171
632
|
revised_success_criteria: revisedCriteria,
|
|
172
633
|
wrong_assumptions: wrongAssumptions,
|
|
173
634
|
worker_results: workerResults,
|
|
174
635
|
compression_checkpoint: typeof raw.compression_checkpoint === "boolean" ? raw.compression_checkpoint : false,
|
|
175
636
|
checkpoint_label: boundedString(raw.checkpoint_label, 200) ?? "",
|
|
176
|
-
|
|
177
|
-
completed_subtasks: boundedStringArray(raw.completed_subtasks, 50, 500),
|
|
178
|
-
blocked_subtasks: boundedStringArray(raw.blocked_subtasks, 50, 500),
|
|
179
|
-
canceled_subtasks: boundedStringArray(raw.canceled_subtasks, 50, 500),
|
|
637
|
+
subgoal_updates: parseSubGoalUpdates(raw.subgoal_updates),
|
|
180
638
|
stop_reason: raw.stop_reason === "gave_up" || raw.stop_reason === "blocked" || raw.stop_reason === "needs_human_input"
|
|
181
639
|
? raw.stop_reason
|
|
182
640
|
: undefined,
|
|
@@ -187,20 +645,31 @@ export function buildSelfEvaluation(raw) {
|
|
|
187
645
|
blocker: typeof raw.blocker === "string" && raw.blocker.trim().length > 0
|
|
188
646
|
? raw.blocker.slice(0, 500)
|
|
189
647
|
: undefined,
|
|
648
|
+
gate_ids: boundedStringArray(raw.gate_ids, 20, 64),
|
|
190
649
|
retroactiveClaims: parseRetroactiveClaims(raw.retroactiveClaims),
|
|
191
650
|
no_change_reason: typeof raw.no_change_reason === "string" && raw.no_change_reason.trim().length > 0
|
|
192
651
|
? raw.no_change_reason.slice(0, 200)
|
|
193
652
|
: undefined,
|
|
194
|
-
drift_clarification: boundedString(raw.drift_clarification, 1000),
|
|
195
653
|
prompt_requests: parsePromptRequests(raw.prompt_requests),
|
|
196
654
|
round_contract: parseRoundContract(raw.round_contract),
|
|
197
655
|
});
|
|
198
656
|
}
|
|
199
|
-
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
|
|
657
|
+
/** v3.8: The ONE set of contract-declaration limits. `validateContractShape`
|
|
658
|
+
* (the strict boundary) rejects a declaration over these; `parseRoundContract`
|
|
659
|
+
* (the lenient path, reached only after the strict boundary passed) truncates
|
|
660
|
+
* to the same numbers. One source, so the two can never drift. */
|
|
661
|
+
export const CONTRACT_LIMITS = {
|
|
662
|
+
items: 20,
|
|
663
|
+
scope: 50,
|
|
664
|
+
criterionRefs: 20,
|
|
665
|
+
subgoalRefs: 20,
|
|
666
|
+
verifyWith: 20,
|
|
667
|
+
};
|
|
668
|
+
/** Parse a Round Contract PROPOSAL from raw JSON input. Lenient on shapes
|
|
669
|
+
* the strict declaration boundary already rejected: strings are trimmed and
|
|
670
|
+
* capped, non-string array entries are dropped. Item ORDER and COUNT are
|
|
671
|
+
* preserved — the derived rci- ids depend on them. Returns undefined only
|
|
672
|
+
* when the raw value is absent or not an object. */
|
|
204
673
|
export function parseRoundContract(raw) {
|
|
205
674
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
206
675
|
return undefined;
|
|
@@ -212,13 +681,25 @@ export function parseRoundContract(raw) {
|
|
|
212
681
|
.filter(Boolean)
|
|
213
682
|
.slice(0, cap)
|
|
214
683
|
: [];
|
|
684
|
+
const items = Array.isArray(obj.items)
|
|
685
|
+
? obj.items
|
|
686
|
+
.slice(0, CONTRACT_LIMITS.items)
|
|
687
|
+
.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item))
|
|
688
|
+
.map((item) => ({
|
|
689
|
+
description: typeof item.description === "string"
|
|
690
|
+
? item.description.trim().slice(0, 200)
|
|
691
|
+
: "",
|
|
692
|
+
criterion_refs: capStrings(item.criterion_refs, 200, CONTRACT_LIMITS.criterionRefs),
|
|
693
|
+
subgoal_refs: capStrings(item.subgoal_refs, 64, CONTRACT_LIMITS.subgoalRefs),
|
|
694
|
+
verify_with: capStrings(item.verify_with, 200, CONTRACT_LIMITS.verifyWith),
|
|
695
|
+
}))
|
|
696
|
+
: [];
|
|
215
697
|
return {
|
|
216
698
|
work_item: typeof obj.work_item === "string" && obj.work_item.trim().length > 0
|
|
217
699
|
? obj.work_item.trim().slice(0, 200)
|
|
218
700
|
: undefined,
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
scope: capStrings(obj.scope, 500, 50),
|
|
701
|
+
scope: capStrings(obj.scope, 500, CONTRACT_LIMITS.scope),
|
|
702
|
+
items,
|
|
222
703
|
};
|
|
223
704
|
}
|
|
224
705
|
/** Parse a PromptRequests object from raw JSON input.
|