loopforge 3.0.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -143
- package/dist/audit.d.ts +50 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +131 -0
- package/dist/audit.js.map +1 -0
- package/dist/canonical-state.d.ts +98 -23
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +257 -181
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +40 -33
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +17 -0
- package/dist/cognitive-facts.d.ts.map +1 -0
- package/dist/cognitive-facts.js +99 -0
- package/dist/cognitive-facts.js.map +1 -0
- package/dist/cognitive-governance.d.ts +25 -33
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +38 -138
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +59 -0
- package/dist/committed-round.d.ts.map +1 -0
- package/dist/committed-round.js +222 -0
- package/dist/committed-round.js.map +1 -0
- package/dist/enforcement-gate.d.ts +78 -0
- package/dist/enforcement-gate.d.ts.map +1 -0
- package/dist/enforcement-gate.js +1109 -0
- package/dist/enforcement-gate.js.map +1 -0
- package/dist/engine.d.ts +111 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +887 -0
- package/dist/engine.js.map +1 -0
- package/dist/evidence-claims.d.ts +50 -0
- package/dist/evidence-claims.d.ts.map +1 -0
- package/dist/evidence-claims.js +116 -0
- package/dist/evidence-claims.js.map +1 -0
- package/dist/evidence-provider.d.ts +50 -23
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +167 -50
- package/dist/evidence-provider.js.map +1 -1
- package/dist/generate-schema.js +1 -84
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +19 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -15
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +58 -3
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +841 -304
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +2 -2
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +2 -2
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts +6 -0
- package/dist/loop-projection.d.ts.map +1 -0
- package/dist/loop-projection.js +26 -0
- package/dist/loop-projection.js.map +1 -0
- package/dist/loop-store.d.ts +115 -24
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +365 -218
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +214 -0
- package/dist/mcp/round-lifecycle.d.ts.map +1 -0
- package/dist/mcp/round-lifecycle.js +935 -0
- package/dist/mcp/round-lifecycle.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +29 -43
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +64 -101
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +433 -1110
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +20 -11
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +587 -413
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +12 -24
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +78 -184
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +61 -50
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +48 -146
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +37 -6
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +487 -278
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -8
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +8 -26
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +398 -236
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +147 -17
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +5 -13
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +32 -69
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +64 -0
- package/dist/round-contract.d.ts.map +1 -0
- package/dist/round-contract.js +80 -0
- package/dist/round-contract.js.map +1 -0
- package/dist/round-coordinator.d.ts +23 -25
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +103 -92
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +13 -13
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +25 -14
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +14 -18
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +37 -47
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +48 -0
- package/dist/self-eval.d.ts.map +1 -0
- package/dist/self-eval.js +248 -0
- package/dist/self-eval.js.map +1 -0
- package/dist/storage.d.ts +9 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +18 -6
- package/dist/storage.js.map +1 -1
- package/dist/token-utils.d.ts +15 -3
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +24 -5
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +149 -0
- package/dist/verification-gate.d.ts.map +1 -0
- package/dist/verification-gate.js +1343 -0
- package/dist/verification-gate.js.map +1 -0
- package/dist/workspace.d.ts +29 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +61 -0
- package/dist/workspace.js.map +1 -0
- package/loop_policy.json +37 -16
- package/package.json +5 -10
- package/skills/perception/SKILL.md +184 -202
- package/dist/canonical-event-writer.d.ts +0 -20
- package/dist/canonical-event-writer.d.ts.map +0 -1
- package/dist/canonical-event-writer.js +0 -131
- package/dist/canonical-event-writer.js.map +0 -1
- package/dist/compiler-subgoals.d.ts +0 -7
- package/dist/compiler-subgoals.d.ts.map +0 -1
- package/dist/compiler-subgoals.js +0 -93
- package/dist/compiler-subgoals.js.map +0 -1
- package/dist/derive-id.d.ts +0 -12
- package/dist/derive-id.d.ts.map +0 -1
- package/dist/derive-id.js +0 -31
- package/dist/derive-id.js.map +0 -1
- package/dist/mcp/session-projection.d.ts +0 -5
- package/dist/mcp/session-projection.d.ts.map +0 -1
- package/dist/mcp/session-projection.js +0 -18
- package/dist/mcp/session-projection.js.map +0 -1
- package/dist/mcp/session-queue.d.ts +0 -3
- package/dist/mcp/session-queue.d.ts.map +0 -1
- package/dist/mcp/session-queue.js +0 -18
- package/dist/mcp/session-queue.js.map +0 -1
- package/dist/mcp/session-types.d.ts +0 -72
- package/dist/mcp/session-types.d.ts.map +0 -1
- package/dist/mcp/session-types.js +0 -2
- package/dist/mcp/session-types.js.map +0 -1
- package/dist/round-commit-store.d.ts +0 -12
- package/dist/round-commit-store.d.ts.map +0 -1
- package/dist/round-commit-store.js +0 -13
- package/dist/round-commit-store.js.map +0 -1
- package/dist/round-gate.d.ts +0 -31
- package/dist/round-gate.d.ts.map +0 -1
- package/dist/round-gate.js +0 -259
- package/dist/round-gate.js.map +0 -1
- package/dist/round-recovery.d.ts +0 -17
- package/dist/round-recovery.d.ts.map +0 -1
- package/dist/round-recovery.js +0 -117
- package/dist/round-recovery.js.map +0 -1
- package/dist/round-report.d.ts +0 -16
- package/dist/round-report.d.ts.map +0 -1
- package/dist/round-report.js +0 -401
- package/dist/round-report.js.map +0 -1
- package/dist/runtime-services.d.ts +0 -59
- package/dist/runtime-services.d.ts.map +0 -1
- package/dist/runtime-services.js +0 -460
- package/dist/runtime-services.js.map +0 -1
- package/dist/vault-format.d.ts +0 -4
- package/dist/vault-format.d.ts.map +0 -1
- package/dist/vault-format.js +0 -4
- package/dist/vault-format.js.map +0 -1
- package/dist/vault-types.d.ts +0 -24
- package/dist/vault-types.d.ts.map +0 -1
- package/dist/vault-types.js +0 -2
- package/dist/vault-types.js.map +0 -1
- package/loopforge-policy.json +0 -340
package/dist/loop-compiler.js
CHANGED
|
@@ -3,23 +3,22 @@
|
|
|
3
3
|
* The compiler evolves structured state and renders one prompt artifact.
|
|
4
4
|
* L0/L1/L2 control state density only; the external Agent owns reasoning.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
7
|
import { getPolicy } from "./policy.js";
|
|
8
|
-
import { AgentStatus, makeLoopCompileResponse,
|
|
8
|
+
import { AgentStatus, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeConstraintMeta, makeMilestoneSummary, makeRollingSummary, makeSubGoal, makeTaskAlignment, } from "./protocol.js";
|
|
9
9
|
import { createCanonicalLoopState, renderCanonicalStateMarkdown, } from "./canonical-state.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { decodeCommittedRound, decodeMergedRound, machineGitMotionSeries, mergedRoundsFromEntries, } from "./committed-round.js";
|
|
11
|
+
import { contractRoundEvaluations, deriveActiveRoundContract, } from "./round-contract.js";
|
|
12
12
|
import { assemblePromptArtifact } from "./prompt-assembler.js";
|
|
13
13
|
import { decidePromptLevel, } from "./prompt-policy.js";
|
|
14
|
-
import { jaccardSimilarity, unique, entryRound } from "./token-utils.js";
|
|
15
|
-
import { manageSubGoals as deriveSubGoals } from "./compiler-subgoals.js";
|
|
14
|
+
import { deriveItemId, jaccardSimilarity, unique, entryRound } from "./token-utils.js";
|
|
16
15
|
function contextEntries(context) {
|
|
17
16
|
if (!context || !Array.isArray(context.results))
|
|
18
17
|
return [];
|
|
19
18
|
return context.results.filter((value) => value !== null && typeof value === "object" && !Array.isArray(value));
|
|
20
19
|
}
|
|
21
20
|
function lineage(entry) {
|
|
22
|
-
const value = entry.loop_lineage;
|
|
21
|
+
const value = entry.loop_lineage ?? entry.lineage;
|
|
23
22
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
24
23
|
? value
|
|
25
24
|
: {};
|
|
@@ -29,12 +28,38 @@ function loopEntries(loopId, context) {
|
|
|
29
28
|
.filter((entry) => entry.loop_id === loopId || lineage(entry).loop_id === loopId)
|
|
30
29
|
.sort((a, b) => entryRound(a) - entryRound(b));
|
|
31
30
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
/** v3.3.1: The canonical entry for a round — its compile-time lineage entry
|
|
32
|
+
* (task_type "loop_lineage"; engine hydration merges the committed
|
|
33
|
+
* decision into it, so one round = one merged lineage entry in the
|
|
34
|
+
* compiler's view). Event/journal entries (delegation journals, gate
|
|
35
|
+
* records) share lineage.round with the round but carry none of its
|
|
36
|
+
* compile-time fields; round-level consumers that matched on the round
|
|
37
|
+
* number alone could hit them first (they sort after the lineage entry of
|
|
38
|
+
* the same round), producing an empty goal_id / task / constraints_active
|
|
39
|
+
* and forcing spurious L2 recovery. A direct :feedback entry is accepted
|
|
40
|
+
* when the caller supplies a raw committed view instead of hydration. */
|
|
41
|
+
function roundCanonicalEntry(entries, round) {
|
|
42
|
+
const candidates = entries.filter((entry) => entryRound(entry) === round);
|
|
43
|
+
return (candidates.find((entry) => entry.task_type === "loop_lineage") ??
|
|
44
|
+
candidates.find((entry) => String(entry.task_id ?? "").endsWith(":feedback")) ??
|
|
45
|
+
// Untyped compiler inputs are treated as round views, not events. Known event
|
|
46
|
+
// types (delegation journals, gate records) are excluded explicitly so
|
|
47
|
+
// they can never shadow a round again.
|
|
48
|
+
candidates.find((entry) => {
|
|
49
|
+
const type = String(entry.task_type ?? "");
|
|
50
|
+
return type === "" || (!type.startsWith("delegation") && !type.endsWith("journal"));
|
|
51
|
+
}) ??
|
|
52
|
+
null);
|
|
53
|
+
}
|
|
54
|
+
/** Return the canonical read model when an entry is a committed round.
|
|
55
|
+
* Uncommitted compiler lineage has no durable decision yet and deliberately
|
|
56
|
+
* remains on the local compile path; it cannot be treated as history. */
|
|
57
|
+
function committedView(entry) {
|
|
58
|
+
return decodeMergedRound(entry) ?? decodeCommittedRound(entry);
|
|
35
59
|
}
|
|
36
60
|
export function computeGoalTextHash(text) {
|
|
37
|
-
|
|
61
|
+
const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
|
|
62
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
|
|
38
63
|
}
|
|
39
64
|
export function deriveGoalId(loopId, task, explicit = "") {
|
|
40
65
|
return explicit.trim() || `${loopId}:${task.trim().toLowerCase()
|
|
@@ -42,9 +67,34 @@ export function deriveGoalId(loopId, task, explicit = "") {
|
|
|
42
67
|
.replace(/^-|-$/g, "")
|
|
43
68
|
.slice(0, 48) || computeGoalTextHash(task)}`;
|
|
44
69
|
}
|
|
70
|
+
/** v3.2: Read the previous round's persisted L1 presentation snapshot — the
|
|
71
|
+
* diff baseline for L1 collapse. Returns null when the previous round's
|
|
72
|
+
* lineage entry lacks the three presented_* fields (as on L0/L2 compiles),
|
|
73
|
+
* so callers render the full L1 state. */
|
|
74
|
+
export function readPresentedBaseline(loopId, round, context) {
|
|
75
|
+
const entry = roundCanonicalEntry(loopEntries(loopId, context), round - 1);
|
|
76
|
+
if (!entry)
|
|
77
|
+
return null;
|
|
78
|
+
const data = lineage(entry);
|
|
79
|
+
const constraintIds = data.presented_constraint_ids;
|
|
80
|
+
const subGoals = data.presented_subgoals;
|
|
81
|
+
const milestoneRanges = data.presented_milestone_ranges;
|
|
82
|
+
if (!Array.isArray(constraintIds) || !constraintIds.every((v) => typeof v === "string") ||
|
|
83
|
+
!Array.isArray(subGoals) || !subGoals.every((v) => Array.isArray(v) && v.length === 2 &&
|
|
84
|
+
typeof v[0] === "string" && typeof v[1] === "string") ||
|
|
85
|
+
!Array.isArray(milestoneRanges) || !milestoneRanges.every((v) => Array.isArray(v) && v.length === 2 &&
|
|
86
|
+
typeof v[0] === "number" && typeof v[1] === "number")) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
round: round - 1,
|
|
91
|
+
constraintIds: constraintIds,
|
|
92
|
+
subGoals: subGoals,
|
|
93
|
+
milestoneRanges: milestoneRanges,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
45
96
|
export function getPreviousRound(loopId, round, context) {
|
|
46
|
-
const entry =
|
|
47
|
-
.find((candidate) => entryRound(candidate) === round);
|
|
97
|
+
const entry = roundCanonicalEntry(loopEntries(loopId, context), round);
|
|
48
98
|
if (!entry)
|
|
49
99
|
return null;
|
|
50
100
|
const data = lineage(entry);
|
|
@@ -61,7 +111,9 @@ export function getPreviousRound(loopId, round, context) {
|
|
|
61
111
|
constraints_active: Array.isArray(data.constraints_active)
|
|
62
112
|
? data.constraints_active.filter((value) => typeof value === "string")
|
|
63
113
|
: [],
|
|
64
|
-
output_summary:
|
|
114
|
+
output_summary: typeof entry.output_summary === "string"
|
|
115
|
+
? entry.output_summary
|
|
116
|
+
: typeof data.output_summary === "string" ? data.output_summary : "",
|
|
65
117
|
};
|
|
66
118
|
}
|
|
67
119
|
function latestObjective(loopId, context) {
|
|
@@ -98,17 +150,17 @@ function evolveObjective(request, context) {
|
|
|
98
150
|
created_at_round: 1,
|
|
99
151
|
loop_id: request.loop_id,
|
|
100
152
|
});
|
|
101
|
-
const last = request.
|
|
153
|
+
const last = request.last_round_result;
|
|
102
154
|
const history = [...(current.refinement_history ?? [])];
|
|
103
155
|
let objective = current.objective || request.task;
|
|
104
156
|
let version = current.version ?? 1;
|
|
105
|
-
if (last?.
|
|
106
|
-
const refinement = last.
|
|
157
|
+
if (last?.objective_refinement?.trim()) {
|
|
158
|
+
const refinement = last.objective_refinement.trim();
|
|
107
159
|
history.push(refinement);
|
|
108
160
|
objective = `${objective}\nRefinement: ${refinement}`;
|
|
109
161
|
version++;
|
|
110
162
|
}
|
|
111
|
-
const revisions = new Map((last?.
|
|
163
|
+
const revisions = new Map((last?.revised_success_criteria ?? []).map((item) => [item.old, item.new]));
|
|
112
164
|
return makeLoopObjective({
|
|
113
165
|
...current,
|
|
114
166
|
objective,
|
|
@@ -119,71 +171,257 @@ function evolveObjective(request, context) {
|
|
|
119
171
|
loop_id: request.loop_id,
|
|
120
172
|
});
|
|
121
173
|
}
|
|
122
|
-
function evolveConstraints(request, objective, previous) {
|
|
123
|
-
const retired = unique(request.
|
|
174
|
+
function evolveConstraints(request, objective, previous, context) {
|
|
175
|
+
const retired = unique(request.last_round_result?.retracted_constraints ?? []);
|
|
124
176
|
const retiredSet = new Set(retired);
|
|
125
|
-
const discovered = unique(request.
|
|
177
|
+
const discovered = unique(request.last_round_result?.discovered_constraints ?? [])
|
|
126
178
|
.slice(0, getPolicy().evolution.max_discovered_constraints_per_round);
|
|
179
|
+
// v2.14: agent-declared retractions of plan/hard/criteria constraints
|
|
180
|
+
// persist for constraints.retire_window rounds (they are re-merged from
|
|
181
|
+
// the request/objective every round, so without this the documented
|
|
182
|
+
// "Removed from the active constraint set" never held for them). The
|
|
183
|
+
// retraction rounds are derived from vault entries — no new persistence
|
|
184
|
+
// format. After the window the constraint returns (hard/plan/criteria
|
|
185
|
+
// constraints never auto-decay — only agent action removes them, and the
|
|
186
|
+
// action's memory expires).
|
|
187
|
+
const window = getPolicy().constraints.retire_window;
|
|
188
|
+
const lastRetractedRound = new Map();
|
|
189
|
+
if (window > 0) {
|
|
190
|
+
for (const entry of loopEntries(request.loop_id, context)) {
|
|
191
|
+
const retractedList = lineage(entry).retracted_constraints;
|
|
192
|
+
if (!Array.isArray(retractedList))
|
|
193
|
+
continue;
|
|
194
|
+
const rnd = entryRound(entry);
|
|
195
|
+
for (const text of retractedList) {
|
|
196
|
+
if (typeof text !== "string" || !text)
|
|
197
|
+
continue;
|
|
198
|
+
const prev = lastRetractedRound.get(text);
|
|
199
|
+
if (prev === undefined || rnd > prev)
|
|
200
|
+
lastRetractedRound.set(text, rnd);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
127
204
|
const active = unique([
|
|
128
205
|
...(previous?.constraints_active ?? []),
|
|
129
206
|
...request.constraints_from_plan,
|
|
130
207
|
...objective.hard_constraints,
|
|
131
208
|
...objective.success_criteria,
|
|
132
209
|
...discovered,
|
|
133
|
-
]).filter((item) =>
|
|
134
|
-
|
|
210
|
+
]).filter((item) => {
|
|
211
|
+
if (retiredSet.has(item))
|
|
212
|
+
return false;
|
|
213
|
+
const retractedRound = lastRetractedRound.get(item);
|
|
214
|
+
if (retractedRound !== undefined && request.round - retractedRound < window) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}).slice(0, getPolicy().evolution.max_active_constraints);
|
|
135
219
|
return { active, retired };
|
|
136
220
|
}
|
|
137
221
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
138
222
|
// Hierarchical Summary (v2.1)
|
|
139
223
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
224
|
+
/** Extract execution_evidence from a vault entry, handling both direct-field
|
|
225
|
+
* and lineage-nested shapes. Returns null when absent. */
|
|
226
|
+
function entryExecutionEvidence(entry) {
|
|
227
|
+
const view = committedView(entry);
|
|
228
|
+
if (view)
|
|
229
|
+
return view.executionEvidence;
|
|
230
|
+
const direct = entry.execution_evidence;
|
|
231
|
+
if (direct && typeof direct === "object" && !Array.isArray(direct)) {
|
|
232
|
+
return direct;
|
|
233
|
+
}
|
|
234
|
+
const lin = lineage(entry);
|
|
235
|
+
const nested = lin.execution_evidence;
|
|
236
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
237
|
+
return nested;
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
/** Read success_criteria_met from an entry's execution_evidence. */
|
|
140
242
|
function entryCriteriaMet(entry) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
243
|
+
const view = committedView(entry);
|
|
244
|
+
if (view)
|
|
245
|
+
return view.executionEvidence?.success_criteria_met
|
|
246
|
+
?.filter((value) => typeof value === "string") ?? [];
|
|
247
|
+
const ev = entryExecutionEvidence(entry);
|
|
248
|
+
if (!ev)
|
|
249
|
+
return [];
|
|
250
|
+
const arr = ev.success_criteria_met;
|
|
251
|
+
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
252
|
+
}
|
|
253
|
+
/** Read success_criteria_remaining from an entry's execution_evidence. */
|
|
254
|
+
function entryCriteriaRemaining(entry) {
|
|
255
|
+
const view = committedView(entry);
|
|
256
|
+
if (view)
|
|
257
|
+
return view.executionEvidence?.success_criteria_remaining
|
|
258
|
+
?.filter((value) => typeof value === "string") ?? [];
|
|
259
|
+
const ev = entryExecutionEvidence(entry);
|
|
260
|
+
if (!ev)
|
|
261
|
+
return [];
|
|
262
|
+
const arr = ev.success_criteria_remaining;
|
|
263
|
+
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
264
|
+
}
|
|
265
|
+
/** v3.2: Deterministic lessons learned — constraints violated repeatedly or
|
|
266
|
+
* verification checks failing repeatedly across rounds (full history, unlike
|
|
267
|
+
* the enforcement gate's R2 3-round window). Presentation only: the output
|
|
268
|
+
* never feeds enforcement decisions. */
|
|
269
|
+
export function deriveLessons(loopId, context, currentRound) {
|
|
270
|
+
const entries = loopEntries(loopId, context).filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
|
|
271
|
+
const violations = new Map();
|
|
272
|
+
const errors = new Map();
|
|
273
|
+
const warns = new Map();
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
const rnd = entryRound(entry);
|
|
276
|
+
const view = committedView(entry);
|
|
277
|
+
const viols = view
|
|
278
|
+
? view.constraintViolations ?? []
|
|
279
|
+
: Array.isArray(entry.constraint_violations)
|
|
280
|
+
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
281
|
+
: [];
|
|
282
|
+
for (const text of viols) {
|
|
283
|
+
const list = violations.get(text) ?? [];
|
|
284
|
+
list.push(rnd);
|
|
285
|
+
violations.set(text, list);
|
|
286
|
+
}
|
|
287
|
+
const flags = view
|
|
288
|
+
? view.verificationFlags
|
|
289
|
+
: Array.isArray(entry.verification_flags) ? entry.verification_flags : [];
|
|
290
|
+
for (const flag of flags) {
|
|
291
|
+
const target = flag.severity === "error"
|
|
292
|
+
? errors
|
|
293
|
+
: flag.severity === "warn" ? warns : null;
|
|
294
|
+
if (!target)
|
|
175
295
|
continue;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
296
|
+
const list = target.get(flag.check) ?? [];
|
|
297
|
+
list.push(rnd);
|
|
298
|
+
target.set(flag.check, list);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const lessons = [];
|
|
302
|
+
for (const [text, rounds] of violations) {
|
|
303
|
+
if (rounds.length >= 2) {
|
|
304
|
+
lessons.push({ text, kind: "constraint_violation", count: rounds.length, rounds });
|
|
179
305
|
}
|
|
180
306
|
}
|
|
181
|
-
|
|
307
|
+
for (const [text, rounds] of errors) {
|
|
308
|
+
if (rounds.length >= 2) {
|
|
309
|
+
lessons.push({ text, kind: "verification_error", count: rounds.length, rounds });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const [text, rounds] of warns) {
|
|
313
|
+
if (rounds.length >= 2) {
|
|
314
|
+
lessons.push({ text, kind: "verification_warning", count: rounds.length, rounds });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return lessons.sort((a, b) => b.count - a.count);
|
|
318
|
+
}
|
|
319
|
+
/** v3.3: Derive display-only round statistics over the last `window`
|
|
320
|
+
* committed rounds — files changed, rejected attempts (snapshot.attempt
|
|
321
|
+
* minus one: the successful commit's attempt counts the redo), and the
|
|
322
|
+
* self-reported progress delta vs the previous round. One row per round,
|
|
323
|
+
* preferring the :feedback entry when both lineage and feedback entries
|
|
324
|
+
* exist. Zero persistence — re-derived from committed entries. */
|
|
325
|
+
function deriveRoundStats(rounds, window = 5) {
|
|
326
|
+
const stats = [];
|
|
327
|
+
let prevProgress = null;
|
|
328
|
+
for (const round of rounds.slice(-window)) {
|
|
329
|
+
const ev = round.executionEvidence;
|
|
330
|
+
const files = ev?.files_changed;
|
|
331
|
+
const rawProgress = ev?.progress_estimate;
|
|
332
|
+
const progress = typeof rawProgress === "number" ? rawProgress : null;
|
|
333
|
+
stats.push({
|
|
334
|
+
round: round.round,
|
|
335
|
+
filesChangedCount: Array.isArray(files) ? files.length : null,
|
|
336
|
+
rejectedAttempts: round.attempt >= 1 ? round.attempt - 1 : null,
|
|
337
|
+
progressDelta: progress !== null && prevProgress !== null
|
|
338
|
+
? Number((progress - prevProgress).toFixed(3))
|
|
339
|
+
: null,
|
|
340
|
+
});
|
|
341
|
+
if (progress !== null)
|
|
342
|
+
prevProgress = progress;
|
|
343
|
+
}
|
|
344
|
+
return stats;
|
|
345
|
+
}
|
|
346
|
+
/** v3.3: Machine git-motion cross-check over the last 3 committed rounds
|
|
347
|
+
* (the R4 window). Undefined when no committed rounds carry git snapshots
|
|
348
|
+
* — the dashboard then shows no machine row. Display-only companion to the
|
|
349
|
+
* R4/R5 exculpatory signal. */
|
|
350
|
+
function deriveMachineStatus(rounds, currentRound) {
|
|
351
|
+
const series = machineGitMotionSeries(rounds, currentRound, 3);
|
|
352
|
+
if (series === null)
|
|
353
|
+
return undefined;
|
|
354
|
+
const motionRounds = series.filter(Boolean).length;
|
|
355
|
+
return { windowRounds: 3, gitMotion: motionRounds > 0, motionRounds };
|
|
356
|
+
}
|
|
357
|
+
/** v3.4: ACTIVE Round Contract for the compile round — derived from the
|
|
358
|
+
* committed evals of earlier rounds only, never from request data. Reads
|
|
359
|
+
* the hydrated merged lineage entries (one per round; a committed decision
|
|
360
|
+
* is merged onto the round's lineage entry with `committed_action` set),
|
|
361
|
+
* skipping rounds whose committed action was "backtrack" (a roll-back
|
|
362
|
+
* directive, not an executed round). Because it runs identically on every
|
|
363
|
+
* compile path — including rejection retries and resume / unpause /
|
|
364
|
+
* backtrack compiles that carry no last_round_result — a contract round's
|
|
365
|
+
* re-compiles keep showing its contract as the Current Task. Null when no
|
|
366
|
+
* active contract: the Current Task falls back to the original task text. */
|
|
367
|
+
function deriveActiveContract(loopId, context, currentRound) {
|
|
368
|
+
const entries = loopEntries(loopId, context);
|
|
369
|
+
const canonical = Array.from({ length: Math.max(0, currentRound - 1) }, (_, index) => roundCanonicalEntry(entries, index + 1)).filter((entry) => entry !== null);
|
|
370
|
+
return deriveActiveRoundContract(contractRoundEvaluations(mergedRoundsFromEntries(canonical, currentRound)));
|
|
371
|
+
}
|
|
372
|
+
/** v3.2: Derive per-criterion status — the "goal → criteria → evidence"
|
|
373
|
+
* vertical view. Each objective criterion gets: met/remaining/unknown
|
|
374
|
+
* (from per-round success_criteria_met/remaining reports, ID-first
|
|
375
|
+
* matching), the round it was first reported met, and any sub-goals whose
|
|
376
|
+
* description matches it (Jaccard). Zero persistence — re-derived from the
|
|
377
|
+
* vault every compile. */
|
|
378
|
+
export function deriveCriterionStatuses(loopId, context, objective, currentRound, subGoals, lastRoundResult) {
|
|
379
|
+
if (!objective || objective.success_criteria.length === 0)
|
|
380
|
+
return [];
|
|
381
|
+
const entries = loopEntries(loopId, context).filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
|
|
382
|
+
// The current round's report lives in last_round_result (the vault only
|
|
383
|
+
// gains it on commit); treat it as the report for round currentRound - 1.
|
|
384
|
+
const lastEv = lastRoundResult?.execution_evidence;
|
|
385
|
+
const lastMet = lastEv?.success_criteria_met ?? [];
|
|
386
|
+
const lastRemaining = lastEv?.success_criteria_remaining ?? [];
|
|
387
|
+
const lastEntry = entries[entries.length - 1] ?? null;
|
|
388
|
+
const lastEntryRemaining = lastEntry ? entryCriteriaRemaining(lastEntry) : [];
|
|
389
|
+
const matchThreshold = getPolicy().evolution.subgoal_match_threshold;
|
|
390
|
+
return objective.success_criteria.map((text) => {
|
|
391
|
+
let metAtRound = null;
|
|
392
|
+
for (const entry of entries) {
|
|
393
|
+
const rnd = entryRound(entry);
|
|
394
|
+
if (entryCriteriaMet(entry).some((met) => criteriaMatch(met, text))) {
|
|
395
|
+
metAtRound = rnd;
|
|
396
|
+
break; // first report wins — chronological scan
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (metAtRound === null && lastMet.some((met) => criteriaMatch(met, text))) {
|
|
400
|
+
metAtRound = currentRound - 1;
|
|
401
|
+
}
|
|
402
|
+
const status = metAtRound !== null
|
|
403
|
+
? "met"
|
|
404
|
+
: lastRemaining.some((remaining) => criteriaMatch(remaining, text)) ||
|
|
405
|
+
lastEntryRemaining.some((remaining) => criteriaMatch(remaining, text))
|
|
406
|
+
? "remaining"
|
|
407
|
+
: "unknown";
|
|
408
|
+
return {
|
|
409
|
+
id: deriveCriterionId(text),
|
|
410
|
+
text,
|
|
411
|
+
status,
|
|
412
|
+
...(metAtRound !== null ? { met_at_round: metAtRound } : {}),
|
|
413
|
+
related_subgoal_ids: subGoals
|
|
414
|
+
.filter((sg) => jaccardSimilarity(text, sg.description) >= matchThreshold)
|
|
415
|
+
.map((sg) => sg.id),
|
|
416
|
+
};
|
|
417
|
+
});
|
|
182
418
|
}
|
|
183
419
|
/** v2.11: Match two criterion references for deduplication.
|
|
184
420
|
* If either is a criterion ID (cr-XXXXXXXX), uses exact ID comparison.
|
|
185
|
-
* Otherwise falls back to Jaccard similarity.
|
|
186
|
-
|
|
421
|
+
* Otherwise falls back to Jaccard similarity.
|
|
422
|
+
* v3.3: exported for the verification gate's windowed criteria-completion
|
|
423
|
+
* scan (R4/R5 exculpatory cross-check). */
|
|
424
|
+
export function criteriaMatch(a, b) {
|
|
187
425
|
const aIsId = isCriterionId(a);
|
|
188
426
|
const bIsId = isCriterionId(b);
|
|
189
427
|
// Both are IDs — exact string comparison
|
|
@@ -197,13 +435,10 @@ function criteriaMatch(a, b) {
|
|
|
197
435
|
// Neither is an ID — Jaccard fallback
|
|
198
436
|
return jaccardSimilarity(a, b) >= getPolicy().evolution.criteria_dedup_threshold;
|
|
199
437
|
}
|
|
200
|
-
function isCriterionId(ref) {
|
|
201
|
-
return /^cr-[a-f0-9]{8}$/.test(ref);
|
|
202
|
-
}
|
|
203
438
|
/** Detect newly met criteria by comparing the current entry's
|
|
204
439
|
* success_criteria_met against the previous entry's.
|
|
205
440
|
* v2.11: ID-first matching (cr-XXXXXXXX) with Jaccard similarity
|
|
206
|
-
* fallback for
|
|
441
|
+
* fallback for natural-language references.
|
|
207
442
|
* Returns empty array when there is no previous entry — the first
|
|
208
443
|
* entry's criteria are the baseline, not a "new" event. */
|
|
209
444
|
function detectNewCriteria(current, previous) {
|
|
@@ -221,21 +456,42 @@ function detectNewCriteria(current, previous) {
|
|
|
221
456
|
}
|
|
222
457
|
/** Read constraints_active from an entry's lineage. */
|
|
223
458
|
function entryActiveConstraints(entry) {
|
|
459
|
+
const view = committedView(entry);
|
|
460
|
+
if (view)
|
|
461
|
+
return view.activeConstraints ?? [];
|
|
224
462
|
const lin = lineage(entry);
|
|
225
463
|
const arr = lin.constraints_active;
|
|
226
464
|
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
227
465
|
}
|
|
228
466
|
/** Read retracted_constraints from an entry. */
|
|
229
467
|
function entryRetractedConstraints(entry) {
|
|
230
|
-
|
|
468
|
+
const view = committedView(entry);
|
|
469
|
+
if (view)
|
|
470
|
+
return view.retractedConstraints ?? [];
|
|
471
|
+
const direct = entry.retracted_constraints;
|
|
472
|
+
if (Array.isArray(direct))
|
|
473
|
+
return direct.filter((v) => typeof v === "string");
|
|
474
|
+
const lin = lineage(entry);
|
|
475
|
+
const nested = lin.retracted_constraints;
|
|
476
|
+
if (Array.isArray(nested))
|
|
477
|
+
return nested.filter((v) => typeof v === "string");
|
|
478
|
+
return [];
|
|
231
479
|
}
|
|
480
|
+
/** Read progress_estimate from an entry's execution_evidence. */
|
|
232
481
|
function entryProgressEstimate(entry) {
|
|
233
|
-
|
|
482
|
+
const ev = entryExecutionEvidence(entry);
|
|
483
|
+
if (!ev)
|
|
484
|
+
return 0;
|
|
485
|
+
const pe = ev.progress_estimate;
|
|
486
|
+
return typeof pe === "number" ? pe : 0;
|
|
234
487
|
}
|
|
235
488
|
/** Build a single MilestoneSummary from a range of completed round entries. */
|
|
236
489
|
function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, kind) {
|
|
237
490
|
const last = phaseEntries.at(-1);
|
|
238
|
-
const
|
|
491
|
+
const lastView = last ? committedView(last) : null;
|
|
492
|
+
const outcomeRaw = lastView
|
|
493
|
+
? (lastView.outputSummary ?? "")
|
|
494
|
+
: last && typeof last.output_summary === "string" ? last.output_summary : "";
|
|
239
495
|
const outcomeMax = getPolicy().checkpoint.outcome_max_chars;
|
|
240
496
|
const outcome = outcomeRaw.length > outcomeMax
|
|
241
497
|
? outcomeRaw.slice(0, outcomeMax) + "…"
|
|
@@ -254,7 +510,56 @@ function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, ki
|
|
|
254
510
|
generated_at_round: endRound,
|
|
255
511
|
});
|
|
256
512
|
}
|
|
257
|
-
|
|
513
|
+
/** v3.0.1: Evenly sample `budget` entries from `mid`, preferring
|
|
514
|
+
* agent_declared milestones (human/agent-labeled anchors) first and filling
|
|
515
|
+
* the remainder at even time positions. The caller guarantees
|
|
516
|
+
* `mid.length > budget`. */
|
|
517
|
+
function sampleMiddleMilestones(mid, budget) {
|
|
518
|
+
if (budget <= 0)
|
|
519
|
+
return [];
|
|
520
|
+
if (mid.length <= budget)
|
|
521
|
+
return mid;
|
|
522
|
+
const chosen = [];
|
|
523
|
+
const used = new Set();
|
|
524
|
+
for (const m of mid) {
|
|
525
|
+
if (m.kind === "agent_declared") {
|
|
526
|
+
chosen.push(m);
|
|
527
|
+
used.add(m);
|
|
528
|
+
if (chosen.length >= budget)
|
|
529
|
+
return chosen;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const rest = mid.filter((m) => !used.has(m));
|
|
533
|
+
const need = budget - chosen.length;
|
|
534
|
+
for (let i = 0; i < need; i++) {
|
|
535
|
+
const idx = Math.floor(((i + 0.5) * rest.length) / need);
|
|
536
|
+
chosen.push(rest[idx]);
|
|
537
|
+
}
|
|
538
|
+
return chosen;
|
|
539
|
+
}
|
|
540
|
+
/** v3.0.1: L1 milestone sampling — when milestones exceed max_milestones,
|
|
541
|
+
* keep the oldest `milestone_head_count` (history anchors), the newest
|
|
542
|
+
* `milestone_tail_count` (current progress), and an even sample of the
|
|
543
|
+
* middle. L2 (full rehydration) is never sampled: the recovery view keeps
|
|
544
|
+
* every milestone as the loop's memory skeleton. */
|
|
545
|
+
function sampleMilestones(milestones, policy, level) {
|
|
546
|
+
if (level === "l2")
|
|
547
|
+
return milestones;
|
|
548
|
+
if (milestones.length <= policy.max_milestones)
|
|
549
|
+
return milestones;
|
|
550
|
+
const head = Math.max(0, policy.milestone_head_count);
|
|
551
|
+
const tail = Math.max(0, policy.milestone_tail_count);
|
|
552
|
+
const midBudget = Math.max(0, policy.max_milestones - head - tail);
|
|
553
|
+
if (midBudget === 0)
|
|
554
|
+
return milestones.slice(0, head).concat(milestones.slice(milestones.length - tail));
|
|
555
|
+
const mid = milestones.slice(head, milestones.length - tail);
|
|
556
|
+
return [
|
|
557
|
+
...milestones.slice(0, head),
|
|
558
|
+
...sampleMiddleMilestones(mid, midBudget),
|
|
559
|
+
...milestones.slice(milestones.length - tail),
|
|
560
|
+
];
|
|
561
|
+
}
|
|
562
|
+
export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0, level) {
|
|
258
563
|
const policy = getPolicy().summary;
|
|
259
564
|
const allEntries = loopEntries(loopId, context);
|
|
260
565
|
// ── Phase 1: Recent Window (preserves existing behavior) ──
|
|
@@ -267,51 +572,43 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
267
572
|
const outcomes = [];
|
|
268
573
|
const issues = [];
|
|
269
574
|
for (const entry of windowEntries) {
|
|
575
|
+
const view = committedView(entry);
|
|
270
576
|
const data = lineage(entry);
|
|
271
577
|
const round = entryRound(entry);
|
|
272
|
-
const success = entry.success ?? data.success;
|
|
273
|
-
const summary =
|
|
578
|
+
const success = view ? view.success : entry.success ?? data.success;
|
|
579
|
+
const summary = view
|
|
580
|
+
? view.outputSummary ?? ""
|
|
581
|
+
: typeof entry.output_summary === "string"
|
|
582
|
+
? entry.output_summary
|
|
583
|
+
: typeof data.output_summary === "string" ? data.output_summary : "";
|
|
584
|
+
// v2.12: Declared outcome wins the label; the default wording stays
|
|
585
|
+
// "accepted" for zero regression on existing loop output.
|
|
586
|
+
const declared = view ? view.outcome : entry.outcome;
|
|
587
|
+
const outcomeLabel = declared === "success" || declared === "partial" ||
|
|
588
|
+
declared === "failed" || declared === "blocked"
|
|
589
|
+
? declared
|
|
590
|
+
: success === false ? "failed" : "accepted";
|
|
274
591
|
if (summary)
|
|
275
|
-
outcomes.push(`[R${round}] ${
|
|
276
|
-
const violations =
|
|
592
|
+
outcomes.push(`[R${round}] ${outcomeLabel}: ${summary}`);
|
|
593
|
+
const violations = view
|
|
594
|
+
? view.constraintViolations ?? []
|
|
595
|
+
: Array.isArray(entry.constraint_violations)
|
|
596
|
+
? entry.constraint_violations
|
|
597
|
+
: Array.isArray(data.constraint_violations) ? data.constraint_violations : [];
|
|
277
598
|
issues.push(...violations.filter((item) => typeof item === "string"));
|
|
278
599
|
}
|
|
279
|
-
const issueCounts = new Map();
|
|
280
|
-
for (const issue of issues) {
|
|
281
|
-
const key = issue.trim().replace(/\s+/g, " ").toLowerCase();
|
|
282
|
-
if (!key)
|
|
283
|
-
continue;
|
|
284
|
-
const previous = issueCounts.get(key);
|
|
285
|
-
issueCounts.set(key, { text: previous?.text ?? issue.trim(), count: (previous?.count ?? 0) + 1 });
|
|
286
|
-
}
|
|
287
600
|
// ── Phase 2: Milestone Accumulation ──
|
|
288
601
|
// Scan all completed entries in round order. For each entry, check three
|
|
289
602
|
// trigger signals in priority order: agent_declared > criteria_milestone > auto.
|
|
290
603
|
const completedEntries = allEntries.filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
|
|
291
|
-
const failedGroups = [];
|
|
292
|
-
for (const entry of completedEntries) {
|
|
293
|
-
const data = lineage(entry);
|
|
294
|
-
if ((entry.success ?? data.success) !== false)
|
|
295
|
-
continue;
|
|
296
|
-
const text = (entryProjection(entry)?.summary ?? "").trim();
|
|
297
|
-
if (!text)
|
|
298
|
-
continue;
|
|
299
|
-
const group = failedGroups.find((candidate) => jaccardSimilarity(candidate.text, text) >= getPolicy().evolution.failed_pattern_match_threshold);
|
|
300
|
-
if (group)
|
|
301
|
-
group.count++;
|
|
302
|
-
else
|
|
303
|
-
failedGroups.push({ text, count: 1 });
|
|
304
|
-
}
|
|
305
604
|
const milestones = [];
|
|
306
605
|
let lastMilestoneRound = 0;
|
|
307
606
|
for (let i = 0; i < completedEntries.length; i++) {
|
|
308
607
|
const entry = completedEntries[i];
|
|
309
608
|
const rnd = entryRound(entry);
|
|
310
609
|
const lin = lineage(entry);
|
|
311
|
-
const report = parseRoundReport(entry.round_report);
|
|
312
610
|
// Signal 1: Agent-declared checkpoint
|
|
313
|
-
const isAgentCheckpoint =
|
|
314
|
-
lin.compression_checkpoint === true;
|
|
611
|
+
const isAgentCheckpoint = lin.compression_checkpoint === true;
|
|
315
612
|
// Signal 2: New criteria met (compare with previous vault entry)
|
|
316
613
|
const prev = i > 0 ? completedEntries[i - 1] : null;
|
|
317
614
|
const newCriteria = detectNewCriteria(entry, prev);
|
|
@@ -325,19 +622,25 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
325
622
|
? "agent_declared"
|
|
326
623
|
: "criteria_milestone";
|
|
327
624
|
const label = isAgentCheckpoint
|
|
328
|
-
? (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
: `Round ${rnd}`))
|
|
625
|
+
? (typeof lin.checkpoint_label === "string" && lin.checkpoint_label.trim()
|
|
626
|
+
? lin.checkpoint_label.trim()
|
|
627
|
+
: `Round ${rnd}`)
|
|
332
628
|
: `Completed: ${newCriteria.join(", ")}`;
|
|
333
629
|
milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, rnd, label, kind));
|
|
334
630
|
lastMilestoneRound = rnd;
|
|
335
631
|
}
|
|
336
632
|
}
|
|
337
|
-
// Signal 3: Safety net — auto milestone when gap exceeds milestone_interval
|
|
633
|
+
// Signal 3: Safety net — auto milestone when gap exceeds milestone_interval.
|
|
634
|
+
// v3.3.1: the policy contract (milestone_interval: "only fires when no
|
|
635
|
+
// agent_declared or criteria_milestone has been created for this many
|
|
636
|
+
// rounds") includes loops that NEVER created a milestone — but the guard
|
|
637
|
+
// `lastMilestoneRound > 0` made the gap start from the (nonexistent) first
|
|
638
|
+
// milestone, so a checkpoint-less loop could run forever with an empty
|
|
639
|
+
// Phase History and no memory skeleton. With lastMilestoneRound == 0 the
|
|
640
|
+
// gap is measured from round 1 and the safety net fires from the very
|
|
641
|
+
// first interval.
|
|
338
642
|
const gapSinceLast = (currentRound - 1) - lastMilestoneRound;
|
|
339
|
-
if (
|
|
340
|
-
gapSinceLast >= policy.milestone_interval &&
|
|
643
|
+
if (gapSinceLast >= policy.milestone_interval &&
|
|
341
644
|
completedEntries.length > 0) {
|
|
342
645
|
const startRound = lastMilestoneRound + 1;
|
|
343
646
|
const endRound = currentRound - 1;
|
|
@@ -349,42 +652,256 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
349
652
|
milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, endRound, `Rounds ${startRound}–${endRound}`, "auto"));
|
|
350
653
|
}
|
|
351
654
|
}
|
|
352
|
-
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const totalRounds = currentRound - 1;
|
|
357
|
-
const phaseCount = cappedMilestones.length;
|
|
358
|
-
const lastProgress = completedEntries.length > 0
|
|
359
|
-
? entryProgressEstimate(completedEntries[completedEntries.length - 1])
|
|
360
|
-
: 0;
|
|
361
|
-
const activeCount = completedEntries.length > 0
|
|
362
|
-
? entryActiveConstraints(completedEntries[completedEntries.length - 1]).length
|
|
363
|
-
: 0;
|
|
364
|
-
const totalResolved = cappedMilestones.reduce((sum, m) => sum + m.resolved_constraints.length, 0);
|
|
365
|
-
loopSynthesis =
|
|
366
|
-
`Loop spans ${totalRounds} rounds across ${phaseCount} phases. ` +
|
|
367
|
-
`Overall progress: ${(lastProgress * 100).toFixed(0)}%. ` +
|
|
368
|
-
`${activeCount} active constraints, ${totalResolved} resolved across all phases.`;
|
|
369
|
-
}
|
|
655
|
+
// v3.0.1: L1 keeps head anchors + newest tail + an even middle sample;
|
|
656
|
+
// L2 (full rehydration) keeps every milestone — the recovery view is the
|
|
657
|
+
// loop's memory skeleton and must not be truncated.
|
|
658
|
+
const cappedMilestones = sampleMilestones(milestones, policy, level);
|
|
370
659
|
if (windowEntries.length === 0 && cappedMilestones.length === 0)
|
|
371
660
|
return null;
|
|
372
661
|
return makeRollingSummary({
|
|
373
662
|
key_outcomes: unique(outcomes),
|
|
374
|
-
recurring_issues:
|
|
375
|
-
.filter((issue) => issue.count >= 2)
|
|
376
|
-
.map((issue) => `${issue.text} (${issue.count} rounds)`),
|
|
663
|
+
recurring_issues: unique(issues),
|
|
377
664
|
rounds_sampled: windowEntries.length,
|
|
378
665
|
generated_at_round: currentRound,
|
|
379
|
-
failed_patterns:
|
|
380
|
-
.filter((failure) => failure.count >= 2)
|
|
381
|
-
.map((failure) => `${failure.text} (${failure.count} attempts)`),
|
|
666
|
+
failed_patterns: [],
|
|
382
667
|
milestones: cappedMilestones,
|
|
383
|
-
loop_synthesis: loopSynthesis || undefined,
|
|
384
668
|
});
|
|
385
669
|
}
|
|
386
|
-
//
|
|
387
|
-
//
|
|
670
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
671
|
+
// Sub-Goal Structured Tracking (v2.2)
|
|
672
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
673
|
+
/** Derive a stable sub-goal ID from its description hash.
|
|
674
|
+
* Exported as the single source of truth (verification-gate imports it). */
|
|
675
|
+
export function deriveSubGoalId(description) {
|
|
676
|
+
return "sg-" + deriveItemId(description);
|
|
677
|
+
}
|
|
678
|
+
/** v2.11: Derive a stable constraint ID from its text hash (c-XXXXXXXX).
|
|
679
|
+
* Same hash strategy as SubGoal — deterministic across rounds. */
|
|
680
|
+
export function deriveConstraintId(text) {
|
|
681
|
+
return "c-" + deriveItemId(text);
|
|
682
|
+
}
|
|
683
|
+
/** v2.11: Derive a stable criterion ID from its text hash (cr-XXXXXXXX).
|
|
684
|
+
* Same hash strategy as SubGoal — deterministic across rounds. */
|
|
685
|
+
export function deriveCriterionId(text) {
|
|
686
|
+
return "cr-" + deriveItemId(text);
|
|
687
|
+
}
|
|
688
|
+
/** v2.11: Check whether a user-provided reference looks like a constraint ID.
|
|
689
|
+
* Matches the pattern c-XXXXXXXX where X is a hex digit. */
|
|
690
|
+
export function isConstraintId(ref) {
|
|
691
|
+
return /^c-[a-f0-9]{8}$/.test(ref);
|
|
692
|
+
}
|
|
693
|
+
/** v2.11: Check whether a user-provided reference looks like a criterion ID.
|
|
694
|
+
* Matches the pattern cr-XXXXXXXX where X is a hex digit. */
|
|
695
|
+
export function isCriterionId(ref) {
|
|
696
|
+
return /^cr-[a-f0-9]{8}$/.test(ref);
|
|
697
|
+
}
|
|
698
|
+
/** Read emerged_subtasks from a vault entry (handles direct + lineage nesting). */
|
|
699
|
+
function entryEmergedSubtasks(entry) {
|
|
700
|
+
const view = committedView(entry);
|
|
701
|
+
if (view)
|
|
702
|
+
return view.emergedSubtasks ?? [];
|
|
703
|
+
const direct = entry.emerged_subtasks;
|
|
704
|
+
if (Array.isArray(direct))
|
|
705
|
+
return direct.filter((v) => typeof v === "string");
|
|
706
|
+
const lin = lineage(entry);
|
|
707
|
+
const nested = lin.emerged_subtasks;
|
|
708
|
+
if (Array.isArray(nested))
|
|
709
|
+
return nested.filter((v) => typeof v === "string");
|
|
710
|
+
return [];
|
|
711
|
+
}
|
|
712
|
+
/** Read completed_subtasks from an entry. */
|
|
713
|
+
function entryCompletedSubtasks(entry) {
|
|
714
|
+
const view = committedView(entry);
|
|
715
|
+
if (view)
|
|
716
|
+
return view.completedSubtasks ?? [];
|
|
717
|
+
const direct = entry.completed_subtasks;
|
|
718
|
+
if (Array.isArray(direct))
|
|
719
|
+
return direct.filter((v) => typeof v === "string");
|
|
720
|
+
return [];
|
|
721
|
+
}
|
|
722
|
+
/** Read blocked_subtasks from an entry. */
|
|
723
|
+
function entryBlockedSubtasks(entry) {
|
|
724
|
+
const view = committedView(entry);
|
|
725
|
+
if (view)
|
|
726
|
+
return view.blockedSubtasks ?? [];
|
|
727
|
+
const direct = entry.blocked_subtasks;
|
|
728
|
+
if (Array.isArray(direct))
|
|
729
|
+
return direct.filter((v) => typeof v === "string");
|
|
730
|
+
return [];
|
|
731
|
+
}
|
|
732
|
+
/** Read canceled_subtasks from an entry. */
|
|
733
|
+
function entryCanceledSubtasks(entry) {
|
|
734
|
+
const view = committedView(entry);
|
|
735
|
+
if (view)
|
|
736
|
+
return view.canceledSubtasks ?? [];
|
|
737
|
+
const direct = entry.canceled_subtasks;
|
|
738
|
+
if (Array.isArray(direct))
|
|
739
|
+
return direct.filter((v) => typeof v === "string");
|
|
740
|
+
return [];
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Match a user-provided sub-task reference against existing SubGoals.
|
|
744
|
+
*
|
|
745
|
+
* Tries exact SubGoal ID match first (e.g. "sg-a3f2b1c0"), then falls
|
|
746
|
+
* back to Jaccard token similarity on descriptions. Returns the index
|
|
747
|
+
* of the best-matching SubGoal, or -1 when no match reaches threshold.
|
|
748
|
+
*/
|
|
749
|
+
function matchSubGoal(target, subGoals, threshold) {
|
|
750
|
+
const trimmed = target.trim();
|
|
751
|
+
// Phase 1: Exact ID match
|
|
752
|
+
for (let i = 0; i < subGoals.length; i++) {
|
|
753
|
+
if (subGoals[i].id === trimmed)
|
|
754
|
+
return i;
|
|
755
|
+
}
|
|
756
|
+
// Phase 2: Jaccard fallback on description text
|
|
757
|
+
let bestIdx = -1;
|
|
758
|
+
let bestScore = 0;
|
|
759
|
+
for (let i = 0; i < subGoals.length; i++) {
|
|
760
|
+
const score = jaccardSimilarity(trimmed, subGoals[i].description);
|
|
761
|
+
if (score > bestScore) {
|
|
762
|
+
bestScore = score;
|
|
763
|
+
bestIdx = i;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return bestScore >= threshold ? bestIdx : -1;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Manage the full sub-goal lifecycle across rounds.
|
|
770
|
+
*
|
|
771
|
+
* Accumulates sub-goals from all prior vault entries, processes the current
|
|
772
|
+
* round's declarations, derives status changes, and returns the complete
|
|
773
|
+
* structured SubGoal list.
|
|
774
|
+
*
|
|
775
|
+
* Status derivation priority (compiler-driven):
|
|
776
|
+
* 1. Agent declared done/canceled/blocked → explicit status change
|
|
777
|
+
* 2. next_action match → auto in_progress
|
|
778
|
+
* 3. success_criteria_met match → auto done
|
|
779
|
+
*/
|
|
780
|
+
function manageSubGoals(loopId, currentRound, lastRoundResult, vaultContext) {
|
|
781
|
+
const policy = getPolicy().evolution;
|
|
782
|
+
const allEntries = loopEntries(loopId, vaultContext);
|
|
783
|
+
const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
|
|
784
|
+
const subGoals = [];
|
|
785
|
+
// Phase 1 — Accumulate historical sub-goals from vault entries
|
|
786
|
+
for (const entry of completedEntries) {
|
|
787
|
+
const rnd = entryRound(entry);
|
|
788
|
+
const emerged = entryEmergedSubtasks(entry);
|
|
789
|
+
for (const desc of emerged) {
|
|
790
|
+
const id = deriveSubGoalId(desc);
|
|
791
|
+
// Dedup: skip if an existing sub-goal is too similar
|
|
792
|
+
if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
|
|
793
|
+
continue;
|
|
794
|
+
subGoals.push(makeSubGoal({
|
|
795
|
+
id,
|
|
796
|
+
description: desc.trim(),
|
|
797
|
+
status: "pending",
|
|
798
|
+
declared_at_round: rnd,
|
|
799
|
+
status_changed_at_round: rnd,
|
|
800
|
+
priority: subGoals.length,
|
|
801
|
+
}));
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
// Phase 2 — Process current round's sub-task declarations
|
|
805
|
+
if (lastRoundResult) {
|
|
806
|
+
const rnd = lastRoundResult.round || (currentRound - 1);
|
|
807
|
+
// New sub-goals from this round
|
|
808
|
+
const emerged = lastRoundResult.emerged_subtasks ?? [];
|
|
809
|
+
for (const desc of emerged) {
|
|
810
|
+
if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
|
|
811
|
+
continue;
|
|
812
|
+
subGoals.push(makeSubGoal({
|
|
813
|
+
id: deriveSubGoalId(desc),
|
|
814
|
+
description: desc.trim(),
|
|
815
|
+
status: "pending",
|
|
816
|
+
declared_at_round: rnd,
|
|
817
|
+
status_changed_at_round: rnd,
|
|
818
|
+
priority: subGoals.length,
|
|
819
|
+
}));
|
|
820
|
+
}
|
|
821
|
+
// Completed sub-goals
|
|
822
|
+
const completed = lastRoundResult.completed_subtasks ?? [];
|
|
823
|
+
for (const desc of completed) {
|
|
824
|
+
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
825
|
+
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
826
|
+
subGoals[idx].status = "done";
|
|
827
|
+
subGoals[idx].status_changed_at_round = rnd;
|
|
828
|
+
subGoals[idx].completed_at_round = rnd;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// Blocked sub-goals
|
|
832
|
+
const blocked = lastRoundResult.blocked_subtasks ?? [];
|
|
833
|
+
for (const desc of blocked) {
|
|
834
|
+
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
835
|
+
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
836
|
+
subGoals[idx].status = "blocked";
|
|
837
|
+
subGoals[idx].status_changed_at_round = rnd;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
// Canceled sub-goals
|
|
841
|
+
const canceled = lastRoundResult.canceled_subtasks ?? [];
|
|
842
|
+
for (const desc of canceled) {
|
|
843
|
+
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
844
|
+
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
845
|
+
subGoals[idx].status = "canceled";
|
|
846
|
+
subGoals[idx].status_changed_at_round = rnd;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// Phase 3 — Auto-derive: next_action match → in_progress
|
|
850
|
+
const nextAction = lastRoundResult.next_action?.trim();
|
|
851
|
+
if (nextAction) {
|
|
852
|
+
for (const sg of subGoals) {
|
|
853
|
+
if (sg.status === "pending") {
|
|
854
|
+
if (jaccardSimilarity(nextAction, sg.description) >= policy.subgoal_auto_in_progress_threshold) {
|
|
855
|
+
sg.status = "in_progress";
|
|
856
|
+
sg.status_changed_at_round = rnd;
|
|
857
|
+
break; // Only one sub-goal can be in_progress at a time
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
// Phase 4 — Auto-complete: new criteria match → done
|
|
863
|
+
const newCriteria = lastRoundResult.execution_evidence?.success_criteria_met ?? [];
|
|
864
|
+
if (newCriteria.length > 0) {
|
|
865
|
+
for (const sg of subGoals) {
|
|
866
|
+
if (sg.status === "pending" || sg.status === "in_progress") {
|
|
867
|
+
for (const crit of newCriteria) {
|
|
868
|
+
if (jaccardSimilarity(crit, sg.description) >= policy.subgoal_auto_complete_threshold) {
|
|
869
|
+
sg.status = "done";
|
|
870
|
+
sg.status_changed_at_round = rnd;
|
|
871
|
+
sg.completed_at_round = rnd;
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
// Sort for display: in_progress first, then pending by age desc, then blocked, then done, then canceled
|
|
880
|
+
subGoals.sort((a, b) => {
|
|
881
|
+
const rank = (s) => {
|
|
882
|
+
switch (s.status) {
|
|
883
|
+
case "in_progress": return 0;
|
|
884
|
+
case "pending": return 1;
|
|
885
|
+
case "blocked": return 2;
|
|
886
|
+
case "done": return 3;
|
|
887
|
+
case "canceled": return 4;
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
const ra = rank(a), rb = rank(b);
|
|
891
|
+
if (ra !== rb)
|
|
892
|
+
return ra - rb;
|
|
893
|
+
// Within same status: pending by oldest first, done by most recent first
|
|
894
|
+
// (v2.14: the previous expression simplified to b.declared - a.declared,
|
|
895
|
+
// which sorted NEWEST first despite the comment)
|
|
896
|
+
if (a.status === "pending")
|
|
897
|
+
return a.declared_at_round - b.declared_at_round;
|
|
898
|
+
if (a.status === "done")
|
|
899
|
+
return (b.completed_at_round ?? 0) - (a.completed_at_round ?? 0);
|
|
900
|
+
return a.declared_at_round - b.declared_at_round;
|
|
901
|
+
});
|
|
902
|
+
return subGoals;
|
|
903
|
+
}
|
|
904
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
388
905
|
// Time-Aware Constraint Management (v2.3)
|
|
389
906
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
390
907
|
/** Determine the source of a constraint by matching against known sources. */
|
|
@@ -409,7 +926,12 @@ function findDiscoveredRound(text, vaultEntries) {
|
|
|
409
926
|
const rnd = entryRound(entry);
|
|
410
927
|
if (rnd < 1)
|
|
411
928
|
continue;
|
|
412
|
-
const
|
|
929
|
+
const view = committedView(entry);
|
|
930
|
+
const discovered = view
|
|
931
|
+
? view.discoveredConstraints ?? []
|
|
932
|
+
: Array.isArray(entry.discovered_constraints)
|
|
933
|
+
? entry.discovered_constraints.filter((v) => typeof v === "string")
|
|
934
|
+
: [];
|
|
413
935
|
for (const d of discovered) {
|
|
414
936
|
// Phase 1: exact ID match (v2.11)
|
|
415
937
|
if (d === targetId || deriveConstraintId(d) === targetId) {
|
|
@@ -417,7 +939,7 @@ function findDiscoveredRound(text, vaultEntries) {
|
|
|
417
939
|
earliest = rnd;
|
|
418
940
|
break;
|
|
419
941
|
}
|
|
420
|
-
// Phase 2: Jaccard fallback
|
|
942
|
+
// Phase 2: Jaccard fallback for natural-language references.
|
|
421
943
|
if (jaccardSimilarity(text, d) >= getPolicy().evolution.constraint_match_threshold) {
|
|
422
944
|
if (earliest === 0 || rnd < earliest)
|
|
423
945
|
earliest = rnd;
|
|
@@ -431,23 +953,30 @@ function findDiscoveredRound(text, vaultEntries) {
|
|
|
431
953
|
* v2.11: ID-first matching. Checks exact constraint ID match (c-XXXXXXXX)
|
|
432
954
|
* against the derived ID of each vault entry's constraint_violations, then
|
|
433
955
|
* falls back to Jaccard similarity on text. Returns 0 if never violated. */
|
|
434
|
-
|
|
956
|
+
/** True when a reported violation text matches a constraint text — same
|
|
957
|
+
* ID-first + Jaccard fallback matching as findLastViolatedRound. */
|
|
958
|
+
function matchesConstraintText(text, violation) {
|
|
435
959
|
const targetId = deriveConstraintId(text);
|
|
960
|
+
if (violation === targetId || deriveConstraintId(violation) === targetId) {
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
return jaccardSimilarity(text, violation) >=
|
|
964
|
+
getPolicy().evolution.constraint_match_threshold;
|
|
965
|
+
}
|
|
966
|
+
function findLastViolatedRound(text, vaultEntries) {
|
|
436
967
|
let latest = 0;
|
|
437
968
|
for (const entry of vaultEntries) {
|
|
438
969
|
const rnd = entryRound(entry);
|
|
439
970
|
if (rnd < 1)
|
|
440
971
|
continue;
|
|
441
|
-
const
|
|
972
|
+
const view = committedView(entry);
|
|
973
|
+
const violations = view
|
|
974
|
+
? view.constraintViolations ?? []
|
|
975
|
+
: Array.isArray(entry.constraint_violations)
|
|
976
|
+
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
977
|
+
: [];
|
|
442
978
|
for (const v of violations) {
|
|
443
|
-
|
|
444
|
-
if (v === targetId || deriveConstraintId(v) === targetId) {
|
|
445
|
-
if (rnd > latest)
|
|
446
|
-
latest = rnd;
|
|
447
|
-
break;
|
|
448
|
-
}
|
|
449
|
-
// Phase 2: Jaccard fallback (backward compatibility)
|
|
450
|
-
if (jaccardSimilarity(text, v) >= getPolicy().evolution.constraint_match_threshold) {
|
|
979
|
+
if (matchesConstraintText(text, v)) {
|
|
451
980
|
if (rnd > latest)
|
|
452
981
|
latest = rnd;
|
|
453
982
|
break;
|
|
@@ -464,20 +993,36 @@ function findLastViolatedRound(text, vaultEntries) {
|
|
|
464
993
|
* constraints never auto-decay. Inactive constraints are auto-reactivated
|
|
465
994
|
* if violated again in the current round.
|
|
466
995
|
*/
|
|
467
|
-
function manageConstraintLifecycle(active, objective, constraintsFromPlan, vaultContext, currentRound) {
|
|
996
|
+
function manageConstraintLifecycle(active, objective, constraintsFromPlan, vaultContext, currentRound, lastRoundViolations = []) {
|
|
468
997
|
const policy = getPolicy().evolution;
|
|
469
998
|
const allEntries = loopEntries(objective.loop_id || "", vaultContext);
|
|
470
999
|
const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
|
|
471
1000
|
const metadata = [];
|
|
472
1001
|
for (const text of active) {
|
|
473
1002
|
const source = constraintSource(text, objective, constraintsFromPlan);
|
|
1003
|
+
// v3.2.1: a violation reported in THIS round (last_round_result) counts
|
|
1004
|
+
// as a violation at currentRound — the previous rounds' scan cannot see
|
|
1005
|
+
// it (this round is not committed yet). This makes the L1 collapse
|
|
1006
|
+
// render "(violated this round)" (diffConstraints compares
|
|
1007
|
+
// last_violated_at_round === round) and re-activates decayed
|
|
1008
|
+
// discovered constraints.
|
|
1009
|
+
const violatedThisRound = lastRoundViolations.some((v) => matchesConstraintText(text, v));
|
|
474
1010
|
// Structural constraints never decay
|
|
475
1011
|
if (source === "hard" || source === "plan" || source === "criteria") {
|
|
1012
|
+
// v3.3.1: one text, one identity — a success-criterion text that also
|
|
1013
|
+
// lives in the merged active set belongs to the cr-XXXXXXXX namespace
|
|
1014
|
+
// (its own Success Criteria section and the verification gate's
|
|
1015
|
+
// criteriaMatch are ID-first on cr-). Rendering it under c-XXXXXXXX in
|
|
1016
|
+
// the active list meant the same text carried two IDs in one prompt,
|
|
1017
|
+
// and an agent echoing the active-list ID into success_criteria_met
|
|
1018
|
+
// could never match.
|
|
476
1019
|
metadata.push(makeConstraintMeta({
|
|
477
|
-
id: deriveConstraintId(text),
|
|
1020
|
+
id: source === "criteria" ? deriveCriterionId(text) : deriveConstraintId(text),
|
|
478
1021
|
text,
|
|
479
1022
|
discovered_at_round: objective.created_at_round || 1,
|
|
480
|
-
last_violated_at_round:
|
|
1023
|
+
last_violated_at_round: violatedThisRound
|
|
1024
|
+
? currentRound
|
|
1025
|
+
: findLastViolatedRound(text, completedEntries),
|
|
481
1026
|
source,
|
|
482
1027
|
status: "active",
|
|
483
1028
|
}));
|
|
@@ -485,7 +1030,9 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
|
|
|
485
1030
|
}
|
|
486
1031
|
// Discovered constraints: check for inactivity
|
|
487
1032
|
const discoveredAt = findDiscoveredRound(text, completedEntries);
|
|
488
|
-
const lastViolated =
|
|
1033
|
+
const lastViolated = violatedThisRound
|
|
1034
|
+
? currentRound
|
|
1035
|
+
: findLastViolatedRound(text, completedEntries);
|
|
489
1036
|
const roundsSinceRelevant = lastViolated > 0
|
|
490
1037
|
? currentRound - lastViolated
|
|
491
1038
|
: currentRound - (discoveredAt || currentRound);
|
|
@@ -513,21 +1060,25 @@ export function alignTask(proposedTask, request, context) {
|
|
|
513
1060
|
const objective = request.loop_objective ?? latestObjective(request.loop_id, context);
|
|
514
1061
|
if (!objective)
|
|
515
1062
|
return makeTaskAlignment();
|
|
1063
|
+
// v2.14: threshold is policy-driven (evolution.progress_mismatch_threshold)
|
|
1064
|
+
const threshold = getPolicy().evolution.progress_mismatch_threshold;
|
|
516
1065
|
const score = jaccardSimilarity(proposedTask, [objective.objective, ...objective.success_criteria, ...objective.hard_constraints].join(" "));
|
|
517
1066
|
return makeTaskAlignment({
|
|
518
|
-
is_aligned: score >=
|
|
1067
|
+
is_aligned: score >= threshold,
|
|
519
1068
|
alignment_score: Number(score.toFixed(2)),
|
|
520
|
-
warning: score <
|
|
521
|
-
escalation: score <
|
|
1069
|
+
warning: score < threshold ? "Current task may be drifting from the loop objective." : "",
|
|
1070
|
+
escalation: score < threshold ? "warn" : "none",
|
|
522
1071
|
});
|
|
523
1072
|
}
|
|
524
1073
|
export function checkLoopHealth(loopId, request, context) {
|
|
525
1074
|
const previous = getPreviousRound(loopId, request.round - 1, context);
|
|
526
1075
|
const alignment = alignTask(request.task, request, context);
|
|
527
|
-
const violations = request.
|
|
1076
|
+
const violations = request.last_round_result?.constraint_violations.length ?? 0;
|
|
528
1077
|
const integrity = Math.max(0, 1 - violations * 0.2);
|
|
529
1078
|
const continuity = previous ? jaccardSimilarity(previous.task, request.task) : 1;
|
|
530
|
-
|
|
1079
|
+
// v2.14: thresholds are policy-driven
|
|
1080
|
+
const drift = alignment.alignment_score < getPolicy().evolution.progress_mismatch_threshold ||
|
|
1081
|
+
continuity < getPolicy().evolution.task_continuity_threshold;
|
|
531
1082
|
return makeLoopHealth({
|
|
532
1083
|
goal_alignment: alignment.alignment_score,
|
|
533
1084
|
constraint_integrity: integrity,
|
|
@@ -539,132 +1090,144 @@ export function checkLoopHealth(loopId, request, context) {
|
|
|
539
1090
|
}
|
|
540
1091
|
function levelDecision(request, context) {
|
|
541
1092
|
const previous = getPreviousRound(request.loop_id, request.round - 1, context);
|
|
542
|
-
const last = request.
|
|
1093
|
+
const last = request.last_round_result;
|
|
543
1094
|
const hasNewInformation = Boolean(request.new_since_last_round.trim() ||
|
|
544
|
-
last?.
|
|
545
|
-
last?.
|
|
546
|
-
last?.
|
|
547
|
-
last?.
|
|
548
|
-
last?.
|
|
549
|
-
last?.
|
|
550
|
-
last?.evidence?.verifiedClaims.length);
|
|
1095
|
+
last?.discovered_constraints?.length ||
|
|
1096
|
+
last?.objective_refinement?.trim() ||
|
|
1097
|
+
last?.emerged_subtasks?.length ||
|
|
1098
|
+
last?.retracted_constraints?.length ||
|
|
1099
|
+
last?.revised_success_criteria?.length ||
|
|
1100
|
+
last?.wrong_assumptions?.length);
|
|
551
1101
|
const lastFullRound = loopEntries(request.loop_id, context)
|
|
552
1102
|
.filter((entry) => lineage(entry).recompile_level === "l2")
|
|
553
1103
|
.map(entryRound)
|
|
554
1104
|
.filter((round) => round < request.round)
|
|
555
1105
|
.at(-1) ?? 1;
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
entryProjection(entry)?.discoveries?.newConstraints.length ||
|
|
564
|
-
entryProjection(entry)?.discoveries?.emergedWork.length);
|
|
565
|
-
});
|
|
566
|
-
const meaningfulSections = 2 + [
|
|
567
|
-
request.task.trim(),
|
|
568
|
-
...(last?.violations ?? []),
|
|
569
|
-
...(last?.evidence?.criteriaRemaining ?? []),
|
|
570
|
-
...(last?.evidence?.criteriaMet ?? []),
|
|
571
|
-
...(last?.discoveries?.newConstraints ?? []),
|
|
572
|
-
...(last?.discoveries?.emergedWork ?? []),
|
|
573
|
-
...(last?.evidence?.observedFiles ?? []),
|
|
574
|
-
].filter(Boolean).length;
|
|
575
|
-
const promptPolicy = getPolicy().prompt;
|
|
1106
|
+
// v2.14: a backtrack committed for this round means the loop is
|
|
1107
|
+
// re-walking restored state — the recovery boundary forces L2
|
|
1108
|
+
// rehydration (the decidePromptLevel recovery_boundary branch existed
|
|
1109
|
+
// but no caller ever set it).
|
|
1110
|
+
const recoveryBoundary = loopEntries(request.loop_id, context)
|
|
1111
|
+
.some((entry) => lineage(entry).committed_action === "backtrack" &&
|
|
1112
|
+
entryRound(entry) === request.round);
|
|
576
1113
|
return decidePromptLevel({
|
|
577
1114
|
round: request.round,
|
|
578
1115
|
attempt: request.attempt,
|
|
579
1116
|
forceLevel: request.force_level,
|
|
580
1117
|
hasPlanSource: Boolean(request.plan_source),
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
constraintChanged: Boolean(last?.discoveries?.newConstraints?.length || last?.constraintEvents?.retired?.length),
|
|
1118
|
+
recoveryBoundary,
|
|
1119
|
+
checkpointBoundary: last?.compression_checkpoint === true,
|
|
1120
|
+
goalChanged: previous !== null && previous.goal_id !== deriveGoalId(request.loop_id, request.task, request.goal_id),
|
|
585
1121
|
previousStateMissing: request.round > 1 && previous === null,
|
|
586
|
-
previousFailedWithoutNewInformation: last?.
|
|
1122
|
+
previousFailedWithoutNewInformation: last?.success === false && !hasNewInformation,
|
|
587
1123
|
verificationContradicted: (request.verification_flags ?? [])
|
|
588
1124
|
.some((flag) => flag.severity === "error"),
|
|
589
1125
|
consecutiveRejections: request.consecutive_rejections,
|
|
590
1126
|
fullRefreshInterval: getPolicy().prompt.full_refresh_interval,
|
|
591
1127
|
lastFullRound,
|
|
592
|
-
l1Streak: Math.max(0, request.round - lastFullRound - 1),
|
|
593
|
-
maxL1Streak: promptPolicy.adaptive_refresh ? promptPolicy.max_l1_streak : undefined,
|
|
594
|
-
meaningfulSections: request.round <= 2
|
|
595
|
-
? Math.max(meaningfulSections, promptPolicy.min_meaningful_sections)
|
|
596
|
-
: meaningfulSections,
|
|
597
|
-
minMeaningfulSections: promptPolicy.adaptive_refresh
|
|
598
|
-
? promptPolicy.min_meaningful_sections
|
|
599
|
-
: undefined,
|
|
600
|
-
staleProgressRounds: request.round - (lastMeaningfulRound ? entryRound(lastMeaningfulRound) : 1),
|
|
601
|
-
staleRoundThreshold: promptPolicy.adaptive_refresh
|
|
602
|
-
? promptPolicy.stale_round_threshold
|
|
603
|
-
: undefined,
|
|
604
1128
|
});
|
|
605
1129
|
}
|
|
606
1130
|
export function decideLevel(request, context) {
|
|
607
1131
|
return levelDecision(request, context).level;
|
|
608
1132
|
}
|
|
609
|
-
export function
|
|
1133
|
+
export function buildSelfEvalBlock(round, prevDriftFlags,
|
|
1134
|
+
/** v2.12: L0 is the minimal retry template — the v2.12 declarative fields
|
|
1135
|
+
* (outcome/blocker/retroactiveClaims) are L1/L2 additions so the retry
|
|
1136
|
+
* prompt stays within its tight budget. */
|
|
1137
|
+
level,
|
|
1138
|
+
/** v3.3/v3.4: Whether this round's Current Task IS the ACTIVE Round
|
|
1139
|
+
* Contract (derived from committed rounds — compileLoop passes
|
|
1140
|
+
* `activeContract != null`). Only then does the template ask the agent
|
|
1141
|
+
* to restate/propose it — a generic empty contract template would invite
|
|
1142
|
+
* placeholder submissions that trigger round_underspecified noise. */
|
|
1143
|
+
hasContract = false,
|
|
1144
|
+
/** v3.5: L2-only prose suggesting a Round Contract declaration when the
|
|
1145
|
+
* Current Task is NOT one (contract_nudge_on_l2 policy, computed at the
|
|
1146
|
+
* compileLoop call site). Mutually exclusive with hasContract. The prose
|
|
1147
|
+
* must never contain the JSON key name `round_contract` — contract-less
|
|
1148
|
+
* L2 tests assert its lowercase absence. */
|
|
1149
|
+
proposalNudge = false) {
|
|
610
1150
|
const hasIntentDrift = prevDriftFlags?.some(f => f.check === "intent_drift") ?? false;
|
|
611
1151
|
const hasSubGoalDrift = prevDriftFlags?.some(f => f.check === "subgoal_drift") ?? false;
|
|
612
1152
|
const needsClarification = hasIntentDrift || hasSubGoalDrift;
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
1153
|
+
const declareOutcome = level !== "l0";
|
|
1154
|
+
const restateContract = hasContract && declareOutcome;
|
|
1155
|
+
const evalObj = {
|
|
1156
|
+
success: false,
|
|
1157
|
+
output_summary: `<verified result of round ${round}>`,
|
|
1158
|
+
constraint_violations: ["<c-XXXXXXXX or text>"],
|
|
1159
|
+
should_continue: true,
|
|
1160
|
+
discovered_constraints: ["<new constraint>"],
|
|
1161
|
+
emerged_subtasks: [],
|
|
1162
|
+
execution_evidence: {
|
|
1163
|
+
files_changed: [],
|
|
1164
|
+
test_results: { passed: 0, failed: 0, skipped: 0 },
|
|
1165
|
+
success_criteria_met: ["<cr-XXXXXXXX or text>"],
|
|
1166
|
+
success_criteria_remaining: ["<cr-XXXXXXXX or text>"],
|
|
1167
|
+
progress_estimate: 0,
|
|
628
1168
|
},
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
},
|
|
635
|
-
subGoalEvents: { completed: [], blocked: [], canceled: [] },
|
|
1169
|
+
wrong_assumptions: [],
|
|
1170
|
+
next_action: "<next concrete action, or empty when complete>",
|
|
1171
|
+
completed_subtasks: ["<sg-XXXXXXXX or text>"],
|
|
1172
|
+
blocked_subtasks: ["<sg-XXXXXXXX or text>"],
|
|
1173
|
+
canceled_subtasks: ["<sg-XXXXXXXX or text>"],
|
|
636
1174
|
};
|
|
1175
|
+
// v2.12: Declarative tri-state outcome (L1/L2 only — keeps L0 retry lean)
|
|
1176
|
+
if (declareOutcome) {
|
|
1177
|
+
evalObj.outcome = "<success|partial|failed|blocked>";
|
|
1178
|
+
evalObj.blocker = "<required only when outcome=blocked>";
|
|
1179
|
+
evalObj.retroactiveClaims = [];
|
|
1180
|
+
}
|
|
1181
|
+
// v3.3/v3.4: Round Contract proposal template (L1/L2, active-contract
|
|
1182
|
+
// rounds only — L0 retry stays lean, and rounds without an active
|
|
1183
|
+
// contract never see the template). When the prompt's Current Task IS the
|
|
1184
|
+
// ACTIVE contract, the agent restates it unchanged as its proposal; when
|
|
1185
|
+
// the active contract's done_when are all satisfied (or the round is
|
|
1186
|
+
// blocked), a NEW contract is declared for the next round instead.
|
|
1187
|
+
if (restateContract) {
|
|
1188
|
+
evalObj.round_contract = {
|
|
1189
|
+
work_item: "<this round's focus, or empty>",
|
|
1190
|
+
done_when: [],
|
|
1191
|
+
verification_plan: [],
|
|
1192
|
+
scope: [],
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
637
1195
|
// v2.8: Inject drift clarification field when previous round had drift flags
|
|
638
1196
|
if (needsClarification) {
|
|
639
1197
|
const flagTypes = [
|
|
640
1198
|
hasIntentDrift ? "intent_drift" : "",
|
|
641
1199
|
hasSubGoalDrift ? "subgoal_drift" : "",
|
|
642
1200
|
].filter(Boolean).join(" / ");
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
report.driftClarification = {
|
|
648
|
-
explanation: "<explain why the current evidence requires this pivot>",
|
|
649
|
-
anchors: ["<criterion ID, constraint ID, or observed file path>"],
|
|
650
|
-
};
|
|
1201
|
+
evalObj.drift_clarification =
|
|
1202
|
+
`<explain why your Round ${round} actions diverged from your stated plan ` +
|
|
1203
|
+
`(${flagTypes} detected in previous round). ` +
|
|
1204
|
+
`Leave empty if no divergence occurred>`;
|
|
651
1205
|
}
|
|
652
1206
|
const lines = [
|
|
653
1207
|
"### LoopForge Evaluation (Required)",
|
|
654
1208
|
"",
|
|
655
1209
|
`After completing Round ${round}, replace the placeholder values below`,
|
|
656
|
-
"with your actual results and
|
|
1210
|
+
"with your actual results and pass this object as `loopforge_next.evaluation`.",
|
|
657
1211
|
];
|
|
658
1212
|
if (needsClarification) {
|
|
659
|
-
lines.push("", "> ⚠️ **Drift Detected:** The verification gate detected that your previous", "> round's
|
|
1213
|
+
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.");
|
|
660
1214
|
}
|
|
661
|
-
lines.push("
|
|
662
|
-
lines.push(JSON.stringify(
|
|
1215
|
+
lines.push("```json");
|
|
1216
|
+
lines.push(JSON.stringify(evalObj, null, 2));
|
|
1217
|
+
lines.push("```");
|
|
663
1218
|
lines.push("");
|
|
664
1219
|
lines.push("IMPORTANT: Replace every <placeholder> with your actual data.");
|
|
665
|
-
lines.push("Set
|
|
1220
|
+
lines.push("Set success=true only when the full goal and ALL hard constraints are verified.");
|
|
666
1221
|
lines.push("Use IDs for exact matching: c-XXXXXXXX, cr-XXXXXXXX, sg-XXXXXXXX.");
|
|
667
1222
|
lines.push("For subtasks, use sub-goal IDs from the Sub-Goal Dashboard above.");
|
|
1223
|
+
// v3.5: L2 contract-less nudge (prose only — never the JSON key name, so
|
|
1224
|
+
// contract-less rounds keep their template-lean assertions).
|
|
1225
|
+
if (proposalNudge) {
|
|
1226
|
+
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.");
|
|
1227
|
+
}
|
|
1228
|
+
if (restateContract) {
|
|
1229
|
+
lines.push("Round Contract: restate the Current Task's contract UNCHANGED in", "`round_contract` (work_item/done_when/verification_plan/scope) and run", "its Verify commands; list satisfied done_when items in", "`success_criteria_met`. When the current contract's done_when items are", "all satisfied, 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).");
|
|
1230
|
+
}
|
|
668
1231
|
return lines.join("\n");
|
|
669
1232
|
}
|
|
670
1233
|
// v2.5: checkpoint() removed — agent_declared milestones from
|
|
@@ -674,10 +1237,14 @@ export function compileLoop(request, context) {
|
|
|
674
1237
|
const decision = levelDecision(request, context);
|
|
675
1238
|
const previous = getPreviousRound(request.loop_id, request.round - 1, context);
|
|
676
1239
|
const objective = evolveObjective(request, context);
|
|
677
|
-
const constraints = evolveConstraints(request, objective, previous);
|
|
678
|
-
const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round);
|
|
679
|
-
const rolling = buildRollingSummary(request.loop_id, request.round, context);
|
|
680
|
-
const subGoals =
|
|
1240
|
+
const constraints = evolveConstraints(request, objective, previous, context);
|
|
1241
|
+
const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round, request.last_round_result?.constraint_violations ?? []);
|
|
1242
|
+
const rolling = buildRollingSummary(request.loop_id, request.round, context, 0, decision.level);
|
|
1243
|
+
const subGoals = manageSubGoals(request.loop_id, request.round, request.last_round_result, context);
|
|
1244
|
+
// v3.2: Goal → criteria → evidence vertical view (derived, zero persistence).
|
|
1245
|
+
const criterionStatuses = deriveCriterionStatuses(request.loop_id, context, objective, request.round, subGoals, request.last_round_result);
|
|
1246
|
+
// v3.2: Lessons learned — repeated violations / verification failures.
|
|
1247
|
+
const lessons = deriveLessons(request.loop_id, context, request.round);
|
|
681
1248
|
const alignment = alignTask(request.task, request, context);
|
|
682
1249
|
const health = checkLoopHealth(request.loop_id, { ...request, loop_objective: objective }, context);
|
|
683
1250
|
const warnings = unique([
|
|
@@ -695,37 +1262,22 @@ export function compileLoop(request, context) {
|
|
|
695
1262
|
// No new persistence; reconstructed same as milestones/sub-goals.
|
|
696
1263
|
const completedEntries = loopEntries(request.loop_id, context)
|
|
697
1264
|
.filter((e) => entryRound(e) >= 1 && entryRound(e) < request.round);
|
|
1265
|
+
const committedRounds = mergedRoundsFromEntries(completedEntries, request.round);
|
|
698
1266
|
const trend = [];
|
|
699
|
-
for (const
|
|
700
|
-
const viols =
|
|
1267
|
+
for (const round of committedRounds.slice(-10)) {
|
|
1268
|
+
const viols = round.evaluation?.constraint_violations ?? [];
|
|
701
1269
|
// Simple proxy: no violations = perfect trust, each violation = -0.1
|
|
702
1270
|
const score = Math.max(0, 1.0 - viols.length * 0.1);
|
|
703
1271
|
trend.push(Number(score.toFixed(2)));
|
|
704
1272
|
}
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
sourceRound: obligation.sourceRound,
|
|
714
|
-
evidenceRefs: [`check:${obligation.check}`],
|
|
715
|
-
status: obligation.status,
|
|
716
|
-
}));
|
|
717
|
-
const latestBlocker = latestReport?.outcome === "blocked" && latestReport.blocker;
|
|
718
|
-
const blockerGate = latestBlocker
|
|
719
|
-
? latestBlocker.action
|
|
720
|
-
? deriveStructuredGate(latestBlocker.action)
|
|
721
|
-
: deriveGate(latestBlocker.reason, latestBlocker.references)
|
|
722
|
-
: null;
|
|
723
|
-
const activeBlockers = latestBlocker && blockerGate?.gate.kind === "user" &&
|
|
724
|
-
!audit.unresolvedUserGates.includes(blockerGate.id)
|
|
725
|
-
? []
|
|
726
|
-
: latestBlocker
|
|
727
|
-
? [latestBlocker.reason]
|
|
728
|
-
: [];
|
|
1273
|
+
// v3.3: Round stats + machine git-motion status (display-only, derived).
|
|
1274
|
+
const roundStats = deriveRoundStats(committedRounds);
|
|
1275
|
+
const machineStatus = deriveMachineStatus(committedRounds, request.round);
|
|
1276
|
+
// v3.4: ACTIVE Round Contract for this compile — derived from committed
|
|
1277
|
+
// rounds, so rejection retries / resume / unpause / backtrack compiles
|
|
1278
|
+
// (no last_round_result) keep showing the contract as Current Task, and a
|
|
1279
|
+
// satisfied or blocked contract stops being shown.
|
|
1280
|
+
const activeContract = deriveActiveContract(request.loop_id, context, request.round);
|
|
729
1281
|
const response = makeLoopCompileResponse({
|
|
730
1282
|
status: AgentStatus.OK,
|
|
731
1283
|
recompile_level: decision.level,
|
|
@@ -744,26 +1296,23 @@ export function compileLoop(request, context) {
|
|
|
744
1296
|
task_alignment: alignment,
|
|
745
1297
|
rolling_summary: rolling,
|
|
746
1298
|
sub_goals: subGoals,
|
|
1299
|
+
criterion_statuses: criterionStatuses,
|
|
1300
|
+
lessons,
|
|
747
1301
|
agent_trust_score: request.round > 1 ? Number(trustScore.toFixed(2)) : undefined,
|
|
748
1302
|
agent_trust_trend: request.round > 1 ? trend : [],
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
// into the canonical next-action projection.
|
|
752
|
-
projection: makeLoopCompileProjection({
|
|
753
|
-
suggestedNextAction: request.last_round_view?.evidence?.criteriaRemaining?.[0] ||
|
|
754
|
-
request.last_round_view?.discoveries?.emergedWork?.[0] || "",
|
|
755
|
-
verifiedFacts: verified.facts,
|
|
756
|
-
verifiedEvidenceRefs: verified.refs,
|
|
757
|
-
regressionObligations,
|
|
758
|
-
discoveredFacts: latestReport?.discoveries?.facts ?? [],
|
|
759
|
-
activeBlockers,
|
|
760
|
-
}),
|
|
1303
|
+
suggested_next_task: request.last_round_result?.next_action?.trim() ||
|
|
1304
|
+
request.last_round_result?.emerged_subtasks?.[0] || "",
|
|
761
1305
|
plan_source: request.plan_source,
|
|
762
1306
|
warnings,
|
|
763
1307
|
});
|
|
764
1308
|
const policy = getPolicy();
|
|
765
1309
|
const statePath = `${policy.state_file.directory}/${request.loop_id}-state.md`;
|
|
766
|
-
const state = createCanonicalLoopState(request, response, statePath
|
|
1310
|
+
const state = createCanonicalLoopState(request, response, statePath, {
|
|
1311
|
+
roundStats,
|
|
1312
|
+
machineStatus,
|
|
1313
|
+
// v3.4: derived active contract — the Current Task's source of truth.
|
|
1314
|
+
roundContract: activeContract,
|
|
1315
|
+
});
|
|
767
1316
|
const markdown = renderCanonicalStateMarkdown(state);
|
|
768
1317
|
// v2.4–v2.5: Adaptive L2 budget — scales with loop complexity
|
|
769
1318
|
const adaptiveL2 = policy.prompt.l2_adaptive_enabled
|
|
@@ -787,36 +1336,24 @@ export function compileLoop(request, context) {
|
|
|
787
1336
|
l2: adaptiveL2,
|
|
788
1337
|
},
|
|
789
1338
|
attempt: request.attempt,
|
|
790
|
-
|
|
1339
|
+
selfEvaluationBlock: buildSelfEvalBlock(request.round, request.verification_flags, decision.level,
|
|
1340
|
+
// v3.4: restate the contract only when this prompt's Current Task IS
|
|
1341
|
+
// the ACTIVE contract (derived — not the previous submission's field,
|
|
1342
|
+
// which is a proposal and may differ from what this round executes).
|
|
1343
|
+
activeContract != null,
|
|
1344
|
+
// v3.5: L2-only declaration nudge when nothing is active (policy-gated;
|
|
1345
|
+
// mutually exclusive with the restate template above).
|
|
1346
|
+
decision.level === "l2" && activeContract === null &&
|
|
1347
|
+
getPolicy().prompt.contract_nudge_on_l2),
|
|
791
1348
|
fullStateMarkdown: l2Pointer ? undefined : markdown,
|
|
792
|
-
|
|
793
|
-
promptRequests: request.
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
.filter((entry) => entryRound(entry) < request.round);
|
|
797
|
-
const previousL2 = [...priorEntries].reverse()
|
|
798
|
-
.find((entry) => lineage(entry).recompile_level === "l2");
|
|
799
|
-
artifact.lastL2Round = artifact.level === "l2"
|
|
800
|
-
? request.round
|
|
801
|
-
: previousL2 ? entryRound(previousL2) : 0;
|
|
802
|
-
const meaningfulEntry = [...priorEntries].reverse().find((entry) => {
|
|
803
|
-
const data = lineage(entry);
|
|
804
|
-
const evidence = entryProjection(entry)?.evidence;
|
|
805
|
-
return Boolean(entryCriteriaMet(entry).length ||
|
|
806
|
-
evidence?.observedFiles.length ||
|
|
807
|
-
entryProjection(entry)?.discoveries?.newConstraints.length ||
|
|
808
|
-
entryProjection(entry)?.discoveries?.emergedWork.length);
|
|
1349
|
+
// v2.9: Model's information needs from the previous round's SelfEvaluation
|
|
1350
|
+
promptRequests: request.last_round_result?.prompt_requests,
|
|
1351
|
+
// v3.2: Previous L1 presentation as the collapse diff baseline.
|
|
1352
|
+
presentedBaseline: readPresentedBaseline(request.loop_id, request.round, context),
|
|
809
1353
|
});
|
|
810
|
-
artifact.lastMeaningfulProgressRound = meaningfulEntry
|
|
811
|
-
? entryRound(meaningfulEntry)
|
|
812
|
-
: request.round <= 1 ? 0 : 1;
|
|
813
|
-
artifact.stateAgeRounds = artifact.lastMeaningfulProgressRound > 0
|
|
814
|
-
? Math.max(0, request.round - artifact.lastMeaningfulProgressRound)
|
|
815
|
-
: request.round - 1;
|
|
816
1354
|
response.prompt = artifact.renderedPrompt;
|
|
817
1355
|
response.prompt_artifact = artifact;
|
|
818
1356
|
response.state_file_content = policy.state_file.enabled ? markdown : undefined;
|
|
819
|
-
response.canonical_state = state;
|
|
820
1357
|
return response;
|
|
821
1358
|
}
|
|
822
1359
|
//# sourceMappingURL=loop-compiler.js.map
|