loopforge 3.0.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -145
- package/dist/audit.d.ts +50 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +158 -0
- package/dist/audit.js.map +1 -0
- package/dist/canonical-state.d.ts +28 -20
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +82 -173
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +25 -16
- package/dist/cli.js.map +1 -1
- 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/enforcement-gate.d.ts +76 -0
- package/dist/enforcement-gate.d.ts.map +1 -0
- package/dist/enforcement-gate.js +1071 -0
- package/dist/enforcement-gate.js.map +1 -0
- package/dist/engine.d.ts +99 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +839 -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 +134 -0
- package/dist/evidence-claims.js.map +1 -0
- package/dist/evidence-provider.d.ts +21 -23
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +74 -34
- 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 +41 -3
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +632 -273
- 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 +26 -0
- package/dist/loop-projection.d.ts.map +1 -0
- package/dist/loop-projection.js +142 -0
- package/dist/loop-projection.js.map +1 -0
- package/dist/loop-store.d.ts +112 -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 +216 -0
- package/dist/mcp/round-lifecycle.d.ts.map +1 -0
- package/dist/mcp/round-lifecycle.js +926 -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 +28 -42
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +53 -101
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +306 -1105
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +13 -16
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +479 -413
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +12 -17
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +95 -161
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +44 -35
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +31 -115
- 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 +389 -160
- 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 +357 -229
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +139 -15
- 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 +19 -55
- package/dist/replay.js.map +1 -1
- package/dist/round-coordinator.d.ts +20 -24
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +99 -92
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +14 -13
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +26 -14
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +15 -18
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +38 -47
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +53 -0
- package/dist/self-eval.d.ts.map +1 -0
- package/dist/self-eval.js +284 -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 +108 -0
- package/dist/verification-gate.d.ts.map +1 -0
- package/dist/verification-gate.js +971 -0
- package/dist/verification-gate.js.map +1 -0
- package/loop_policy.json +44 -15
- package/package.json +5 -10
- package/skills/perception/SKILL.md +182 -199
- 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,20 @@
|
|
|
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 { auditEntries, deriveGate, deriveStructuredGate } from "./cognitive-governance.js";
|
|
11
|
-
import { parseRoundReport, deriveCompilerProjection } from "./round-report.js";
|
|
12
10
|
import { assemblePromptArtifact } from "./prompt-assembler.js";
|
|
13
11
|
import { decidePromptLevel, } from "./prompt-policy.js";
|
|
14
|
-
import { jaccardSimilarity, unique, entryRound } from "./token-utils.js";
|
|
15
|
-
import { manageSubGoals as deriveSubGoals } from "./compiler-subgoals.js";
|
|
12
|
+
import { deriveItemId, jaccardSimilarity, unique, entryRound } from "./token-utils.js";
|
|
16
13
|
function contextEntries(context) {
|
|
17
14
|
if (!context || !Array.isArray(context.results))
|
|
18
15
|
return [];
|
|
19
16
|
return context.results.filter((value) => value !== null && typeof value === "object" && !Array.isArray(value));
|
|
20
17
|
}
|
|
21
18
|
function lineage(entry) {
|
|
22
|
-
const value = entry.loop_lineage;
|
|
19
|
+
const value = entry.loop_lineage ?? entry.lineage;
|
|
23
20
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
24
21
|
? value
|
|
25
22
|
: {};
|
|
@@ -29,12 +26,9 @@ function loopEntries(loopId, context) {
|
|
|
29
26
|
.filter((entry) => entry.loop_id === loopId || lineage(entry).loop_id === loopId)
|
|
30
27
|
.sort((a, b) => entryRound(a) - entryRound(b));
|
|
31
28
|
}
|
|
32
|
-
function entryProjection(entry) {
|
|
33
|
-
const report = parseRoundReport(entry.round_report);
|
|
34
|
-
return report ? deriveCompilerProjection(report, entryRound(entry)) : null;
|
|
35
|
-
}
|
|
36
29
|
export function computeGoalTextHash(text) {
|
|
37
|
-
|
|
30
|
+
const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
|
|
31
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
|
|
38
32
|
}
|
|
39
33
|
export function deriveGoalId(loopId, task, explicit = "") {
|
|
40
34
|
return explicit.trim() || `${loopId}:${task.trim().toLowerCase()
|
|
@@ -42,6 +36,34 @@ export function deriveGoalId(loopId, task, explicit = "") {
|
|
|
42
36
|
.replace(/^-|-$/g, "")
|
|
43
37
|
.slice(0, 48) || computeGoalTextHash(task)}`;
|
|
44
38
|
}
|
|
39
|
+
/** v3.2: Read the previous round's persisted L1 presentation snapshot — the
|
|
40
|
+
* diff baseline for L1 collapse. Returns null when the previous round's
|
|
41
|
+
* lineage entry exists but lacks all three presented_* fields (old-format
|
|
42
|
+
* entries, L0/L2 compiles) — callers then render in full, which keeps
|
|
43
|
+
* existing fixtures and pre-v3.2 loops on the full-render path. */
|
|
44
|
+
export function readPresentedBaseline(loopId, round, context) {
|
|
45
|
+
const entry = [...loopEntries(loopId, context)].reverse()
|
|
46
|
+
.find((candidate) => entryRound(candidate) === round - 1);
|
|
47
|
+
if (!entry)
|
|
48
|
+
return null;
|
|
49
|
+
const data = lineage(entry);
|
|
50
|
+
const constraintIds = data.presented_constraint_ids;
|
|
51
|
+
const subGoals = data.presented_subgoals;
|
|
52
|
+
const milestoneRanges = data.presented_milestone_ranges;
|
|
53
|
+
if (!Array.isArray(constraintIds) || !constraintIds.every((v) => typeof v === "string") ||
|
|
54
|
+
!Array.isArray(subGoals) || !subGoals.every((v) => Array.isArray(v) && v.length === 2 &&
|
|
55
|
+
typeof v[0] === "string" && typeof v[1] === "string") ||
|
|
56
|
+
!Array.isArray(milestoneRanges) || !milestoneRanges.every((v) => Array.isArray(v) && v.length === 2 &&
|
|
57
|
+
typeof v[0] === "number" && typeof v[1] === "number")) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
round: round - 1,
|
|
62
|
+
constraintIds: constraintIds,
|
|
63
|
+
subGoals: subGoals,
|
|
64
|
+
milestoneRanges: milestoneRanges,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
45
67
|
export function getPreviousRound(loopId, round, context) {
|
|
46
68
|
const entry = [...loopEntries(loopId, context)].reverse()
|
|
47
69
|
.find((candidate) => entryRound(candidate) === round);
|
|
@@ -61,7 +83,9 @@ export function getPreviousRound(loopId, round, context) {
|
|
|
61
83
|
constraints_active: Array.isArray(data.constraints_active)
|
|
62
84
|
? data.constraints_active.filter((value) => typeof value === "string")
|
|
63
85
|
: [],
|
|
64
|
-
output_summary:
|
|
86
|
+
output_summary: typeof entry.output_summary === "string"
|
|
87
|
+
? entry.output_summary
|
|
88
|
+
: typeof data.output_summary === "string" ? data.output_summary : "",
|
|
65
89
|
};
|
|
66
90
|
}
|
|
67
91
|
function latestObjective(loopId, context) {
|
|
@@ -98,17 +122,17 @@ function evolveObjective(request, context) {
|
|
|
98
122
|
created_at_round: 1,
|
|
99
123
|
loop_id: request.loop_id,
|
|
100
124
|
});
|
|
101
|
-
const last = request.
|
|
125
|
+
const last = request.last_round_result;
|
|
102
126
|
const history = [...(current.refinement_history ?? [])];
|
|
103
127
|
let objective = current.objective || request.task;
|
|
104
128
|
let version = current.version ?? 1;
|
|
105
|
-
if (last?.
|
|
106
|
-
const refinement = last.
|
|
129
|
+
if (last?.objective_refinement?.trim()) {
|
|
130
|
+
const refinement = last.objective_refinement.trim();
|
|
107
131
|
history.push(refinement);
|
|
108
132
|
objective = `${objective}\nRefinement: ${refinement}`;
|
|
109
133
|
version++;
|
|
110
134
|
}
|
|
111
|
-
const revisions = new Map((last?.
|
|
135
|
+
const revisions = new Map((last?.revised_success_criteria ?? []).map((item) => [item.old, item.new]));
|
|
112
136
|
return makeLoopObjective({
|
|
113
137
|
...current,
|
|
114
138
|
objective,
|
|
@@ -119,66 +143,183 @@ function evolveObjective(request, context) {
|
|
|
119
143
|
loop_id: request.loop_id,
|
|
120
144
|
});
|
|
121
145
|
}
|
|
122
|
-
function evolveConstraints(request, objective, previous) {
|
|
123
|
-
const retired = unique(request.
|
|
146
|
+
function evolveConstraints(request, objective, previous, context) {
|
|
147
|
+
const retired = unique(request.last_round_result?.retracted_constraints ?? []);
|
|
124
148
|
const retiredSet = new Set(retired);
|
|
125
|
-
const discovered = unique(request.
|
|
149
|
+
const discovered = unique(request.last_round_result?.discovered_constraints ?? [])
|
|
126
150
|
.slice(0, getPolicy().evolution.max_discovered_constraints_per_round);
|
|
151
|
+
// v2.14: agent-declared retractions of plan/hard/criteria constraints
|
|
152
|
+
// persist for constraints.retire_window rounds (they are re-merged from
|
|
153
|
+
// the request/objective every round, so without this the documented
|
|
154
|
+
// "Removed from the active constraint set" never held for them). The
|
|
155
|
+
// retraction rounds are derived from vault entries — no new persistence
|
|
156
|
+
// format. After the window the constraint returns (hard/plan/criteria
|
|
157
|
+
// constraints never auto-decay — only agent action removes them, and the
|
|
158
|
+
// action's memory expires).
|
|
159
|
+
const window = getPolicy().constraints.retire_window;
|
|
160
|
+
const lastRetractedRound = new Map();
|
|
161
|
+
if (window > 0) {
|
|
162
|
+
for (const entry of loopEntries(request.loop_id, context)) {
|
|
163
|
+
const retractedList = lineage(entry).retracted_constraints;
|
|
164
|
+
if (!Array.isArray(retractedList))
|
|
165
|
+
continue;
|
|
166
|
+
const rnd = entryRound(entry);
|
|
167
|
+
for (const text of retractedList) {
|
|
168
|
+
if (typeof text !== "string" || !text)
|
|
169
|
+
continue;
|
|
170
|
+
const prev = lastRetractedRound.get(text);
|
|
171
|
+
if (prev === undefined || rnd > prev)
|
|
172
|
+
lastRetractedRound.set(text, rnd);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
127
176
|
const active = unique([
|
|
128
177
|
...(previous?.constraints_active ?? []),
|
|
129
178
|
...request.constraints_from_plan,
|
|
130
179
|
...objective.hard_constraints,
|
|
131
180
|
...objective.success_criteria,
|
|
132
181
|
...discovered,
|
|
133
|
-
]).filter((item) =>
|
|
134
|
-
|
|
182
|
+
]).filter((item) => {
|
|
183
|
+
if (retiredSet.has(item))
|
|
184
|
+
return false;
|
|
185
|
+
const retractedRound = lastRetractedRound.get(item);
|
|
186
|
+
if (retractedRound !== undefined && request.round - retractedRound < window) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
}).slice(0, getPolicy().evolution.max_active_constraints);
|
|
135
191
|
return { active, retired };
|
|
136
192
|
}
|
|
137
193
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
138
194
|
// Hierarchical Summary (v2.1)
|
|
139
195
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
196
|
+
/** Extract execution_evidence from a vault entry, handling both direct-field
|
|
197
|
+
* and lineage-nested shapes. Returns null when absent. */
|
|
198
|
+
function entryExecutionEvidence(entry) {
|
|
199
|
+
const direct = entry.execution_evidence;
|
|
200
|
+
if (direct && typeof direct === "object" && !Array.isArray(direct)) {
|
|
201
|
+
return direct;
|
|
202
|
+
}
|
|
203
|
+
const lin = lineage(entry);
|
|
204
|
+
const nested = lin.execution_evidence;
|
|
205
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
206
|
+
return nested;
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
/** Read success_criteria_met from an entry's execution_evidence. */
|
|
140
211
|
function entryCriteriaMet(entry) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
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
|
-
|
|
212
|
+
const ev = entryExecutionEvidence(entry);
|
|
213
|
+
if (!ev)
|
|
214
|
+
return [];
|
|
215
|
+
const arr = ev.success_criteria_met;
|
|
216
|
+
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
217
|
+
}
|
|
218
|
+
/** Read success_criteria_remaining from an entry's execution_evidence. */
|
|
219
|
+
function entryCriteriaRemaining(entry) {
|
|
220
|
+
const ev = entryExecutionEvidence(entry);
|
|
221
|
+
if (!ev)
|
|
222
|
+
return [];
|
|
223
|
+
const arr = ev.success_criteria_remaining;
|
|
224
|
+
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
225
|
+
}
|
|
226
|
+
/** v3.2: Deterministic lessons learned — constraints violated repeatedly or
|
|
227
|
+
* verification checks failing repeatedly across rounds (full history, unlike
|
|
228
|
+
* the enforcement gate's R2 3-round window). Presentation only: the output
|
|
229
|
+
* never feeds enforcement decisions. */
|
|
230
|
+
export function deriveLessons(loopId, context, currentRound) {
|
|
231
|
+
const entries = loopEntries(loopId, context).filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
|
|
232
|
+
const violations = new Map();
|
|
233
|
+
const errors = new Map();
|
|
234
|
+
const warns = new Map();
|
|
235
|
+
for (const entry of entries) {
|
|
236
|
+
const rnd = entryRound(entry);
|
|
237
|
+
const viols = Array.isArray(entry.constraint_violations)
|
|
238
|
+
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
239
|
+
: [];
|
|
240
|
+
for (const text of viols) {
|
|
241
|
+
const list = violations.get(text) ?? [];
|
|
242
|
+
list.push(rnd);
|
|
243
|
+
violations.set(text, list);
|
|
244
|
+
}
|
|
245
|
+
const flags = Array.isArray(entry.verification_flags)
|
|
246
|
+
? entry.verification_flags
|
|
247
|
+
: [];
|
|
248
|
+
for (const flag of flags) {
|
|
249
|
+
const target = flag.severity === "error"
|
|
250
|
+
? errors
|
|
251
|
+
: flag.severity === "warn" ? warns : null;
|
|
252
|
+
if (!target)
|
|
175
253
|
continue;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
254
|
+
const list = target.get(flag.check) ?? [];
|
|
255
|
+
list.push(rnd);
|
|
256
|
+
target.set(flag.check, list);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const lessons = [];
|
|
260
|
+
for (const [text, rounds] of violations) {
|
|
261
|
+
if (rounds.length >= 2) {
|
|
262
|
+
lessons.push({ text, kind: "constraint_violation", count: rounds.length, rounds });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const [text, rounds] of errors) {
|
|
266
|
+
if (rounds.length >= 2) {
|
|
267
|
+
lessons.push({ text, kind: "verification_error", count: rounds.length, rounds });
|
|
179
268
|
}
|
|
180
269
|
}
|
|
181
|
-
|
|
270
|
+
for (const [text, rounds] of warns) {
|
|
271
|
+
if (rounds.length >= 2) {
|
|
272
|
+
lessons.push({ text, kind: "verification_warning", count: rounds.length, rounds });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return lessons.sort((a, b) => b.count - a.count);
|
|
276
|
+
}
|
|
277
|
+
/** v3.2: Derive per-criterion status — the "goal → criteria → evidence"
|
|
278
|
+
* vertical view. Each objective criterion gets: met/remaining/unknown
|
|
279
|
+
* (from per-round success_criteria_met/remaining reports, ID-first
|
|
280
|
+
* matching), the round it was first reported met, and any sub-goals whose
|
|
281
|
+
* description matches it (Jaccard). Zero persistence — re-derived from the
|
|
282
|
+
* vault every compile. */
|
|
283
|
+
export function deriveCriterionStatuses(loopId, context, objective, currentRound, subGoals, lastRoundResult) {
|
|
284
|
+
if (!objective || objective.success_criteria.length === 0)
|
|
285
|
+
return [];
|
|
286
|
+
const entries = loopEntries(loopId, context).filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
|
|
287
|
+
// The current round's report lives in last_round_result (the vault only
|
|
288
|
+
// gains it on commit); treat it as the report for round currentRound - 1.
|
|
289
|
+
const lastEv = lastRoundResult?.execution_evidence;
|
|
290
|
+
const lastMet = lastEv?.success_criteria_met ?? [];
|
|
291
|
+
const lastRemaining = lastEv?.success_criteria_remaining ?? [];
|
|
292
|
+
const lastEntry = entries[entries.length - 1] ?? null;
|
|
293
|
+
const lastEntryRemaining = lastEntry ? entryCriteriaRemaining(lastEntry) : [];
|
|
294
|
+
const matchThreshold = getPolicy().evolution.subgoal_match_threshold;
|
|
295
|
+
return objective.success_criteria.map((text) => {
|
|
296
|
+
let metAtRound = null;
|
|
297
|
+
for (const entry of entries) {
|
|
298
|
+
const rnd = entryRound(entry);
|
|
299
|
+
if (entryCriteriaMet(entry).some((met) => criteriaMatch(met, text))) {
|
|
300
|
+
metAtRound = rnd;
|
|
301
|
+
break; // first report wins — chronological scan
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (metAtRound === null && lastMet.some((met) => criteriaMatch(met, text))) {
|
|
305
|
+
metAtRound = currentRound - 1;
|
|
306
|
+
}
|
|
307
|
+
const status = metAtRound !== null
|
|
308
|
+
? "met"
|
|
309
|
+
: lastRemaining.some((remaining) => criteriaMatch(remaining, text)) ||
|
|
310
|
+
lastEntryRemaining.some((remaining) => criteriaMatch(remaining, text))
|
|
311
|
+
? "remaining"
|
|
312
|
+
: "unknown";
|
|
313
|
+
return {
|
|
314
|
+
id: deriveCriterionId(text),
|
|
315
|
+
text,
|
|
316
|
+
status,
|
|
317
|
+
...(metAtRound !== null ? { met_at_round: metAtRound } : {}),
|
|
318
|
+
related_subgoal_ids: subGoals
|
|
319
|
+
.filter((sg) => jaccardSimilarity(text, sg.description) >= matchThreshold)
|
|
320
|
+
.map((sg) => sg.id),
|
|
321
|
+
};
|
|
322
|
+
});
|
|
182
323
|
}
|
|
183
324
|
/** v2.11: Match two criterion references for deduplication.
|
|
184
325
|
* If either is a criterion ID (cr-XXXXXXXX), uses exact ID comparison.
|
|
@@ -197,9 +338,6 @@ function criteriaMatch(a, b) {
|
|
|
197
338
|
// Neither is an ID — Jaccard fallback
|
|
198
339
|
return jaccardSimilarity(a, b) >= getPolicy().evolution.criteria_dedup_threshold;
|
|
199
340
|
}
|
|
200
|
-
function isCriterionId(ref) {
|
|
201
|
-
return /^cr-[a-f0-9]{8}$/.test(ref);
|
|
202
|
-
}
|
|
203
341
|
/** Detect newly met criteria by comparing the current entry's
|
|
204
342
|
* success_criteria_met against the previous entry's.
|
|
205
343
|
* v2.11: ID-first matching (cr-XXXXXXXX) with Jaccard similarity
|
|
@@ -227,15 +365,29 @@ function entryActiveConstraints(entry) {
|
|
|
227
365
|
}
|
|
228
366
|
/** Read retracted_constraints from an entry. */
|
|
229
367
|
function entryRetractedConstraints(entry) {
|
|
230
|
-
|
|
368
|
+
const direct = entry.retracted_constraints;
|
|
369
|
+
if (Array.isArray(direct))
|
|
370
|
+
return direct.filter((v) => typeof v === "string");
|
|
371
|
+
const lin = lineage(entry);
|
|
372
|
+
const nested = lin.retracted_constraints;
|
|
373
|
+
if (Array.isArray(nested))
|
|
374
|
+
return nested.filter((v) => typeof v === "string");
|
|
375
|
+
return [];
|
|
231
376
|
}
|
|
377
|
+
/** Read progress_estimate from an entry's execution_evidence. */
|
|
232
378
|
function entryProgressEstimate(entry) {
|
|
233
|
-
|
|
379
|
+
const ev = entryExecutionEvidence(entry);
|
|
380
|
+
if (!ev)
|
|
381
|
+
return 0;
|
|
382
|
+
const pe = ev.progress_estimate;
|
|
383
|
+
return typeof pe === "number" ? pe : 0;
|
|
234
384
|
}
|
|
235
385
|
/** Build a single MilestoneSummary from a range of completed round entries. */
|
|
236
386
|
function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, kind) {
|
|
237
387
|
const last = phaseEntries.at(-1);
|
|
238
|
-
const outcomeRaw = last
|
|
388
|
+
const outcomeRaw = last
|
|
389
|
+
? (typeof last.output_summary === "string" ? last.output_summary : "")
|
|
390
|
+
: "";
|
|
239
391
|
const outcomeMax = getPolicy().checkpoint.outcome_max_chars;
|
|
240
392
|
const outcome = outcomeRaw.length > outcomeMax
|
|
241
393
|
? outcomeRaw.slice(0, outcomeMax) + "…"
|
|
@@ -254,7 +406,56 @@ function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, ki
|
|
|
254
406
|
generated_at_round: endRound,
|
|
255
407
|
});
|
|
256
408
|
}
|
|
257
|
-
|
|
409
|
+
/** v3.0.1: Evenly sample `budget` entries from `mid`, preferring
|
|
410
|
+
* agent_declared milestones (human/agent-labeled anchors) first and filling
|
|
411
|
+
* the remainder at even time positions. The caller guarantees
|
|
412
|
+
* `mid.length > budget`. */
|
|
413
|
+
function sampleMiddleMilestones(mid, budget) {
|
|
414
|
+
if (budget <= 0)
|
|
415
|
+
return [];
|
|
416
|
+
if (mid.length <= budget)
|
|
417
|
+
return mid;
|
|
418
|
+
const chosen = [];
|
|
419
|
+
const used = new Set();
|
|
420
|
+
for (const m of mid) {
|
|
421
|
+
if (m.kind === "agent_declared") {
|
|
422
|
+
chosen.push(m);
|
|
423
|
+
used.add(m);
|
|
424
|
+
if (chosen.length >= budget)
|
|
425
|
+
return chosen;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const rest = mid.filter((m) => !used.has(m));
|
|
429
|
+
const need = budget - chosen.length;
|
|
430
|
+
for (let i = 0; i < need; i++) {
|
|
431
|
+
const idx = Math.floor(((i + 0.5) * rest.length) / need);
|
|
432
|
+
chosen.push(rest[idx]);
|
|
433
|
+
}
|
|
434
|
+
return chosen;
|
|
435
|
+
}
|
|
436
|
+
/** v3.0.1: L1 milestone sampling — when milestones exceed max_milestones,
|
|
437
|
+
* keep the oldest `milestone_head_count` (history anchors), the newest
|
|
438
|
+
* `milestone_tail_count` (current progress), and an even sample of the
|
|
439
|
+
* middle. L2 (full rehydration) is never sampled: the recovery view keeps
|
|
440
|
+
* every milestone as the loop's memory skeleton. */
|
|
441
|
+
function sampleMilestones(milestones, policy, level) {
|
|
442
|
+
if (level === "l2")
|
|
443
|
+
return milestones;
|
|
444
|
+
if (milestones.length <= policy.max_milestones)
|
|
445
|
+
return milestones;
|
|
446
|
+
const head = Math.max(0, policy.milestone_head_count);
|
|
447
|
+
const tail = Math.max(0, policy.milestone_tail_count);
|
|
448
|
+
const midBudget = Math.max(0, policy.max_milestones - head - tail);
|
|
449
|
+
if (midBudget === 0)
|
|
450
|
+
return milestones.slice(0, head).concat(milestones.slice(milestones.length - tail));
|
|
451
|
+
const mid = milestones.slice(head, milestones.length - tail);
|
|
452
|
+
return [
|
|
453
|
+
...milestones.slice(0, head),
|
|
454
|
+
...sampleMiddleMilestones(mid, midBudget),
|
|
455
|
+
...milestones.slice(milestones.length - tail),
|
|
456
|
+
];
|
|
457
|
+
}
|
|
458
|
+
export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0, level) {
|
|
258
459
|
const policy = getPolicy().summary;
|
|
259
460
|
const allEntries = loopEntries(loopId, context);
|
|
260
461
|
// ── Phase 1: Recent Window (preserves existing behavior) ──
|
|
@@ -270,48 +471,35 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
270
471
|
const data = lineage(entry);
|
|
271
472
|
const round = entryRound(entry);
|
|
272
473
|
const success = entry.success ?? data.success;
|
|
273
|
-
const summary =
|
|
474
|
+
const summary = typeof entry.output_summary === "string"
|
|
475
|
+
? entry.output_summary
|
|
476
|
+
: typeof data.output_summary === "string" ? data.output_summary : "";
|
|
477
|
+
// v2.12: Declared outcome wins the label; the default wording stays
|
|
478
|
+
// "accepted" for zero regression on existing loop output.
|
|
479
|
+
const declared = entry.outcome;
|
|
480
|
+
const outcomeLabel = declared === "success" || declared === "partial" ||
|
|
481
|
+
declared === "failed" || declared === "blocked"
|
|
482
|
+
? declared
|
|
483
|
+
: success === false ? "failed" : "accepted";
|
|
274
484
|
if (summary)
|
|
275
|
-
outcomes.push(`[R${round}] ${
|
|
276
|
-
const violations =
|
|
485
|
+
outcomes.push(`[R${round}] ${outcomeLabel}: ${summary}`);
|
|
486
|
+
const violations = Array.isArray(entry.constraint_violations)
|
|
487
|
+
? entry.constraint_violations
|
|
488
|
+
: Array.isArray(data.constraint_violations) ? data.constraint_violations : [];
|
|
277
489
|
issues.push(...violations.filter((item) => typeof item === "string"));
|
|
278
490
|
}
|
|
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
491
|
// ── Phase 2: Milestone Accumulation ──
|
|
288
492
|
// Scan all completed entries in round order. For each entry, check three
|
|
289
493
|
// trigger signals in priority order: agent_declared > criteria_milestone > auto.
|
|
290
494
|
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
495
|
const milestones = [];
|
|
306
496
|
let lastMilestoneRound = 0;
|
|
307
497
|
for (let i = 0; i < completedEntries.length; i++) {
|
|
308
498
|
const entry = completedEntries[i];
|
|
309
499
|
const rnd = entryRound(entry);
|
|
310
500
|
const lin = lineage(entry);
|
|
311
|
-
const report = parseRoundReport(entry.round_report);
|
|
312
501
|
// Signal 1: Agent-declared checkpoint
|
|
313
|
-
const isAgentCheckpoint =
|
|
314
|
-
lin.compression_checkpoint === true;
|
|
502
|
+
const isAgentCheckpoint = lin.compression_checkpoint === true;
|
|
315
503
|
// Signal 2: New criteria met (compare with previous vault entry)
|
|
316
504
|
const prev = i > 0 ? completedEntries[i - 1] : null;
|
|
317
505
|
const newCriteria = detectNewCriteria(entry, prev);
|
|
@@ -325,10 +513,9 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
325
513
|
? "agent_declared"
|
|
326
514
|
: "criteria_milestone";
|
|
327
515
|
const label = isAgentCheckpoint
|
|
328
|
-
? (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
: `Round ${rnd}`))
|
|
516
|
+
? (typeof lin.checkpoint_label === "string" && lin.checkpoint_label.trim()
|
|
517
|
+
? lin.checkpoint_label.trim()
|
|
518
|
+
: `Round ${rnd}`)
|
|
332
519
|
: `Completed: ${newCriteria.join(", ")}`;
|
|
333
520
|
milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, rnd, label, kind));
|
|
334
521
|
lastMilestoneRound = rnd;
|
|
@@ -349,7 +536,10 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
349
536
|
milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, endRound, `Rounds ${startRound}–${endRound}`, "auto"));
|
|
350
537
|
}
|
|
351
538
|
}
|
|
352
|
-
|
|
539
|
+
// v3.0.1: L1 keeps head anchors + newest tail + an even middle sample;
|
|
540
|
+
// L2 (full rehydration) keeps every milestone — the recovery view is the
|
|
541
|
+
// loop's memory skeleton and must not be truncated.
|
|
542
|
+
const cappedMilestones = sampleMilestones(milestones, policy, level);
|
|
353
543
|
// ── Phase 3: Loop Synthesis (formulaic) ──
|
|
354
544
|
let loopSynthesis = "";
|
|
355
545
|
if (policy.enable_loop_synthesis && currentRound > 1) {
|
|
@@ -371,20 +561,237 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
371
561
|
return null;
|
|
372
562
|
return makeRollingSummary({
|
|
373
563
|
key_outcomes: unique(outcomes),
|
|
374
|
-
recurring_issues:
|
|
375
|
-
.filter((issue) => issue.count >= 2)
|
|
376
|
-
.map((issue) => `${issue.text} (${issue.count} rounds)`),
|
|
564
|
+
recurring_issues: unique(issues),
|
|
377
565
|
rounds_sampled: windowEntries.length,
|
|
378
566
|
generated_at_round: currentRound,
|
|
379
|
-
failed_patterns:
|
|
380
|
-
.filter((failure) => failure.count >= 2)
|
|
381
|
-
.map((failure) => `${failure.text} (${failure.count} attempts)`),
|
|
567
|
+
failed_patterns: [],
|
|
382
568
|
milestones: cappedMilestones,
|
|
383
569
|
loop_synthesis: loopSynthesis || undefined,
|
|
384
570
|
});
|
|
385
571
|
}
|
|
386
|
-
//
|
|
387
|
-
//
|
|
572
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
573
|
+
// Sub-Goal Structured Tracking (v2.2)
|
|
574
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
575
|
+
/** Derive a stable sub-goal ID from its description hash.
|
|
576
|
+
* Exported as the single source of truth (verification-gate imports it). */
|
|
577
|
+
export function deriveSubGoalId(description) {
|
|
578
|
+
return "sg-" + deriveItemId(description);
|
|
579
|
+
}
|
|
580
|
+
/** v2.11: Derive a stable constraint ID from its text hash (c-XXXXXXXX).
|
|
581
|
+
* Same hash strategy as SubGoal — deterministic across rounds. */
|
|
582
|
+
export function deriveConstraintId(text) {
|
|
583
|
+
return "c-" + deriveItemId(text);
|
|
584
|
+
}
|
|
585
|
+
/** v2.11: Derive a stable criterion ID from its text hash (cr-XXXXXXXX).
|
|
586
|
+
* Same hash strategy as SubGoal — deterministic across rounds. */
|
|
587
|
+
export function deriveCriterionId(text) {
|
|
588
|
+
return "cr-" + deriveItemId(text);
|
|
589
|
+
}
|
|
590
|
+
/** v2.11: Check whether a user-provided reference looks like a constraint ID.
|
|
591
|
+
* Matches the pattern c-XXXXXXXX where X is a hex digit. */
|
|
592
|
+
export function isConstraintId(ref) {
|
|
593
|
+
return /^c-[a-f0-9]{8}$/.test(ref);
|
|
594
|
+
}
|
|
595
|
+
/** v2.11: Check whether a user-provided reference looks like a criterion ID.
|
|
596
|
+
* Matches the pattern cr-XXXXXXXX where X is a hex digit. */
|
|
597
|
+
export function isCriterionId(ref) {
|
|
598
|
+
return /^cr-[a-f0-9]{8}$/.test(ref);
|
|
599
|
+
}
|
|
600
|
+
/** Read emerged_subtasks from a vault entry (handles direct + lineage nesting). */
|
|
601
|
+
function entryEmergedSubtasks(entry) {
|
|
602
|
+
const direct = entry.emerged_subtasks;
|
|
603
|
+
if (Array.isArray(direct))
|
|
604
|
+
return direct.filter((v) => typeof v === "string");
|
|
605
|
+
const lin = lineage(entry);
|
|
606
|
+
const nested = lin.emerged_subtasks;
|
|
607
|
+
if (Array.isArray(nested))
|
|
608
|
+
return nested.filter((v) => typeof v === "string");
|
|
609
|
+
return [];
|
|
610
|
+
}
|
|
611
|
+
/** Read completed_subtasks from an entry. */
|
|
612
|
+
function entryCompletedSubtasks(entry) {
|
|
613
|
+
const direct = entry.completed_subtasks;
|
|
614
|
+
if (Array.isArray(direct))
|
|
615
|
+
return direct.filter((v) => typeof v === "string");
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
/** Read blocked_subtasks from an entry. */
|
|
619
|
+
function entryBlockedSubtasks(entry) {
|
|
620
|
+
const direct = entry.blocked_subtasks;
|
|
621
|
+
if (Array.isArray(direct))
|
|
622
|
+
return direct.filter((v) => typeof v === "string");
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
/** Read canceled_subtasks from an entry. */
|
|
626
|
+
function entryCanceledSubtasks(entry) {
|
|
627
|
+
const direct = entry.canceled_subtasks;
|
|
628
|
+
if (Array.isArray(direct))
|
|
629
|
+
return direct.filter((v) => typeof v === "string");
|
|
630
|
+
return [];
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Match a user-provided sub-task reference against existing SubGoals.
|
|
634
|
+
*
|
|
635
|
+
* Tries exact SubGoal ID match first (e.g. "sg-a3f2b1c0"), then falls
|
|
636
|
+
* back to Jaccard token similarity on descriptions. Returns the index
|
|
637
|
+
* of the best-matching SubGoal, or -1 when no match reaches threshold.
|
|
638
|
+
*/
|
|
639
|
+
function matchSubGoal(target, subGoals, threshold) {
|
|
640
|
+
const trimmed = target.trim();
|
|
641
|
+
// Phase 1: Exact ID match
|
|
642
|
+
for (let i = 0; i < subGoals.length; i++) {
|
|
643
|
+
if (subGoals[i].id === trimmed)
|
|
644
|
+
return i;
|
|
645
|
+
}
|
|
646
|
+
// Phase 2: Jaccard fallback on description text
|
|
647
|
+
let bestIdx = -1;
|
|
648
|
+
let bestScore = 0;
|
|
649
|
+
for (let i = 0; i < subGoals.length; i++) {
|
|
650
|
+
const score = jaccardSimilarity(trimmed, subGoals[i].description);
|
|
651
|
+
if (score > bestScore) {
|
|
652
|
+
bestScore = score;
|
|
653
|
+
bestIdx = i;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return bestScore >= threshold ? bestIdx : -1;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Manage the full sub-goal lifecycle across rounds.
|
|
660
|
+
*
|
|
661
|
+
* Accumulates sub-goals from all prior vault entries, processes the current
|
|
662
|
+
* round's declarations, derives status changes, and returns the complete
|
|
663
|
+
* structured SubGoal list.
|
|
664
|
+
*
|
|
665
|
+
* Status derivation priority (compiler-driven):
|
|
666
|
+
* 1. Agent declared done/canceled/blocked → explicit status change
|
|
667
|
+
* 2. next_action match → auto in_progress
|
|
668
|
+
* 3. success_criteria_met match → auto done
|
|
669
|
+
*/
|
|
670
|
+
function manageSubGoals(loopId, currentRound, lastRoundResult, vaultContext) {
|
|
671
|
+
const policy = getPolicy().evolution;
|
|
672
|
+
const allEntries = loopEntries(loopId, vaultContext);
|
|
673
|
+
const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
|
|
674
|
+
const subGoals = [];
|
|
675
|
+
// Phase 1 — Accumulate historical sub-goals from vault entries
|
|
676
|
+
for (const entry of completedEntries) {
|
|
677
|
+
const rnd = entryRound(entry);
|
|
678
|
+
const emerged = entryEmergedSubtasks(entry);
|
|
679
|
+
for (const desc of emerged) {
|
|
680
|
+
const id = deriveSubGoalId(desc);
|
|
681
|
+
// Dedup: skip if an existing sub-goal is too similar
|
|
682
|
+
if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
|
|
683
|
+
continue;
|
|
684
|
+
subGoals.push(makeSubGoal({
|
|
685
|
+
id,
|
|
686
|
+
description: desc.trim(),
|
|
687
|
+
status: "pending",
|
|
688
|
+
declared_at_round: rnd,
|
|
689
|
+
status_changed_at_round: rnd,
|
|
690
|
+
priority: subGoals.length,
|
|
691
|
+
}));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// Phase 2 — Process current round's sub-task declarations
|
|
695
|
+
if (lastRoundResult) {
|
|
696
|
+
const rnd = lastRoundResult.round || (currentRound - 1);
|
|
697
|
+
// New sub-goals from this round
|
|
698
|
+
const emerged = lastRoundResult.emerged_subtasks ?? [];
|
|
699
|
+
for (const desc of emerged) {
|
|
700
|
+
if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
|
|
701
|
+
continue;
|
|
702
|
+
subGoals.push(makeSubGoal({
|
|
703
|
+
id: deriveSubGoalId(desc),
|
|
704
|
+
description: desc.trim(),
|
|
705
|
+
status: "pending",
|
|
706
|
+
declared_at_round: rnd,
|
|
707
|
+
status_changed_at_round: rnd,
|
|
708
|
+
priority: subGoals.length,
|
|
709
|
+
}));
|
|
710
|
+
}
|
|
711
|
+
// Completed sub-goals
|
|
712
|
+
const completed = lastRoundResult.completed_subtasks ?? [];
|
|
713
|
+
for (const desc of completed) {
|
|
714
|
+
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
715
|
+
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
716
|
+
subGoals[idx].status = "done";
|
|
717
|
+
subGoals[idx].status_changed_at_round = rnd;
|
|
718
|
+
subGoals[idx].completed_at_round = rnd;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
// Blocked sub-goals
|
|
722
|
+
const blocked = lastRoundResult.blocked_subtasks ?? [];
|
|
723
|
+
for (const desc of blocked) {
|
|
724
|
+
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
725
|
+
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
726
|
+
subGoals[idx].status = "blocked";
|
|
727
|
+
subGoals[idx].status_changed_at_round = rnd;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
// Canceled sub-goals
|
|
731
|
+
const canceled = lastRoundResult.canceled_subtasks ?? [];
|
|
732
|
+
for (const desc of canceled) {
|
|
733
|
+
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
734
|
+
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
735
|
+
subGoals[idx].status = "canceled";
|
|
736
|
+
subGoals[idx].status_changed_at_round = rnd;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// Phase 3 — Auto-derive: next_action match → in_progress
|
|
740
|
+
const nextAction = lastRoundResult.next_action?.trim();
|
|
741
|
+
if (nextAction) {
|
|
742
|
+
for (const sg of subGoals) {
|
|
743
|
+
if (sg.status === "pending") {
|
|
744
|
+
if (jaccardSimilarity(nextAction, sg.description) >= policy.subgoal_auto_in_progress_threshold) {
|
|
745
|
+
sg.status = "in_progress";
|
|
746
|
+
sg.status_changed_at_round = rnd;
|
|
747
|
+
break; // Only one sub-goal can be in_progress at a time
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Phase 4 — Auto-complete: new criteria match → done
|
|
753
|
+
const newCriteria = lastRoundResult.execution_evidence?.success_criteria_met ?? [];
|
|
754
|
+
if (newCriteria.length > 0) {
|
|
755
|
+
for (const sg of subGoals) {
|
|
756
|
+
if (sg.status === "pending" || sg.status === "in_progress") {
|
|
757
|
+
for (const crit of newCriteria) {
|
|
758
|
+
if (jaccardSimilarity(crit, sg.description) >= policy.subgoal_auto_complete_threshold) {
|
|
759
|
+
sg.status = "done";
|
|
760
|
+
sg.status_changed_at_round = rnd;
|
|
761
|
+
sg.completed_at_round = rnd;
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// Sort for display: in_progress first, then pending by age desc, then blocked, then done, then canceled
|
|
770
|
+
subGoals.sort((a, b) => {
|
|
771
|
+
const rank = (s) => {
|
|
772
|
+
switch (s.status) {
|
|
773
|
+
case "in_progress": return 0;
|
|
774
|
+
case "pending": return 1;
|
|
775
|
+
case "blocked": return 2;
|
|
776
|
+
case "done": return 3;
|
|
777
|
+
case "canceled": return 4;
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
const ra = rank(a), rb = rank(b);
|
|
781
|
+
if (ra !== rb)
|
|
782
|
+
return ra - rb;
|
|
783
|
+
// Within same status: pending by oldest first, done by most recent first
|
|
784
|
+
// (v2.14: the previous expression simplified to b.declared - a.declared,
|
|
785
|
+
// which sorted NEWEST first despite the comment)
|
|
786
|
+
if (a.status === "pending")
|
|
787
|
+
return a.declared_at_round - b.declared_at_round;
|
|
788
|
+
if (a.status === "done")
|
|
789
|
+
return (b.completed_at_round ?? 0) - (a.completed_at_round ?? 0);
|
|
790
|
+
return a.declared_at_round - b.declared_at_round;
|
|
791
|
+
});
|
|
792
|
+
return subGoals;
|
|
793
|
+
}
|
|
794
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
388
795
|
// Time-Aware Constraint Management (v2.3)
|
|
389
796
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
390
797
|
/** Determine the source of a constraint by matching against known sources. */
|
|
@@ -409,7 +816,9 @@ function findDiscoveredRound(text, vaultEntries) {
|
|
|
409
816
|
const rnd = entryRound(entry);
|
|
410
817
|
if (rnd < 1)
|
|
411
818
|
continue;
|
|
412
|
-
const discovered =
|
|
819
|
+
const discovered = Array.isArray(entry.discovered_constraints)
|
|
820
|
+
? entry.discovered_constraints.filter((v) => typeof v === "string")
|
|
821
|
+
: [];
|
|
413
822
|
for (const d of discovered) {
|
|
414
823
|
// Phase 1: exact ID match (v2.11)
|
|
415
824
|
if (d === targetId || deriveConstraintId(d) === targetId) {
|
|
@@ -431,23 +840,27 @@ function findDiscoveredRound(text, vaultEntries) {
|
|
|
431
840
|
* v2.11: ID-first matching. Checks exact constraint ID match (c-XXXXXXXX)
|
|
432
841
|
* against the derived ID of each vault entry's constraint_violations, then
|
|
433
842
|
* falls back to Jaccard similarity on text. Returns 0 if never violated. */
|
|
434
|
-
|
|
843
|
+
/** True when a reported violation text matches a constraint text — same
|
|
844
|
+
* ID-first + Jaccard fallback matching as findLastViolatedRound. */
|
|
845
|
+
function matchesConstraintText(text, violation) {
|
|
435
846
|
const targetId = deriveConstraintId(text);
|
|
847
|
+
if (violation === targetId || deriveConstraintId(violation) === targetId) {
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
return jaccardSimilarity(text, violation) >=
|
|
851
|
+
getPolicy().evolution.constraint_match_threshold;
|
|
852
|
+
}
|
|
853
|
+
function findLastViolatedRound(text, vaultEntries) {
|
|
436
854
|
let latest = 0;
|
|
437
855
|
for (const entry of vaultEntries) {
|
|
438
856
|
const rnd = entryRound(entry);
|
|
439
857
|
if (rnd < 1)
|
|
440
858
|
continue;
|
|
441
|
-
const violations =
|
|
859
|
+
const violations = Array.isArray(entry.constraint_violations)
|
|
860
|
+
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
861
|
+
: [];
|
|
442
862
|
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) {
|
|
863
|
+
if (matchesConstraintText(text, v)) {
|
|
451
864
|
if (rnd > latest)
|
|
452
865
|
latest = rnd;
|
|
453
866
|
break;
|
|
@@ -464,20 +877,29 @@ function findLastViolatedRound(text, vaultEntries) {
|
|
|
464
877
|
* constraints never auto-decay. Inactive constraints are auto-reactivated
|
|
465
878
|
* if violated again in the current round.
|
|
466
879
|
*/
|
|
467
|
-
function manageConstraintLifecycle(active, objective, constraintsFromPlan, vaultContext, currentRound) {
|
|
880
|
+
function manageConstraintLifecycle(active, objective, constraintsFromPlan, vaultContext, currentRound, lastRoundViolations = []) {
|
|
468
881
|
const policy = getPolicy().evolution;
|
|
469
882
|
const allEntries = loopEntries(objective.loop_id || "", vaultContext);
|
|
470
883
|
const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
|
|
471
884
|
const metadata = [];
|
|
472
885
|
for (const text of active) {
|
|
473
886
|
const source = constraintSource(text, objective, constraintsFromPlan);
|
|
887
|
+
// v3.2.1: a violation reported in THIS round (last_round_result) counts
|
|
888
|
+
// as a violation at currentRound — the previous rounds' scan cannot see
|
|
889
|
+
// it (this round is not committed yet). This makes the L1 collapse
|
|
890
|
+
// render "(violated this round)" (diffConstraints compares
|
|
891
|
+
// last_violated_at_round === round) and re-activates decayed
|
|
892
|
+
// discovered constraints.
|
|
893
|
+
const violatedThisRound = lastRoundViolations.some((v) => matchesConstraintText(text, v));
|
|
474
894
|
// Structural constraints never decay
|
|
475
895
|
if (source === "hard" || source === "plan" || source === "criteria") {
|
|
476
896
|
metadata.push(makeConstraintMeta({
|
|
477
897
|
id: deriveConstraintId(text),
|
|
478
898
|
text,
|
|
479
899
|
discovered_at_round: objective.created_at_round || 1,
|
|
480
|
-
last_violated_at_round:
|
|
900
|
+
last_violated_at_round: violatedThisRound
|
|
901
|
+
? currentRound
|
|
902
|
+
: findLastViolatedRound(text, completedEntries),
|
|
481
903
|
source,
|
|
482
904
|
status: "active",
|
|
483
905
|
}));
|
|
@@ -485,7 +907,9 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
|
|
|
485
907
|
}
|
|
486
908
|
// Discovered constraints: check for inactivity
|
|
487
909
|
const discoveredAt = findDiscoveredRound(text, completedEntries);
|
|
488
|
-
const lastViolated =
|
|
910
|
+
const lastViolated = violatedThisRound
|
|
911
|
+
? currentRound
|
|
912
|
+
: findLastViolatedRound(text, completedEntries);
|
|
489
913
|
const roundsSinceRelevant = lastViolated > 0
|
|
490
914
|
? currentRound - lastViolated
|
|
491
915
|
: currentRound - (discoveredAt || currentRound);
|
|
@@ -513,21 +937,25 @@ export function alignTask(proposedTask, request, context) {
|
|
|
513
937
|
const objective = request.loop_objective ?? latestObjective(request.loop_id, context);
|
|
514
938
|
if (!objective)
|
|
515
939
|
return makeTaskAlignment();
|
|
940
|
+
// v2.14: threshold is policy-driven (evolution.progress_mismatch_threshold)
|
|
941
|
+
const threshold = getPolicy().evolution.progress_mismatch_threshold;
|
|
516
942
|
const score = jaccardSimilarity(proposedTask, [objective.objective, ...objective.success_criteria, ...objective.hard_constraints].join(" "));
|
|
517
943
|
return makeTaskAlignment({
|
|
518
|
-
is_aligned: score >=
|
|
944
|
+
is_aligned: score >= threshold,
|
|
519
945
|
alignment_score: Number(score.toFixed(2)),
|
|
520
|
-
warning: score <
|
|
521
|
-
escalation: score <
|
|
946
|
+
warning: score < threshold ? "Current task may be drifting from the loop objective." : "",
|
|
947
|
+
escalation: score < threshold ? "warn" : "none",
|
|
522
948
|
});
|
|
523
949
|
}
|
|
524
950
|
export function checkLoopHealth(loopId, request, context) {
|
|
525
951
|
const previous = getPreviousRound(loopId, request.round - 1, context);
|
|
526
952
|
const alignment = alignTask(request.task, request, context);
|
|
527
|
-
const violations = request.
|
|
953
|
+
const violations = request.last_round_result?.constraint_violations.length ?? 0;
|
|
528
954
|
const integrity = Math.max(0, 1 - violations * 0.2);
|
|
529
955
|
const continuity = previous ? jaccardSimilarity(previous.task, request.task) : 1;
|
|
530
|
-
|
|
956
|
+
// v2.14: thresholds are policy-driven
|
|
957
|
+
const drift = alignment.alignment_score < getPolicy().evolution.progress_mismatch_threshold ||
|
|
958
|
+
continuity < getPolicy().evolution.task_continuity_threshold;
|
|
531
959
|
return makeLoopHealth({
|
|
532
960
|
goal_alignment: alignment.alignment_score,
|
|
533
961
|
constraint_integrity: integrity,
|
|
@@ -539,115 +967,91 @@ export function checkLoopHealth(loopId, request, context) {
|
|
|
539
967
|
}
|
|
540
968
|
function levelDecision(request, context) {
|
|
541
969
|
const previous = getPreviousRound(request.loop_id, request.round - 1, context);
|
|
542
|
-
const last = request.
|
|
970
|
+
const last = request.last_round_result;
|
|
543
971
|
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);
|
|
972
|
+
last?.discovered_constraints?.length ||
|
|
973
|
+
last?.objective_refinement?.trim() ||
|
|
974
|
+
last?.emerged_subtasks?.length ||
|
|
975
|
+
last?.retracted_constraints?.length ||
|
|
976
|
+
last?.revised_success_criteria?.length ||
|
|
977
|
+
last?.wrong_assumptions?.length);
|
|
551
978
|
const lastFullRound = loopEntries(request.loop_id, context)
|
|
552
979
|
.filter((entry) => lineage(entry).recompile_level === "l2")
|
|
553
980
|
.map(entryRound)
|
|
554
981
|
.filter((round) => round < request.round)
|
|
555
982
|
.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;
|
|
983
|
+
// v2.14: a backtrack committed for this round means the loop is
|
|
984
|
+
// re-walking restored state — the recovery boundary forces L2
|
|
985
|
+
// rehydration (the decidePromptLevel recovery_boundary branch existed
|
|
986
|
+
// but no caller ever set it).
|
|
987
|
+
const recoveryBoundary = loopEntries(request.loop_id, context)
|
|
988
|
+
.some((entry) => lineage(entry).committed_action === "backtrack" &&
|
|
989
|
+
entryRound(entry) === request.round);
|
|
576
990
|
return decidePromptLevel({
|
|
577
991
|
round: request.round,
|
|
578
992
|
attempt: request.attempt,
|
|
579
993
|
forceLevel: request.force_level,
|
|
580
994
|
hasPlanSource: Boolean(request.plan_source),
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
constraintChanged: Boolean(last?.discoveries?.newConstraints?.length || last?.constraintEvents?.retired?.length),
|
|
995
|
+
recoveryBoundary,
|
|
996
|
+
checkpointBoundary: last?.compression_checkpoint === true,
|
|
997
|
+
goalChanged: previous !== null && previous.goal_id !== deriveGoalId(request.loop_id, request.task, request.goal_id),
|
|
585
998
|
previousStateMissing: request.round > 1 && previous === null,
|
|
586
|
-
previousFailedWithoutNewInformation: last?.
|
|
999
|
+
previousFailedWithoutNewInformation: last?.success === false && !hasNewInformation,
|
|
587
1000
|
verificationContradicted: (request.verification_flags ?? [])
|
|
588
1001
|
.some((flag) => flag.severity === "error"),
|
|
589
1002
|
consecutiveRejections: request.consecutive_rejections,
|
|
590
1003
|
fullRefreshInterval: getPolicy().prompt.full_refresh_interval,
|
|
591
1004
|
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
1005
|
});
|
|
605
1006
|
}
|
|
606
1007
|
export function decideLevel(request, context) {
|
|
607
1008
|
return levelDecision(request, context).level;
|
|
608
1009
|
}
|
|
609
|
-
export function
|
|
1010
|
+
export function buildSelfEvalBlock(round, prevDriftFlags,
|
|
1011
|
+
/** v2.12: L0 is the minimal retry template — the v2.12 declarative fields
|
|
1012
|
+
* (outcome/blocker/retroactiveClaims) are L1/L2 additions so the retry
|
|
1013
|
+
* prompt stays within its tight budget. */
|
|
1014
|
+
level) {
|
|
610
1015
|
const hasIntentDrift = prevDriftFlags?.some(f => f.check === "intent_drift") ?? false;
|
|
611
1016
|
const hasSubGoalDrift = prevDriftFlags?.some(f => f.check === "subgoal_drift") ?? false;
|
|
612
1017
|
const needsClarification = hasIntentDrift || hasSubGoalDrift;
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
noChangeReason: "",
|
|
1018
|
+
const declareOutcome = level !== "l0";
|
|
1019
|
+
const evalObj = {
|
|
1020
|
+
success: false,
|
|
1021
|
+
output_summary: `<verified result of round ${round}>`,
|
|
1022
|
+
constraint_violations: ["<c-XXXXXXXX or text>"],
|
|
1023
|
+
should_continue: true,
|
|
1024
|
+
discovered_constraints: ["<new constraint>"],
|
|
1025
|
+
emerged_subtasks: [],
|
|
1026
|
+
execution_evidence: {
|
|
1027
|
+
files_changed: [],
|
|
1028
|
+
test_results: { passed: 0, failed: 0, skipped: 0 },
|
|
1029
|
+
success_criteria_met: ["<cr-XXXXXXXX or text>"],
|
|
1030
|
+
success_criteria_remaining: ["<cr-XXXXXXXX or text>"],
|
|
1031
|
+
progress_estimate: 0,
|
|
628
1032
|
},
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
},
|
|
635
|
-
subGoalEvents: { completed: [], blocked: [], canceled: [] },
|
|
1033
|
+
wrong_assumptions: [],
|
|
1034
|
+
next_action: "<next concrete action, or empty when complete>",
|
|
1035
|
+
completed_subtasks: ["<sg-XXXXXXXX or text>"],
|
|
1036
|
+
blocked_subtasks: ["<sg-XXXXXXXX or text>"],
|
|
1037
|
+
canceled_subtasks: ["<sg-XXXXXXXX or text>"],
|
|
636
1038
|
};
|
|
1039
|
+
// v2.12: Declarative tri-state outcome (L1/L2 only — keeps L0 retry lean)
|
|
1040
|
+
if (declareOutcome) {
|
|
1041
|
+
evalObj.outcome = "<success|partial|failed|blocked>";
|
|
1042
|
+
evalObj.blocker = "<required only when outcome=blocked>";
|
|
1043
|
+
evalObj.retroactiveClaims = [];
|
|
1044
|
+
}
|
|
637
1045
|
// v2.8: Inject drift clarification field when previous round had drift flags
|
|
638
1046
|
if (needsClarification) {
|
|
639
1047
|
const flagTypes = [
|
|
640
1048
|
hasIntentDrift ? "intent_drift" : "",
|
|
641
1049
|
hasSubGoalDrift ? "subgoal_drift" : "",
|
|
642
1050
|
].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
|
-
};
|
|
1051
|
+
evalObj.drift_clarification =
|
|
1052
|
+
`<explain why your Round ${round} actions diverged from your stated plan ` +
|
|
1053
|
+
`(${flagTypes} detected in previous round). ` +
|
|
1054
|
+
`Leave empty if no divergence occurred>`;
|
|
651
1055
|
}
|
|
652
1056
|
const lines = [
|
|
653
1057
|
"### LoopForge Evaluation (Required)",
|
|
@@ -656,13 +1060,14 @@ export function buildRoundReportBlock(round, prevDriftFlags) {
|
|
|
656
1060
|
"with your actual results and submit via `loopforge_next`.",
|
|
657
1061
|
];
|
|
658
1062
|
if (needsClarification) {
|
|
659
|
-
lines.push("", "> ⚠️ **Drift Detected:** The verification gate detected that your previous", "> round's
|
|
1063
|
+
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
1064
|
}
|
|
661
|
-
lines.push("
|
|
662
|
-
lines.push(JSON.stringify(
|
|
1065
|
+
lines.push("---loopforge-eval");
|
|
1066
|
+
lines.push(JSON.stringify(evalObj, null, 2));
|
|
1067
|
+
lines.push("---end-loopforge-eval");
|
|
663
1068
|
lines.push("");
|
|
664
1069
|
lines.push("IMPORTANT: Replace every <placeholder> with your actual data.");
|
|
665
|
-
lines.push("Set
|
|
1070
|
+
lines.push("Set success=true only when the full goal and ALL hard constraints are verified.");
|
|
666
1071
|
lines.push("Use IDs for exact matching: c-XXXXXXXX, cr-XXXXXXXX, sg-XXXXXXXX.");
|
|
667
1072
|
lines.push("For subtasks, use sub-goal IDs from the Sub-Goal Dashboard above.");
|
|
668
1073
|
return lines.join("\n");
|
|
@@ -674,10 +1079,14 @@ export function compileLoop(request, context) {
|
|
|
674
1079
|
const decision = levelDecision(request, context);
|
|
675
1080
|
const previous = getPreviousRound(request.loop_id, request.round - 1, context);
|
|
676
1081
|
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 =
|
|
1082
|
+
const constraints = evolveConstraints(request, objective, previous, context);
|
|
1083
|
+
const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round, request.last_round_result?.constraint_violations ?? []);
|
|
1084
|
+
const rolling = buildRollingSummary(request.loop_id, request.round, context, 0, decision.level);
|
|
1085
|
+
const subGoals = manageSubGoals(request.loop_id, request.round, request.last_round_result, context);
|
|
1086
|
+
// v3.2: Goal → criteria → evidence vertical view (derived, zero persistence).
|
|
1087
|
+
const criterionStatuses = deriveCriterionStatuses(request.loop_id, context, objective, request.round, subGoals, request.last_round_result);
|
|
1088
|
+
// v3.2: Lessons learned — repeated violations / verification failures.
|
|
1089
|
+
const lessons = deriveLessons(request.loop_id, context, request.round);
|
|
681
1090
|
const alignment = alignTask(request.task, request, context);
|
|
682
1091
|
const health = checkLoopHealth(request.loop_id, { ...request, loop_objective: objective }, context);
|
|
683
1092
|
const warnings = unique([
|
|
@@ -697,35 +1106,13 @@ export function compileLoop(request, context) {
|
|
|
697
1106
|
.filter((e) => entryRound(e) >= 1 && entryRound(e) < request.round);
|
|
698
1107
|
const trend = [];
|
|
699
1108
|
for (const entry of completedEntries.slice(-10)) {
|
|
700
|
-
const viols =
|
|
1109
|
+
const viols = Array.isArray(entry.constraint_violations)
|
|
1110
|
+
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
1111
|
+
: [];
|
|
701
1112
|
// Simple proxy: no violations = perfect trust, each violation = -0.1
|
|
702
1113
|
const score = Math.max(0, 1.0 - viols.length * 0.1);
|
|
703
1114
|
trend.push(Number(score.toFixed(2)));
|
|
704
1115
|
}
|
|
705
|
-
const verified = verifiedClaims(request.loop_id, context);
|
|
706
|
-
const latestReport = [...loopEntries(request.loop_id, context)].reverse()
|
|
707
|
-
.map((entry) => parseRoundReport(entry.round_report))
|
|
708
|
-
.find((report) => report !== null);
|
|
709
|
-
const audit = auditEntries(request.loop_id, loopEntries(request.loop_id, context));
|
|
710
|
-
const regressionObligations = audit.regressionObligations.map((obligation) => ({
|
|
711
|
-
id: obligation.id,
|
|
712
|
-
description: `Re-run ${obligation.check}`,
|
|
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
|
-
: [];
|
|
729
1116
|
const response = makeLoopCompileResponse({
|
|
730
1117
|
status: AgentStatus.OK,
|
|
731
1118
|
recompile_level: decision.level,
|
|
@@ -744,20 +1131,12 @@ export function compileLoop(request, context) {
|
|
|
744
1131
|
task_alignment: alignment,
|
|
745
1132
|
rolling_summary: rolling,
|
|
746
1133
|
sub_goals: subGoals,
|
|
1134
|
+
criterion_statuses: criterionStatuses,
|
|
1135
|
+
lessons,
|
|
747
1136
|
agent_trust_score: request.round > 1 ? Number(trustScore.toFixed(2)) : undefined,
|
|
748
1137
|
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
|
-
}),
|
|
1138
|
+
suggested_next_task: request.last_round_result?.next_action?.trim() ||
|
|
1139
|
+
request.last_round_result?.emerged_subtasks?.[0] || "",
|
|
761
1140
|
plan_source: request.plan_source,
|
|
762
1141
|
warnings,
|
|
763
1142
|
});
|
|
@@ -787,36 +1166,16 @@ export function compileLoop(request, context) {
|
|
|
787
1166
|
l2: adaptiveL2,
|
|
788
1167
|
},
|
|
789
1168
|
attempt: request.attempt,
|
|
790
|
-
|
|
1169
|
+
selfEvaluationBlock: buildSelfEvalBlock(request.round, request.verification_flags, decision.level),
|
|
791
1170
|
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);
|
|
1171
|
+
// v2.9: Model's information needs from the previous round's SelfEvaluation
|
|
1172
|
+
promptRequests: request.last_round_result?.prompt_requests,
|
|
1173
|
+
// v3.2: Previous L1 presentation as the collapse diff baseline.
|
|
1174
|
+
presentedBaseline: readPresentedBaseline(request.loop_id, request.round, context),
|
|
809
1175
|
});
|
|
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
1176
|
response.prompt = artifact.renderedPrompt;
|
|
817
1177
|
response.prompt_artifact = artifact;
|
|
818
1178
|
response.state_file_content = policy.state_file.enabled ? markdown : undefined;
|
|
819
|
-
response.canonical_state = state;
|
|
820
1179
|
return response;
|
|
821
1180
|
}
|
|
822
1181
|
//# sourceMappingURL=loop-compiler.js.map
|