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/loop-compiler.js
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { getPolicy } from "./policy.js";
|
|
8
|
-
import { AgentStatus, makeLoopCompileResponse,
|
|
8
|
+
import { AgentStatus, makeLoopCompileResponse, makeLoopObjective, makeConstraintMeta, makeMilestoneSummary, makeRollingSummary, } from "./protocol.js";
|
|
9
9
|
import { createCanonicalLoopState, renderCanonicalStateMarkdown, } from "./canonical-state.js";
|
|
10
|
-
import { decodeRound, entryLineage, entryCriteriaMet, entryCriteriaRemaining, entryActiveConstraints, entryRetractedConstraints, entryProgressEstimate,
|
|
11
|
-
import {
|
|
10
|
+
import { decodeBacktrackRecord, decodeRound, entryLineage, entryCriteriaMet, entryCriteriaRemaining, entryActiveConstraints, entryRetractedConstraints, entryProgressEstimate, machineGitMotionSeries, derivationRounds, } from "./committed-round.js";
|
|
11
|
+
import { NO_IN_FLIGHT_ROUND, deriveRoundFacts } from "./round-facts.js";
|
|
12
|
+
import { recoveryBriefLines } from "./enforcement-gate.js";
|
|
13
|
+
import { deriveSubGoals, duplicateEmergedDeclarations } from "./subgoal-state.js";
|
|
14
|
+
import { claimedMetCriteria, claimedRemainingCriteria } from "./self-eval.js";
|
|
12
15
|
import { assemblePromptArtifact } from "./prompt-assembler.js";
|
|
13
16
|
import { decidePromptLevel, } from "./prompt-policy.js";
|
|
14
|
-
import { deriveItemId,
|
|
17
|
+
import { deriveCriterionId, deriveItemId, isCriterionId, normalizeText, unique, entryRound, } from "./token-utils.js";
|
|
15
18
|
function contextEntries(context) {
|
|
16
19
|
if (!context || !Array.isArray(context.results))
|
|
17
20
|
return [];
|
|
@@ -55,32 +58,6 @@ export function deriveGoalId(loopId, task, explicit = "") {
|
|
|
55
58
|
.replace(/^-|-$/g, "")
|
|
56
59
|
.slice(0, 48) || computeGoalTextHash(task)}`;
|
|
57
60
|
}
|
|
58
|
-
/** v3.2: Read the previous round's persisted L1 presentation snapshot — the
|
|
59
|
-
* diff baseline for L1 collapse. Returns null when the previous round's
|
|
60
|
-
* lineage entry lacks the three presented_* fields (as on L0/L2 compiles),
|
|
61
|
-
* so callers render the full L1 state. */
|
|
62
|
-
export function readPresentedBaseline(loopId, round, context) {
|
|
63
|
-
const entry = roundCanonicalEntry(loopEntries(loopId, context), round - 1);
|
|
64
|
-
if (!entry)
|
|
65
|
-
return null;
|
|
66
|
-
const data = entryLineage(entry);
|
|
67
|
-
const constraintIds = data.presented_constraint_ids;
|
|
68
|
-
const subGoals = data.presented_subgoals;
|
|
69
|
-
const milestoneRanges = data.presented_milestone_ranges;
|
|
70
|
-
if (!Array.isArray(constraintIds) || !constraintIds.every((v) => typeof v === "string") ||
|
|
71
|
-
!Array.isArray(subGoals) || !subGoals.every((v) => Array.isArray(v) && v.length === 2 &&
|
|
72
|
-
typeof v[0] === "string" && typeof v[1] === "string") ||
|
|
73
|
-
!Array.isArray(milestoneRanges) || !milestoneRanges.every((v) => Array.isArray(v) && v.length === 2 &&
|
|
74
|
-
typeof v[0] === "number" && typeof v[1] === "number")) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
round: round - 1,
|
|
79
|
-
constraintIds: constraintIds,
|
|
80
|
-
subGoals: subGoals,
|
|
81
|
-
milestoneRanges: milestoneRanges,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
61
|
export function getPreviousRound(loopId, round, context) {
|
|
85
62
|
const entry = roundCanonicalEntry(loopEntries(loopId, context), round);
|
|
86
63
|
if (!entry)
|
|
@@ -216,86 +193,50 @@ function evolveConstraints(request, objective, previous, context) {
|
|
|
216
193
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
217
194
|
// Hierarchical Summary (v2.1)
|
|
218
195
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
219
|
-
/** v3.
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
for (const flag of flags) {
|
|
245
|
-
const target = flag.severity === "error"
|
|
246
|
-
? errors
|
|
247
|
-
: flag.severity === "warn" ? warns : null;
|
|
248
|
-
if (!target)
|
|
249
|
-
continue;
|
|
250
|
-
const list = target.get(flag.check) ?? [];
|
|
251
|
-
list.push(rnd);
|
|
252
|
-
target.set(flag.check, list);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
const lessons = [];
|
|
256
|
-
for (const [text, rounds] of violations) {
|
|
257
|
-
if (rounds.length >= 2) {
|
|
258
|
-
lessons.push({ text, kind: "constraint_violation", count: rounds.length, rounds });
|
|
196
|
+
/** v3.8.1: THE recurring-fact derivation — one walk over the committed
|
|
197
|
+
* history, grouping repeated machine facts by what they are ABOUT.
|
|
198
|
+
*
|
|
199
|
+
* This replaces three separate answers to "what keeps going wrong":
|
|
200
|
+
* `deriveLessons` (whole history, count >= 2, keyed by constraint text or
|
|
201
|
+
* check id), `rolling_summary.recurring_issues` (the last 5 rounds' raw
|
|
202
|
+
* violation texts, with NO threshold at all despite the name), and the L1/L2
|
|
203
|
+
* sections built on top of each. Three windows over one fact set is exactly
|
|
204
|
+
* the parallel semantics this release removes.
|
|
205
|
+
*
|
|
206
|
+
* Callers FILTER this list rather than re-deriving: the state file renders
|
|
207
|
+
* the genuinely recurring set (count >= 2), the prompt renders the recent
|
|
208
|
+
* tail as Active Warnings.
|
|
209
|
+
*
|
|
210
|
+
* Presentation only: the result never feeds enforcement. */
|
|
211
|
+
export function deriveRecurringFlags(committedRounds) {
|
|
212
|
+
const byKey = new Map();
|
|
213
|
+
const record = (kind, subject, ref, round) => {
|
|
214
|
+
const key = `${kind}${subject}${ref}`;
|
|
215
|
+
const existing = byKey.get(key);
|
|
216
|
+
if (existing) {
|
|
217
|
+
existing.count++;
|
|
218
|
+
if (!existing.rounds.includes(round))
|
|
219
|
+
existing.rounds.push(round);
|
|
220
|
+
return;
|
|
259
221
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
222
|
+
byKey.set(key, { subject, kind, ref, count: 1, rounds: [round] });
|
|
223
|
+
};
|
|
224
|
+
for (const round of committedRounds) {
|
|
225
|
+
for (const text of round.constraintViolations ?? []) {
|
|
226
|
+
record("constraint_violation", text, "", round.round);
|
|
264
227
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
228
|
+
for (const flag of round.verificationFlags) {
|
|
229
|
+
const kind = flag.severity === "error"
|
|
230
|
+
? "verification_error"
|
|
231
|
+
: flag.severity === "warn" ? "verification_warning" : null;
|
|
232
|
+
if (!kind)
|
|
233
|
+
continue;
|
|
234
|
+
record(kind, flag.check, flag.ref ?? "", round.round);
|
|
269
235
|
}
|
|
270
236
|
}
|
|
271
|
-
return
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
* committed rounds — files changed, rejected attempts (snapshot.attempt
|
|
275
|
-
* minus one: the successful commit's attempt counts the redo), and the
|
|
276
|
-
* self-reported progress delta vs the previous round. One row per round,
|
|
277
|
-
* preferring the :feedback entry when both lineage and feedback entries
|
|
278
|
-
* exist. Zero persistence — re-derived from committed entries. */
|
|
279
|
-
function deriveRoundStats(rounds, window = 5) {
|
|
280
|
-
const stats = [];
|
|
281
|
-
let prevProgress = null;
|
|
282
|
-
for (const round of rounds.slice(-window)) {
|
|
283
|
-
const ev = round.executionEvidence;
|
|
284
|
-
const files = ev?.files_changed;
|
|
285
|
-
const rawProgress = ev?.progress_estimate;
|
|
286
|
-
const progress = typeof rawProgress === "number" ? rawProgress : null;
|
|
287
|
-
stats.push({
|
|
288
|
-
round: round.round,
|
|
289
|
-
filesChangedCount: Array.isArray(files) ? files.length : null,
|
|
290
|
-
rejectedAttempts: round.attempt >= 1 ? round.attempt - 1 : null,
|
|
291
|
-
progressDelta: progress !== null && prevProgress !== null
|
|
292
|
-
? Number((progress - prevProgress).toFixed(3))
|
|
293
|
-
: null,
|
|
294
|
-
});
|
|
295
|
-
if (progress !== null)
|
|
296
|
-
prevProgress = progress;
|
|
297
|
-
}
|
|
298
|
-
return stats;
|
|
237
|
+
return [...byKey.values()].sort((a, b) => b.count - a.count ||
|
|
238
|
+
a.subject.localeCompare(b.subject) ||
|
|
239
|
+
a.ref.localeCompare(b.ref));
|
|
299
240
|
}
|
|
300
241
|
/** v3.3: Machine git-motion cross-check over the last 3 committed rounds
|
|
301
242
|
* (the R4 window). Undefined when no committed rounds carry git snapshots
|
|
@@ -308,39 +249,36 @@ function deriveMachineStatus(rounds, currentRound) {
|
|
|
308
249
|
const motionRounds = series.filter(Boolean).length;
|
|
309
250
|
return { windowRounds: 3, gitMotion: motionRounds > 0, motionRounds };
|
|
310
251
|
}
|
|
311
|
-
/** v3.4: ACTIVE Round Contract for the compile round — derived from the
|
|
312
|
-
* committed evals of earlier rounds only, never from request data. Reads
|
|
313
|
-
* the hydrated merged lineage entries (one per round; a committed decision
|
|
314
|
-
* is merged onto the round's lineage entry with `committed_action` set),
|
|
315
|
-
* skipping rounds whose committed action was "backtrack" (a roll-back
|
|
316
|
-
* directive, not an executed round). Because it runs identically on every
|
|
317
|
-
* compile path — including rejection retries and resume / unpause /
|
|
318
|
-
* backtrack compiles that carry no last_round_result — a contract round's
|
|
319
|
-
* re-compiles keep showing its contract as the Current Task. Null when no
|
|
320
|
-
* active contract: the Current Task falls back to the original task text. */
|
|
321
|
-
function deriveActiveContract(loopId, context, currentRound) {
|
|
322
|
-
const entries = loopEntries(loopId, context);
|
|
323
|
-
const canonical = Array.from({ length: Math.max(0, currentRound - 1) }, (_, index) => roundCanonicalEntry(entries, index + 1)).filter((entry) => entry !== null);
|
|
324
|
-
return deriveActiveRoundContract(contractRoundEvaluations(mergedRoundsFromEntries(canonical, currentRound)));
|
|
325
|
-
}
|
|
326
252
|
/** v3.2: Derive per-criterion status — the "goal → criteria → evidence"
|
|
327
253
|
* vertical view. Each objective criterion gets: met/remaining/unknown
|
|
328
|
-
* (from per-round
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* vault every compile. */
|
|
332
|
-
export function deriveCriterionStatuses(loopId, context, objective, currentRound,
|
|
254
|
+
* (from per-round criterion_claims, ID-first or normalized-exact matching),
|
|
255
|
+
* the round it was first reported met, and the sub-goals a contract item
|
|
256
|
+
* referencing it also names (explicit `subgoal_refs`, never a text guess).
|
|
257
|
+
* Zero persistence — re-derived from the vault every compile. */
|
|
258
|
+
export function deriveCriterionStatuses(loopId, context, objective, currentRound, lastRoundResult,
|
|
259
|
+
/** v3.8.1: the criterion machine facts from the shared bundle. A criterion
|
|
260
|
+
* is `verified` / `contradicted` / `insufficient` only through an item that
|
|
261
|
+
* references it — a claim alone can never reach `verified` — and the fact
|
|
262
|
+
* is derived over the WHOLE committed history, so it survives its contract
|
|
263
|
+
* closing (see `deriveCriterionFacts`). */
|
|
264
|
+
criterionFacts = []) {
|
|
333
265
|
if (!objective || objective.success_criteria.length === 0)
|
|
334
266
|
return [];
|
|
335
267
|
const entries = loopEntries(loopId, context).filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
|
|
336
268
|
// The current round's report lives in last_round_result (the vault only
|
|
337
269
|
// gains it on commit); treat it as the report for round currentRound - 1.
|
|
338
|
-
const lastEv = lastRoundResult?.
|
|
339
|
-
const lastMet = lastEv
|
|
340
|
-
const lastRemaining = lastEv
|
|
270
|
+
const lastEv = lastRoundResult?.execution_report;
|
|
271
|
+
const lastMet = claimedMetCriteria(lastEv);
|
|
272
|
+
const lastRemaining = claimedRemainingCriteria(lastEv);
|
|
341
273
|
const lastEntry = entries[entries.length - 1] ?? null;
|
|
342
274
|
const lastEntryRemaining = lastEntry ? entryCriteriaRemaining(lastEntry) : [];
|
|
343
|
-
|
|
275
|
+
// v3.8.1: criterion → machine status + explicit sub-goal links, from the
|
|
276
|
+
// shared `RoundFacts` bundle (whole committed history). It used to be
|
|
277
|
+
// re-derived here from the ACTIVE contract, which made both maps empty the
|
|
278
|
+
// moment the contract closed — the criterion's machine fact, and the
|
|
279
|
+
// sub-goal links carried on the same item, disappeared while the sub-goal
|
|
280
|
+
// side of the very same fact survived. One derivation, one window.
|
|
281
|
+
const factsByCriterion = new Map(criterionFacts.map((fact) => [fact.criterion_id, fact]));
|
|
344
282
|
return objective.success_criteria.map((text) => {
|
|
345
283
|
let metAtRound = null;
|
|
346
284
|
for (const entry of entries) {
|
|
@@ -353,28 +291,33 @@ export function deriveCriterionStatuses(loopId, context, objective, currentRound
|
|
|
353
291
|
if (metAtRound === null && lastMet.some((met) => criteriaMatch(met, text))) {
|
|
354
292
|
metAtRound = currentRound - 1;
|
|
355
293
|
}
|
|
356
|
-
|
|
357
|
-
|
|
294
|
+
// v3.8: a claim reaches `claimed` at most. Only a machine fact about a
|
|
295
|
+
// contract item that references this criterion can go further.
|
|
296
|
+
const claimedStatus = metAtRound !== null
|
|
297
|
+
? "claimed"
|
|
358
298
|
: lastRemaining.some((remaining) => criteriaMatch(remaining, text)) ||
|
|
359
299
|
lastEntryRemaining.some((remaining) => criteriaMatch(remaining, text))
|
|
360
300
|
? "remaining"
|
|
361
301
|
: "unknown";
|
|
302
|
+
const fact = factsByCriterion.get(deriveCriterionId(text));
|
|
303
|
+
const status = fact?.status ?? claimedStatus;
|
|
362
304
|
return {
|
|
363
305
|
id: deriveCriterionId(text),
|
|
364
306
|
text,
|
|
365
307
|
status,
|
|
366
308
|
...(metAtRound !== null ? { met_at_round: metAtRound } : {}),
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
309
|
+
// v3.8.1: explicit refs only — empty when no contract item links this
|
|
310
|
+
// criterion to a sub-goal.
|
|
311
|
+
related_subgoal_ids: fact?.related_subgoal_ids ?? [],
|
|
370
312
|
};
|
|
371
313
|
});
|
|
372
314
|
}
|
|
373
|
-
/**
|
|
374
|
-
*
|
|
375
|
-
|
|
376
|
-
*
|
|
377
|
-
*
|
|
315
|
+
/** v3.8: Rank item statuses so the strongest machine fact wins when several
|
|
316
|
+
* items reference the same criterion. */
|
|
317
|
+
/** Match two criterion references. If either is a criterion id
|
|
318
|
+
* (cr-XXXXXXXX), compares ids; otherwise requires the two texts to be
|
|
319
|
+
* EXACTLY equal after normalization (v3.8.1 — the similarity fallback is
|
|
320
|
+
* gone, so a paraphrase is a different criterion). */
|
|
378
321
|
export function criteriaMatch(a, b) {
|
|
379
322
|
const aIsId = isCriterionId(a);
|
|
380
323
|
const bIsId = isCriterionId(b);
|
|
@@ -386,13 +329,16 @@ export function criteriaMatch(a, b) {
|
|
|
386
329
|
return a === deriveCriterionId(b);
|
|
387
330
|
if (bIsId)
|
|
388
331
|
return deriveCriterionId(a) === b;
|
|
389
|
-
//
|
|
390
|
-
|
|
332
|
+
// v3.8.1: normalized text must be EXACTLY equal. The former Jaccard
|
|
333
|
+
// fallback let two merely-similar criterion strings count as the same
|
|
334
|
+
// criterion, which could fuse distinct criteria (and suppress or invent a
|
|
335
|
+
// criteria milestone) on a similarity score rather than on an identity.
|
|
336
|
+
return normalizeText(a) === normalizeText(b);
|
|
391
337
|
}
|
|
392
338
|
/** Detect newly met criteria by comparing the current entry's
|
|
393
|
-
*
|
|
394
|
-
* v2.11:
|
|
395
|
-
*
|
|
339
|
+
* met criterion_claims against the previous entry's.
|
|
340
|
+
* v2.11/v3.8.1: id-first (cr-XXXXXXXX), then normalized-exact text —
|
|
341
|
+
* no similarity.
|
|
396
342
|
* Returns empty array when there is no previous entry — the first
|
|
397
343
|
* entry's criteria are the baseline, not a "new" event. */
|
|
398
344
|
function detectNewCriteria(current, previous) {
|
|
@@ -433,56 +379,14 @@ function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, ki
|
|
|
433
379
|
generated_at_round: endRound,
|
|
434
380
|
});
|
|
435
381
|
}
|
|
436
|
-
/** v3.
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return mid;
|
|
445
|
-
const chosen = [];
|
|
446
|
-
const used = new Set();
|
|
447
|
-
for (const m of mid) {
|
|
448
|
-
if (m.kind === "agent_declared") {
|
|
449
|
-
chosen.push(m);
|
|
450
|
-
used.add(m);
|
|
451
|
-
if (chosen.length >= budget)
|
|
452
|
-
return chosen;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
const rest = mid.filter((m) => !used.has(m));
|
|
456
|
-
const need = budget - chosen.length;
|
|
457
|
-
for (let i = 0; i < need; i++) {
|
|
458
|
-
const idx = Math.floor(((i + 0.5) * rest.length) / need);
|
|
459
|
-
chosen.push(rest[idx]);
|
|
460
|
-
}
|
|
461
|
-
return chosen;
|
|
462
|
-
}
|
|
463
|
-
/** v3.0.1: L1 milestone sampling — when milestones exceed max_milestones,
|
|
464
|
-
* keep the oldest `milestone_head_count` (history anchors), the newest
|
|
465
|
-
* `milestone_tail_count` (current progress), and an even sample of the
|
|
466
|
-
* middle. L2 (full rehydration) is never sampled: the recovery view keeps
|
|
467
|
-
* every milestone as the loop's memory skeleton. */
|
|
468
|
-
function sampleMilestones(milestones, policy, level) {
|
|
469
|
-
if (level === "l2")
|
|
470
|
-
return milestones;
|
|
471
|
-
if (milestones.length <= policy.max_milestones)
|
|
472
|
-
return milestones;
|
|
473
|
-
const head = Math.max(0, policy.milestone_head_count);
|
|
474
|
-
const tail = Math.max(0, policy.milestone_tail_count);
|
|
475
|
-
const midBudget = Math.max(0, policy.max_milestones - head - tail);
|
|
476
|
-
if (midBudget === 0)
|
|
477
|
-
return milestones.slice(0, head).concat(milestones.slice(milestones.length - tail));
|
|
478
|
-
const mid = milestones.slice(head, milestones.length - tail);
|
|
479
|
-
return [
|
|
480
|
-
...milestones.slice(0, head),
|
|
481
|
-
...sampleMiddleMilestones(mid, midBudget),
|
|
482
|
-
...milestones.slice(milestones.length - tail),
|
|
483
|
-
];
|
|
484
|
-
}
|
|
485
|
-
export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0, level) {
|
|
382
|
+
/** v3.8.1: the milestone history bound. Applied identically at every prompt
|
|
383
|
+
* level and in the state file, so the milestone set is a function of
|
|
384
|
+
* committed facts rather than of which level happened to compile. (The
|
|
385
|
+
* v3.0.1 sampler chose which milestones to keep from an UNBOUNDED list and
|
|
386
|
+
* did nothing at L2 — so the state file’s phase history depended on the
|
|
387
|
+
* prompt level and had no bound of its own.) */
|
|
388
|
+
const MILESTONE_HISTORY_CAP = 50;
|
|
389
|
+
export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0) {
|
|
486
390
|
const policy = getPolicy().summary;
|
|
487
391
|
const allEntries = loopEntries(loopId, context);
|
|
488
392
|
// ── Phase 1: Recent Window (preserves existing behavior) ──
|
|
@@ -493,7 +397,6 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
493
397
|
})
|
|
494
398
|
.slice(-policy.window);
|
|
495
399
|
const outcomes = [];
|
|
496
|
-
const issues = [];
|
|
497
400
|
for (const entry of windowEntries) {
|
|
498
401
|
const view = decodeRound(entry);
|
|
499
402
|
const data = entryLineage(entry);
|
|
@@ -513,12 +416,6 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
513
416
|
: success === false ? "failed" : "accepted";
|
|
514
417
|
if (summary)
|
|
515
418
|
outcomes.push(`[R${round}] ${outcomeLabel}: ${summary}`);
|
|
516
|
-
const violations = view
|
|
517
|
-
? view.constraintViolations ?? []
|
|
518
|
-
: Array.isArray(entry.constraint_violations)
|
|
519
|
-
? entry.constraint_violations
|
|
520
|
-
: Array.isArray(data.constraint_violations) ? data.constraint_violations : [];
|
|
521
|
-
issues.push(...violations.filter((item) => typeof item === "string"));
|
|
522
419
|
}
|
|
523
420
|
// ── Phase 2: Milestone Accumulation ──
|
|
524
421
|
// Scan all completed entries in round order. For each entry, check three
|
|
@@ -575,15 +472,17 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
575
472
|
milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, endRound, `Rounds ${startRound}–${endRound}`, "auto"));
|
|
576
473
|
}
|
|
577
474
|
}
|
|
578
|
-
// v3.
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
|
|
475
|
+
// v3.8.1: one bound, applied identically at every prompt level. The v3.0.1
|
|
476
|
+
// sampler kept head anchors + a newest tail + an even middle sample, and
|
|
477
|
+
// deliberately did nothing at L2 — so the milestone set written to the state
|
|
478
|
+
// file depended on which level happened to compile, not on committed facts.
|
|
479
|
+
// It also did not bound growth at all (it only chose WHICH milestones to
|
|
480
|
+
// keep from an unbounded list), which is why the cap has to live here.
|
|
481
|
+
const cappedMilestones = milestones.slice(-MILESTONE_HISTORY_CAP);
|
|
582
482
|
if (windowEntries.length === 0 && cappedMilestones.length === 0)
|
|
583
483
|
return null;
|
|
584
484
|
return makeRollingSummary({
|
|
585
485
|
key_outcomes: unique(outcomes),
|
|
586
|
-
recurring_issues: unique(issues),
|
|
587
486
|
rounds_sampled: windowEntries.length,
|
|
588
487
|
generated_at_round: currentRound,
|
|
589
488
|
failed_patterns: [],
|
|
@@ -593,188 +492,17 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
593
492
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
594
493
|
// Sub-Goal Structured Tracking (v2.2)
|
|
595
494
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
596
|
-
/** Derive a stable sub-goal ID from its description hash.
|
|
597
|
-
* Exported as the single source of truth (verification-gate imports it). */
|
|
598
|
-
export function deriveSubGoalId(description) {
|
|
599
|
-
return "sg-" + deriveItemId(description);
|
|
600
|
-
}
|
|
601
495
|
/** v2.11: Derive a stable constraint ID from its text hash (c-XXXXXXXX).
|
|
602
496
|
* Same hash strategy as SubGoal — deterministic across rounds. */
|
|
603
497
|
export function deriveConstraintId(text) {
|
|
604
498
|
return "c-" + deriveItemId(text);
|
|
605
499
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
* Matches the pattern cr-XXXXXXXX where X is a hex digit. Module-local:
|
|
613
|
-
* consumers with the same need (round-contract.ts) keep their own copy to
|
|
614
|
-
* avoid an import cycle. */
|
|
615
|
-
function isCriterionId(ref) {
|
|
616
|
-
return /^cr-[a-f0-9]{8}$/.test(ref);
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* Match a user-provided description against existing SubGoals for DEDUP
|
|
620
|
-
* (emerged_subtasks creation). Exact ID first, then Jaccard similarity on
|
|
621
|
-
* descriptions. v3.7.1: status transitions no longer use this — they are
|
|
622
|
-
* strict ID references validated before the round advances.
|
|
623
|
-
*/
|
|
624
|
-
function matchSubGoal(target, subGoals, threshold) {
|
|
625
|
-
const trimmed = target.trim();
|
|
626
|
-
// Phase 1: Exact ID match
|
|
627
|
-
for (let i = 0; i < subGoals.length; i++) {
|
|
628
|
-
if (subGoals[i].id === trimmed)
|
|
629
|
-
return i;
|
|
630
|
-
}
|
|
631
|
-
// Phase 2: Jaccard fallback on description text
|
|
632
|
-
let bestIdx = -1;
|
|
633
|
-
let bestScore = 0;
|
|
634
|
-
for (let i = 0; i < subGoals.length; i++) {
|
|
635
|
-
const score = jaccardSimilarity(trimmed, subGoals[i].description);
|
|
636
|
-
if (score > bestScore) {
|
|
637
|
-
bestScore = score;
|
|
638
|
-
bestIdx = i;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
return bestScore >= threshold ? bestIdx : -1;
|
|
642
|
-
}
|
|
643
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
644
|
-
// Sub-goal transition matrix (v3.7.1 — single source)
|
|
645
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
646
|
-
/** Closed migration matrix. done/canceled are terminal (no out-edges); the
|
|
647
|
-
* matrix rejects re-opening. blocked → in_progress is the recovery path. */
|
|
648
|
-
export const SUBGOAL_TRANSITIONS = {
|
|
649
|
-
in_progress: ["pending", "blocked"],
|
|
650
|
-
done: ["pending", "in_progress", "blocked"],
|
|
651
|
-
blocked: ["pending", "in_progress"],
|
|
652
|
-
canceled: ["pending", "in_progress", "blocked"],
|
|
653
|
-
};
|
|
654
|
-
/** Whether a transition is legal. Same-status is a legal no-op (used by
|
|
655
|
-
* replay idempotency). References to terminal sub-goals are rejected by
|
|
656
|
-
* validateSubGoalUpdates before this is consulted. */
|
|
657
|
-
export function canTransitionSubGoal(from, to) {
|
|
658
|
-
if (from === to)
|
|
659
|
-
return true;
|
|
660
|
-
return SUBGOAL_TRANSITIONS[to].includes(from);
|
|
661
|
-
}
|
|
662
|
-
/** Referential validation of a payload's subgoal_updates against the
|
|
663
|
-
* derived sub-goal set. Returns one error per invalid entry:
|
|
664
|
-
* unknown_id | terminal_reference | illegal_transition. */
|
|
665
|
-
export function validateSubGoalUpdates(subGoals, updates) {
|
|
666
|
-
const byId = new Map(subGoals.map((sg) => [sg.id, sg]));
|
|
667
|
-
const errors = [];
|
|
668
|
-
for (const update of updates) {
|
|
669
|
-
const target = byId.get(update.id);
|
|
670
|
-
if (!target) {
|
|
671
|
-
errors.push({ id: update.id, reason: "unknown_id" });
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
if (target.status === "done" || target.status === "canceled") {
|
|
675
|
-
errors.push({ id: update.id, reason: "terminal_reference" });
|
|
676
|
-
continue;
|
|
677
|
-
}
|
|
678
|
-
if (!canTransitionSubGoal(target.status, update.status)) {
|
|
679
|
-
errors.push({ id: update.id, reason: "illegal_transition" });
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
return errors;
|
|
683
|
-
}
|
|
684
|
-
/** Apply one transition to the accumulated list. Guards make replay of
|
|
685
|
-
* committed history idempotent: terminal sub-goals never change and
|
|
686
|
-
* same-status updates are no-ops. */
|
|
687
|
-
function applySubGoalUpdate(subGoals, update, rnd) {
|
|
688
|
-
const target = subGoals.find((sg) => sg.id === update.id);
|
|
689
|
-
if (!target)
|
|
690
|
-
return;
|
|
691
|
-
if (target.status === "done" || target.status === "canceled")
|
|
692
|
-
return;
|
|
693
|
-
if (!canTransitionSubGoal(target.status, update.status))
|
|
694
|
-
return;
|
|
695
|
-
if (target.status === update.status)
|
|
696
|
-
return;
|
|
697
|
-
target.status = update.status;
|
|
698
|
-
target.status_changed_at_round = rnd;
|
|
699
|
-
if (update.status === "done")
|
|
700
|
-
target.completed_at_round = rnd;
|
|
701
|
-
}
|
|
702
|
-
/**
|
|
703
|
-
* Manage the full sub-goal lifecycle across rounds.
|
|
704
|
-
*
|
|
705
|
-
* v3.7.1 derivation rule: EVERY committed round (round < currentRound) is
|
|
706
|
-
* replayed in round order — emerged_subtasks create pending items, then
|
|
707
|
-
* subgoal_updates apply explicit transitions. The round's own report
|
|
708
|
-
* (last_round_result, which the engine rebuilds from the same committed
|
|
709
|
-
* evaluation) contributes the same data on the fixture path, so applying
|
|
710
|
-
* it again is an idempotent no-op. Older transitions therefore never
|
|
711
|
-
* regress on later compiles, and no status is ever inferred.
|
|
712
|
-
*/
|
|
713
|
-
function manageSubGoals(loopId, currentRound, lastRoundResult, vaultContext) {
|
|
714
|
-
const policy = getPolicy().evolution;
|
|
715
|
-
const allEntries = loopEntries(loopId, vaultContext);
|
|
716
|
-
const completedEntries = allEntries
|
|
717
|
-
.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound)
|
|
718
|
-
.sort((a, b) => entryRound(a) - entryRound(b));
|
|
719
|
-
const subGoals = [];
|
|
720
|
-
const createEmerged = (descs, rnd) => {
|
|
721
|
-
for (const desc of descs) {
|
|
722
|
-
// Dedup: skip if an existing sub-goal is too similar
|
|
723
|
-
if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
|
|
724
|
-
continue;
|
|
725
|
-
subGoals.push(makeSubGoal({
|
|
726
|
-
id: deriveSubGoalId(desc),
|
|
727
|
-
description: desc.trim(),
|
|
728
|
-
status: "pending",
|
|
729
|
-
declared_at_round: rnd,
|
|
730
|
-
status_changed_at_round: rnd,
|
|
731
|
-
priority: subGoals.length,
|
|
732
|
-
}));
|
|
733
|
-
}
|
|
734
|
-
};
|
|
735
|
-
// Phase 1 — Replay every committed round in order: create, then apply
|
|
736
|
-
// that round's declared transitions.
|
|
737
|
-
for (const entry of completedEntries) {
|
|
738
|
-
const rnd = entryRound(entry);
|
|
739
|
-
createEmerged(entryEmergedSubtasks(entry), rnd);
|
|
740
|
-
for (const update of entrySubGoalUpdates(entry)) {
|
|
741
|
-
applySubGoalUpdate(subGoals, update, rnd);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
// Phase 2 — The current round's own report (fixture/rebuild path; on the
|
|
745
|
-
// vault path this duplicates the last committed round and is idempotent).
|
|
746
|
-
if (lastRoundResult) {
|
|
747
|
-
const rnd = lastRoundResult.round || (currentRound - 1);
|
|
748
|
-
createEmerged(lastRoundResult.emerged_subtasks ?? [], rnd);
|
|
749
|
-
for (const update of lastRoundResult.subgoal_updates ?? []) {
|
|
750
|
-
applySubGoalUpdate(subGoals, update, rnd);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
// Sort for display: in_progress first, then pending by age desc, then blocked, then done, then canceled
|
|
754
|
-
subGoals.sort((a, b) => {
|
|
755
|
-
const rank = (s) => {
|
|
756
|
-
switch (s.status) {
|
|
757
|
-
case "in_progress": return 0;
|
|
758
|
-
case "pending": return 1;
|
|
759
|
-
case "blocked": return 2;
|
|
760
|
-
case "done": return 3;
|
|
761
|
-
case "canceled": return 4;
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
const ra = rank(a), rb = rank(b);
|
|
765
|
-
if (ra !== rb)
|
|
766
|
-
return ra - rb;
|
|
767
|
-
// Within same status: pending by oldest first, done by most recent first
|
|
768
|
-
// (v2.14: the previous expression simplified to b.declared - a.declared,
|
|
769
|
-
// which sorted NEWEST first despite the comment)
|
|
770
|
-
if (a.status === "pending")
|
|
771
|
-
return a.declared_at_round - b.declared_at_round;
|
|
772
|
-
if (a.status === "done")
|
|
773
|
-
return (b.completed_at_round ?? 0) - (a.completed_at_round ?? 0);
|
|
774
|
-
return a.declared_at_round - b.declared_at_round;
|
|
775
|
-
});
|
|
776
|
-
return subGoals;
|
|
777
|
-
}
|
|
500
|
+
// v3.8.1: `deriveCriterionId` / `isCriterionId` moved to token-utils.ts, where
|
|
501
|
+
// the other stable-id helpers live — the criterion fact derivation in
|
|
502
|
+
// round-facts.ts needs them, and defining them here would make
|
|
503
|
+
// round-facts ⇄ loop-compiler an import cycle. Re-exported so this module's
|
|
504
|
+
// public surface ("./compiler") is unchanged.
|
|
505
|
+
export { deriveCriterionId, isCriterionId } from "./token-utils.js";
|
|
778
506
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
779
507
|
// Time-Aware Constraint Management (v2.3)
|
|
780
508
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -789,19 +517,18 @@ function constraintSource(text, objective, constraintsFromPlan) {
|
|
|
789
517
|
return "criteria";
|
|
790
518
|
return "discovered";
|
|
791
519
|
}
|
|
792
|
-
/**
|
|
793
|
-
* v2.11: ID-first
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
|
|
797
|
-
*
|
|
520
|
+
/** True when a reported violation text matches a constraint text.
|
|
521
|
+
* v2.11: ID-first — an exact `c-XXXXXXXX` match against the derived id.
|
|
522
|
+
* v3.8.1: the Jaccard fallback is gone; the remaining arm requires the
|
|
523
|
+
* reported text to be EXACTLY equal to the constraint after normalization,
|
|
524
|
+
* so a reported violation can no longer attach itself to a constraint it
|
|
525
|
+
* merely resembles. */
|
|
798
526
|
function matchesConstraintText(text, violation) {
|
|
799
527
|
const targetId = deriveConstraintId(text);
|
|
800
528
|
if (violation === targetId || deriveConstraintId(violation) === targetId) {
|
|
801
529
|
return true;
|
|
802
530
|
}
|
|
803
|
-
return
|
|
804
|
-
getPolicy().evolution.constraint_match_threshold;
|
|
531
|
+
return normalizeText(text) === normalizeText(violation);
|
|
805
532
|
}
|
|
806
533
|
function findLastViolatedRound(text, vaultEntries) {
|
|
807
534
|
let latest = 0;
|
|
@@ -853,7 +580,7 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
|
|
|
853
580
|
// (its own Success Criteria section and the verification gate's
|
|
854
581
|
// criteriaMatch are ID-first on cr-). Rendering it under c-XXXXXXXX in
|
|
855
582
|
// the active list meant the same text carried two IDs in one prompt,
|
|
856
|
-
// and an agent echoing the active-list ID into
|
|
583
|
+
// and an agent echoing the active-list ID into its criterion claims
|
|
857
584
|
// could never match.
|
|
858
585
|
metadata.push(makeConstraintMeta({
|
|
859
586
|
id: source === "criteria" ? deriveCriterionId(text) : deriveConstraintId(text),
|
|
@@ -866,38 +593,6 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
|
|
|
866
593
|
}
|
|
867
594
|
return { active, metadata };
|
|
868
595
|
}
|
|
869
|
-
export function alignTask(proposedTask, request, context) {
|
|
870
|
-
const objective = request.loop_objective ?? latestObjective(request.loop_id, context);
|
|
871
|
-
if (!objective)
|
|
872
|
-
return makeTaskAlignment();
|
|
873
|
-
// v2.14: threshold is policy-driven (evolution.progress_mismatch_threshold)
|
|
874
|
-
const threshold = getPolicy().evolution.progress_mismatch_threshold;
|
|
875
|
-
const score = jaccardSimilarity(proposedTask, [objective.objective, ...objective.success_criteria, ...objective.hard_constraints].join(" "));
|
|
876
|
-
return makeTaskAlignment({
|
|
877
|
-
is_aligned: score >= threshold,
|
|
878
|
-
alignment_score: Number(score.toFixed(2)),
|
|
879
|
-
warning: score < threshold ? "Current task may be drifting from the loop objective." : "",
|
|
880
|
-
escalation: score < threshold ? "warn" : "none",
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
|
-
export function checkLoopHealth(loopId, request, context) {
|
|
884
|
-
const previous = getPreviousRound(loopId, request.round - 1, context);
|
|
885
|
-
const alignment = alignTask(request.task, request, context);
|
|
886
|
-
const violations = request.last_round_result?.constraint_violations.length ?? 0;
|
|
887
|
-
const integrity = Math.max(0, 1 - violations * 0.2);
|
|
888
|
-
const continuity = previous ? jaccardSimilarity(previous.task, request.task) : 1;
|
|
889
|
-
// v2.14: thresholds are policy-driven
|
|
890
|
-
const drift = alignment.alignment_score < getPolicy().evolution.progress_mismatch_threshold ||
|
|
891
|
-
continuity < getPolicy().evolution.task_continuity_threshold;
|
|
892
|
-
return makeLoopHealth({
|
|
893
|
-
goal_alignment: alignment.alignment_score,
|
|
894
|
-
constraint_integrity: integrity,
|
|
895
|
-
drift_detected: drift,
|
|
896
|
-
strategy_stability: true,
|
|
897
|
-
task_continuity: Number(continuity.toFixed(2)),
|
|
898
|
-
escalation_recommended: drift || integrity < 0.6 ? "l2" : "none",
|
|
899
|
-
});
|
|
900
|
-
}
|
|
901
596
|
function levelDecision(request, context) {
|
|
902
597
|
const previous = getPreviousRound(request.loop_id, request.round - 1, context);
|
|
903
598
|
const last = request.last_round_result;
|
|
@@ -908,11 +603,6 @@ function levelDecision(request, context) {
|
|
|
908
603
|
last?.retracted_constraints?.length ||
|
|
909
604
|
last?.revised_success_criteria?.length ||
|
|
910
605
|
last?.wrong_assumptions?.length);
|
|
911
|
-
const lastFullRound = loopEntries(request.loop_id, context)
|
|
912
|
-
.filter((entry) => entryLineage(entry).recompile_level === "l2")
|
|
913
|
-
.map(entryRound)
|
|
914
|
-
.filter((round) => round < request.round)
|
|
915
|
-
.at(-1) ?? 1;
|
|
916
606
|
// v2.14: a backtrack committed for this round means the loop is
|
|
917
607
|
// re-walking restored state — the recovery boundary forces L2
|
|
918
608
|
// rehydration (the decidePromptLevel recovery_boundary branch existed
|
|
@@ -933,14 +623,12 @@ function levelDecision(request, context) {
|
|
|
933
623
|
verificationContradicted: (request.verification_flags ?? [])
|
|
934
624
|
.some((flag) => flag.severity === "error"),
|
|
935
625
|
consecutiveRejections: request.consecutive_rejections,
|
|
936
|
-
fullRefreshInterval: getPolicy().prompt.full_refresh_interval,
|
|
937
|
-
lastFullRound,
|
|
938
626
|
});
|
|
939
627
|
}
|
|
940
628
|
export function decideLevel(request, context) {
|
|
941
629
|
return levelDecision(request, context).level;
|
|
942
630
|
}
|
|
943
|
-
export function buildSelfEvalBlock(round,
|
|
631
|
+
export function buildSelfEvalBlock(round,
|
|
944
632
|
/** v2.12: L0 is the minimal retry template — the v2.12 declarative fields
|
|
945
633
|
* (outcome/blocker/retroactiveClaims) are L1/L2 additions so the retry
|
|
946
634
|
* prompt stays within its tight budget. */
|
|
@@ -950,16 +638,7 @@ level,
|
|
|
950
638
|
* `activeContract != null`). Only then does the template ask the agent
|
|
951
639
|
* to restate/propose it — a generic empty contract template would invite
|
|
952
640
|
* placeholder submissions that trigger round_underspecified noise. */
|
|
953
|
-
hasContract = false
|
|
954
|
-
/** v3.5: L2-only prose suggesting a Round Contract declaration when the
|
|
955
|
-
* Current Task is NOT one (contract_nudge_on_l2 policy, computed at the
|
|
956
|
-
* compileLoop call site). Mutually exclusive with hasContract. The prose
|
|
957
|
-
* must never contain the JSON key name `round_contract` — contract-less
|
|
958
|
-
* L2 tests assert its lowercase absence. */
|
|
959
|
-
proposalNudge = false) {
|
|
960
|
-
const hasIntentDrift = prevDriftFlags?.some(f => f.check === "intent_drift") ?? false;
|
|
961
|
-
const hasSubGoalDrift = prevDriftFlags?.some(f => f.check === "subgoal_drift") ?? false;
|
|
962
|
-
const needsClarification = hasIntentDrift || hasSubGoalDrift;
|
|
641
|
+
hasContract = false) {
|
|
963
642
|
const declareOutcome = level !== "l0";
|
|
964
643
|
const restateContract = hasContract && declareOutcome;
|
|
965
644
|
const evalObj = {
|
|
@@ -969,24 +648,31 @@ proposalNudge = false) {
|
|
|
969
648
|
should_continue: true,
|
|
970
649
|
discovered_constraints: ["<new constraint>"],
|
|
971
650
|
emerged_subtasks: [],
|
|
972
|
-
|
|
651
|
+
execution_report: {
|
|
973
652
|
files_changed: [],
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
653
|
+
tests_reported: { passed: 0, failed: 0, skipped: 0 },
|
|
654
|
+
// The contract claim is functional (it is what advances an item), so it
|
|
655
|
+
// stays at every level. The advisory criterion claim is an L1/L2
|
|
656
|
+
// addition — the L0 retry template is a lean delta kept under budget.
|
|
657
|
+
contract_item_claims: [
|
|
658
|
+
{ item_id: "<rci-XXXXXXXX from the Current Task>", outcome: "met" },
|
|
659
|
+
],
|
|
977
660
|
progress_estimate: 0,
|
|
978
661
|
},
|
|
979
662
|
wrong_assumptions: [],
|
|
980
|
-
next_action: "<next concrete action, or empty when complete>",
|
|
981
663
|
subgoal_updates: [
|
|
982
664
|
{ id: "<sg-XXXXXXXX from the Sub-Goal Dashboard>", status: "done" },
|
|
983
665
|
],
|
|
984
666
|
};
|
|
985
|
-
// v2.12: Declarative
|
|
667
|
+
// v2.12: Declarative outcome (L1/L2 only — keeps L0 retry lean)
|
|
986
668
|
if (declareOutcome) {
|
|
987
669
|
evalObj.outcome = "<success|partial|failed|blocked>";
|
|
988
670
|
evalObj.blocker = "<required only when outcome=blocked>";
|
|
989
671
|
evalObj.retroactiveClaims = [];
|
|
672
|
+
// v3.8: the advisory criterion claim rides with the other L1/L2 additions.
|
|
673
|
+
evalObj.execution_report.criterion_claims = [
|
|
674
|
+
{ criterion_id: "<cr-XXXXXXXX or text>", outcome: "met" },
|
|
675
|
+
];
|
|
990
676
|
}
|
|
991
677
|
// v3.3/v3.4: Round Contract proposal template (L1/L2, active-contract
|
|
992
678
|
// rounds only — L0 retry stays lean, and rounds without an active
|
|
@@ -1002,26 +688,12 @@ proposalNudge = false) {
|
|
|
1002
688
|
scope: [],
|
|
1003
689
|
};
|
|
1004
690
|
}
|
|
1005
|
-
// v2.8: Inject drift clarification field when previous round had drift flags
|
|
1006
|
-
if (needsClarification) {
|
|
1007
|
-
const flagTypes = [
|
|
1008
|
-
hasIntentDrift ? "intent_drift" : "",
|
|
1009
|
-
hasSubGoalDrift ? "subgoal_drift" : "",
|
|
1010
|
-
].filter(Boolean).join(" / ");
|
|
1011
|
-
evalObj.drift_clarification =
|
|
1012
|
-
`<explain why your Round ${round} actions diverged from your stated plan ` +
|
|
1013
|
-
`(${flagTypes} detected in previous round). ` +
|
|
1014
|
-
`Leave empty if no divergence occurred>`;
|
|
1015
|
-
}
|
|
1016
691
|
const lines = [
|
|
1017
692
|
"### LoopForge Evaluation (Required)",
|
|
1018
693
|
"",
|
|
1019
694
|
`After completing Round ${round}, replace the placeholder values below`,
|
|
1020
695
|
"with your actual results and pass this object as `loopforge_next.evaluation`.",
|
|
1021
696
|
];
|
|
1022
|
-
if (needsClarification) {
|
|
1023
|
-
lines.push("", "> ⚠️ **Drift Detected:** The verification gate detected that your previous", "> round's declared intent (next_action) did not match your actual output.", "> Please explain the pivot in the `drift_clarification` field below.", "> An honest explanation helps the enforcement gate distinguish intentional", "> course corrections from unacknowledged drift.");
|
|
1024
|
-
}
|
|
1025
697
|
lines.push("```json");
|
|
1026
698
|
lines.push(JSON.stringify(evalObj, null, 2));
|
|
1027
699
|
lines.push("```");
|
|
@@ -1031,13 +703,8 @@ proposalNudge = false) {
|
|
|
1031
703
|
lines.push("Use IDs for exact matching: c-XXXXXXXX, cr-XXXXXXXX, sg-XXXXXXXX.");
|
|
1032
704
|
lines.push("Sub-goal changes go through `subgoal_updates` (active sub-goal IDs only;");
|
|
1033
705
|
lines.push("done/canceled are terminal — reopen via `emerged_subtasks`; bad IDs reject).");
|
|
1034
|
-
// v3.5: L2 contract-less nudge (prose only — never the JSON key name, so
|
|
1035
|
-
// contract-less rounds keep their template-lean assertions).
|
|
1036
|
-
if (proposalNudge) {
|
|
1037
|
-
lines.push("", "If the remaining work will span several rounds, consider declaring a", "Round Contract for the next round — list its done_when items, the", "verification_plan commands that will back them, and the scope. This", "keeps each round's boundary machine-checkable.");
|
|
1038
|
-
}
|
|
1039
706
|
if (restateContract) {
|
|
1040
|
-
lines.push("Round Contract: restate the Current Task's contract UNCHANGED in", "`round_contract` (work_item/
|
|
707
|
+
lines.push("Round Contract: restate the Current Task's contract UNCHANGED in", "`round_contract` (work_item/items/scope) and run each item's Verify", "commands; claim the items you completed in", "`execution_report.contract_item_claims` using the `rci-XXXXXXXX` ids", "the Current Task renders. An item only becomes verified when its bound", "command is observed passing — a claim alone leaves it insufficient.", "When every item is verified, or the round is blocked (outcome=blocked", "+ blocker), declare the NEXT round's contract instead (or omit", "`round_contract` when the whole task is done — the Current Task", "reverts to the original task).");
|
|
1041
708
|
}
|
|
1042
709
|
return lines.join("\n");
|
|
1043
710
|
}
|
|
@@ -1054,32 +721,22 @@ function committedBacktrackBrief(loopId, round, context) {
|
|
|
1054
721
|
const entry = loopEntries(loopId, context).find((e) => entryLineage(e).committed_action === "backtrack" && entryRound(e) === round);
|
|
1055
722
|
if (!entry)
|
|
1056
723
|
return undefined;
|
|
1057
|
-
//
|
|
1058
|
-
// the
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
:
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
if (failedRounds.length > 0)
|
|
1074
|
-
lines.push(`- **Failed rounds**: ${failedRounds.join(", ")}`);
|
|
1075
|
-
for (const approach of approaches)
|
|
1076
|
-
lines.push(` - Failed approach: ${approach}`);
|
|
1077
|
-
if (assumptions.length > 0) {
|
|
1078
|
-
lines.push("- **Falsified assumptions** (do not rebuild on these):");
|
|
1079
|
-
for (const assumption of assumptions)
|
|
1080
|
-
lines.push(` - ${assumption}`);
|
|
1081
|
-
}
|
|
1082
|
-
return lines;
|
|
724
|
+
// v3.8.1: one decoder for the committed rollback record and one renderer for
|
|
725
|
+
// the brief. The facts ride on the merged lineage entry (engine.ts stamps
|
|
726
|
+
// them from the committed decision) and the same `decodeBacktrackRecord`
|
|
727
|
+
// reads the durable :feedback envelope, so both carriers decode to one shape
|
|
728
|
+
// — and this brief and the rollback prompt no longer render the same
|
|
729
|
+
// rollback with two hand-written line builders.
|
|
730
|
+
const record = decodeBacktrackRecord(entryLineage(entry));
|
|
731
|
+
if (!record)
|
|
732
|
+
return undefined;
|
|
733
|
+
return recoveryBriefLines({
|
|
734
|
+
target: record.target,
|
|
735
|
+
triggerRule: record.triggerRule,
|
|
736
|
+
failedRounds: record.failedRounds,
|
|
737
|
+
approaches: record.approaches,
|
|
738
|
+
wrongAssumptions: record.wrongAssumptions,
|
|
739
|
+
});
|
|
1083
740
|
}
|
|
1084
741
|
export function compileLoop(request, context) {
|
|
1085
742
|
const decision = levelDecision(request, context);
|
|
@@ -1087,45 +744,76 @@ export function compileLoop(request, context) {
|
|
|
1087
744
|
const objective = evolveObjective(request, context);
|
|
1088
745
|
const constraints = evolveConstraints(request, objective, previous, context);
|
|
1089
746
|
const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round, request.last_round_result?.constraint_violations ?? []);
|
|
1090
|
-
const rolling = buildRollingSummary(request.loop_id, request.round, context, 0
|
|
1091
|
-
|
|
747
|
+
const rolling = buildRollingSummary(request.loop_id, request.round, context, 0);
|
|
748
|
+
// v3.8: ONE committed-round derivation feeds every projection below
|
|
749
|
+
// (sub-goals, the active contract, its item statuses, criteria).
|
|
750
|
+
const windowRounds = derivationRounds(loopEntries(request.loop_id, context), request.round);
|
|
751
|
+
// v3.8: sub-goal lifecycle lives in subgoal-state.ts and consumes the
|
|
752
|
+
// shared committed-round view — one history interpretation.
|
|
753
|
+
const subGoals = deriveSubGoals({
|
|
754
|
+
loopId: request.loop_id,
|
|
755
|
+
currentRound: request.round,
|
|
756
|
+
rounds: windowRounds,
|
|
757
|
+
currentReport: request.last_round_result
|
|
758
|
+
? {
|
|
759
|
+
round: request.last_round_result.round || (request.round - 1),
|
|
760
|
+
emergedSubtasks: request.last_round_result.emerged_subtasks ?? [],
|
|
761
|
+
subgoalUpdates: request.last_round_result.subgoal_updates ?? [],
|
|
762
|
+
}
|
|
763
|
+
: null,
|
|
764
|
+
});
|
|
765
|
+
// v3.4/v3.8/v3.8.1: the ACTIVE Round Contract, its per-item statuses and the
|
|
766
|
+
// verified sub-goal facts for this compile — ONE `deriveRoundFacts` call,
|
|
767
|
+
// the same bundle the projection consumes. Derived from committed rounds, so
|
|
768
|
+
// rejection retries, resume, unpause and backtrack compiles (which carry no
|
|
769
|
+
// last_round_result) keep showing the same contract, and a verified or
|
|
770
|
+
// blocked contract stops being shown. A verified item stays proven after its
|
|
771
|
+
// contract closes because the bundle reads the whole history, not the active
|
|
772
|
+
// contract. The compile path has no in-flight observations of its own.
|
|
773
|
+
const roundFacts = deriveRoundFacts({
|
|
774
|
+
rounds: windowRounds,
|
|
775
|
+
currentRound: request.round,
|
|
776
|
+
inFlight: NO_IN_FLIGHT_ROUND,
|
|
777
|
+
subGoals,
|
|
778
|
+
commands: getPolicy().evidence.commands ?? [],
|
|
779
|
+
});
|
|
780
|
+
// `roundFacts.verifiedSubGoals` is not read here — it reaches the state file
|
|
781
|
+
// and the projection through the bundle itself.
|
|
782
|
+
const { activeContract } = roundFacts;
|
|
1092
783
|
// v3.2: Goal → criteria → evidence vertical view (derived, zero persistence).
|
|
1093
|
-
const criterionStatuses = deriveCriterionStatuses(request.loop_id, context, objective, request.round,
|
|
1094
|
-
// v3.
|
|
1095
|
-
|
|
1096
|
-
const
|
|
1097
|
-
|
|
784
|
+
const criterionStatuses = deriveCriterionStatuses(request.loop_id, context, objective, request.round, request.last_round_result, roundFacts.criterionFacts);
|
|
785
|
+
// v3.8.1: the ONE recurring-fact derivation, over the shared window. The
|
|
786
|
+
// prompt renders its recent tail; the state file renders the recurring set.
|
|
787
|
+
const recurringFlags = deriveRecurringFlags(windowRounds);
|
|
788
|
+
// v3.8.1: a same-round exact repeat is the only sub-goal duplication the
|
|
789
|
+
// runtime can state as a fact — those entries really were dropped, so the
|
|
790
|
+
// agent is told. The cross-round near-duplicate similarity diagnostic is
|
|
791
|
+
// gone with the rest of the fuzzy matching, and so are the task-alignment
|
|
792
|
+
// and loop-health warnings: both were Jaccard scores over task/objective
|
|
793
|
+
// text, never machine facts.
|
|
794
|
+
const repeatedDeclarations = duplicateEmergedDeclarations(request.last_round_result?.emerged_subtasks ?? []);
|
|
1098
795
|
const warnings = unique([
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
796
|
+
repeatedDeclarations.length > 0
|
|
797
|
+
? `duplicate_declaration: ${repeatedDeclarations.length} sub-goal ` +
|
|
798
|
+
`declaration(s) repeated an earlier entry in the same round and were ` +
|
|
799
|
+
`not created: ${repeatedDeclarations.join("; ")}`
|
|
1102
800
|
: "",
|
|
1103
801
|
]);
|
|
1104
|
-
//
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
//
|
|
1110
|
-
//
|
|
802
|
+
// v3.8.1: the agent trust score and trend are deleted. The score was
|
|
803
|
+
// `1 - errors*0.15 - warns*0.03` — arbitrary weights with no machine
|
|
804
|
+
// meaning, re-encoding flag counts that are already visible. The trend was
|
|
805
|
+
// worse: `1 - violations*0.1` per round, computed from
|
|
806
|
+
// `constraint_violations`, i.e. a DIFFERENT source from the score it was
|
|
807
|
+
// displayed next to. "Trend" was therefore not the score's history at all.
|
|
808
|
+
// Neither fed a decision.
|
|
1111
809
|
const completedEntries = loopEntries(request.loop_id, context)
|
|
1112
810
|
.filter((e) => entryRound(e) >= 1 && entryRound(e) < request.round);
|
|
1113
|
-
const committedRounds =
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
const score = Math.max(0, 1.0 - viols.length * 0.1);
|
|
1119
|
-
trend.push(Number(score.toFixed(2)));
|
|
1120
|
-
}
|
|
1121
|
-
// v3.3: Round stats + machine git-motion status (display-only, derived).
|
|
1122
|
-
const roundStats = deriveRoundStats(committedRounds);
|
|
811
|
+
const committedRounds = derivationRounds(completedEntries, request.round);
|
|
812
|
+
// v3.3/v3.8.1: machine git-motion (display-only, derived). The per-round
|
|
813
|
+
// "round stats" table is deleted: its only renderer was the L2 prompt
|
|
814
|
+
// section removed above, so it was computed, put into the canonical state
|
|
815
|
+
// (and therefore into stateHash) and read by nothing.
|
|
1123
816
|
const machineStatus = deriveMachineStatus(committedRounds, request.round);
|
|
1124
|
-
// v3.4: ACTIVE Round Contract for this compile — derived from committed
|
|
1125
|
-
// rounds, so rejection retries / resume / unpause / backtrack compiles
|
|
1126
|
-
// (no last_round_result) keep showing the contract as Current Task, and a
|
|
1127
|
-
// satisfied or blocked contract stops being shown.
|
|
1128
|
-
const activeContract = deriveActiveContract(request.loop_id, context, request.round);
|
|
1129
817
|
const response = makeLoopCompileResponse({
|
|
1130
818
|
status: AgentStatus.OK,
|
|
1131
819
|
recompile_level: decision.level,
|
|
@@ -1139,16 +827,10 @@ export function compileLoop(request, context) {
|
|
|
1139
827
|
goal_id: deriveGoalId(request.loop_id, request.task, request.goal_id),
|
|
1140
828
|
goal_text_hash: computeGoalTextHash(request.task),
|
|
1141
829
|
loop_objective: objective,
|
|
1142
|
-
loop_health: health,
|
|
1143
|
-
task_alignment: alignment,
|
|
1144
830
|
rolling_summary: rolling,
|
|
1145
831
|
sub_goals: subGoals,
|
|
1146
832
|
criterion_statuses: criterionStatuses,
|
|
1147
|
-
|
|
1148
|
-
agent_trust_score: request.round > 1 ? Number(trustScore.toFixed(2)) : undefined,
|
|
1149
|
-
agent_trust_trend: request.round > 1 ? trend : [],
|
|
1150
|
-
suggested_next_task: request.last_round_result?.next_action?.trim() ||
|
|
1151
|
-
request.last_round_result?.emerged_subtasks?.[0] || "",
|
|
833
|
+
recurring_flags: recurringFlags,
|
|
1152
834
|
plan_source: request.plan_source,
|
|
1153
835
|
warnings,
|
|
1154
836
|
});
|
|
@@ -1161,10 +843,11 @@ export function compileLoop(request, context) {
|
|
|
1161
843
|
? `${policy.state_file.directory}/${request.loop_id}-state.md`
|
|
1162
844
|
: "";
|
|
1163
845
|
const state = createCanonicalLoopState(request, response, statePath, {
|
|
1164
|
-
roundStats,
|
|
1165
846
|
machineStatus,
|
|
1166
|
-
// v3.4:
|
|
1167
|
-
|
|
847
|
+
// v3.4/v3.8.1: the contract-fact bundle — the Current Task's source of
|
|
848
|
+
// truth, its verification view and the machine-verified sub-goal facts as
|
|
849
|
+
// ONE derivation. The static capability rides on the state itself.
|
|
850
|
+
roundFacts,
|
|
1168
851
|
});
|
|
1169
852
|
const markdown = renderCanonicalStateMarkdown(state, {
|
|
1170
853
|
// v3.7.1: retry attempts are marked in the derived view (attempt is
|
|
@@ -1174,13 +857,12 @@ export function compileLoop(request, context) {
|
|
|
1174
857
|
attempt: request.attempt,
|
|
1175
858
|
recoveryBrief: committedBacktrackBrief(request.loop_id, request.round, context),
|
|
1176
859
|
});
|
|
1177
|
-
//
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
: policy.prompt.l2_max_chars;
|
|
860
|
+
// v3.8.1: the L2 budget is a FIXED ceiling. The v2.4 adaptive budget
|
|
861
|
+
// scaled it by round count, milestone count and sub-goal count, which made
|
|
862
|
+
// the prompt's allowed content depend on how long the loop had been running
|
|
863
|
+
// rather than on the facts of this round — the same loop could render a
|
|
864
|
+
// section at round 5 and silently drop it at round 50.
|
|
865
|
+
//
|
|
1184
866
|
// v2.8: L2 pointer mode — when enabled, skip the monolithic markdown blob.
|
|
1185
867
|
// Structured L2 sections (milestones, sub-goals, trust, progress) still
|
|
1186
868
|
// render. The state file on disk is the durable source of truth.
|
|
@@ -1195,23 +877,17 @@ export function compileLoop(request, context) {
|
|
|
1195
877
|
budgets: {
|
|
1196
878
|
l0: policy.prompt.l0_max_chars,
|
|
1197
879
|
l1: policy.prompt.l1_max_chars,
|
|
1198
|
-
l2:
|
|
880
|
+
l2: policy.prompt.l2_max_chars,
|
|
1199
881
|
},
|
|
1200
882
|
attempt: request.attempt,
|
|
1201
|
-
selfEvaluationBlock: buildSelfEvalBlock(request.round,
|
|
883
|
+
selfEvaluationBlock: buildSelfEvalBlock(request.round, decision.level,
|
|
1202
884
|
// v3.4: restate the contract only when this prompt's Current Task IS
|
|
1203
885
|
// the ACTIVE contract (derived — not the previous submission's field,
|
|
1204
886
|
// which is a proposal and may differ from what this round executes).
|
|
1205
|
-
activeContract != null,
|
|
1206
|
-
// v3.5: L2-only declaration nudge when nothing is active (policy-gated;
|
|
1207
|
-
// mutually exclusive with the restate template above).
|
|
1208
|
-
decision.level === "l2" && activeContract === null &&
|
|
1209
|
-
getPolicy().prompt.contract_nudge_on_l2),
|
|
887
|
+
activeContract != null),
|
|
1210
888
|
fullStateMarkdown: l2Pointer ? undefined : markdown,
|
|
1211
889
|
// v2.9: Model's information needs from the previous round's SelfEvaluation
|
|
1212
890
|
promptRequests: request.last_round_result?.prompt_requests,
|
|
1213
|
-
// v3.2: Previous L1 presentation as the collapse diff baseline.
|
|
1214
|
-
presentedBaseline: readPresentedBaseline(request.loop_id, request.round, context),
|
|
1215
891
|
});
|
|
1216
892
|
response.prompt = artifact.renderedPrompt;
|
|
1217
893
|
response.prompt_artifact = artifact;
|