loopforge 2.11.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 +4 -4
- 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 +21 -9
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +34 -16
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -6
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +40 -0
- package/dist/cognitive-governance.d.ts.map +1 -0
- package/dist/cognitive-governance.js +126 -0
- package/dist/cognitive-governance.js.map +1 -0
- package/dist/enforcement-gate.d.ts +8 -3
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +419 -92
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +47 -7
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +303 -60
- package/dist/engine.js.map +1 -1
- 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/generate-schema.js +6 -1
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- 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 +310 -38
- package/dist/loop-compiler.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 +91 -41
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +243 -173
- 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 +3 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +65 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +47 -139
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +284 -784
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +23 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +270 -79
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +11 -0
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +118 -21
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +26 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +13 -2
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +34 -1
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +320 -42
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/protocol.d.ts +169 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +39 -0
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +5 -5
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +9 -8
- package/dist/replay.js.map +1 -1
- package/dist/round-coordinator.d.ts +6 -3
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +38 -19
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +9 -4
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +15 -7
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +7 -3
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +93 -88
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +31 -1
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +115 -12
- package/dist/self-eval.js.map +1 -1
- package/dist/storage.d.ts +3 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +5 -4
- package/dist/storage.js.map +1 -1
- package/dist/token-utils.d.ts +20 -7
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +68 -9
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +54 -2
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +433 -25
- package/dist/verification-gate.js.map +1 -1
- package/loop_policy.json +35 -8
- package/package.json +3 -2
- package/skills/perception/SKILL.md +24 -6
- package/dist/backends/interface.d.ts +0 -39
- package/dist/backends/interface.d.ts.map +0 -1
- package/dist/backends/interface.js +0 -6
- package/dist/backends/interface.js.map +0 -1
package/dist/prompt-assembler.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { hashCanonicalState } from "./canonical-state.js";
|
|
9
|
-
import { jaccardSimilarity } from "./token-utils.js";
|
|
9
|
+
import { deriveItemId, STABLE_ID_RE, jaccardSimilarity } from "./token-utils.js";
|
|
10
10
|
import { getPolicy } from "./policy.js";
|
|
11
11
|
export const PROMPT_ARTIFACT_SCHEMA_VERSION = 1;
|
|
12
12
|
export const BASE_PROMPT_VERSION = "2.0.0";
|
|
@@ -23,10 +23,6 @@ function bullets(values, prefix = "- ") {
|
|
|
23
23
|
}
|
|
24
24
|
/** v2.11: Derive a stable item ID from text content.
|
|
25
25
|
* Same hash strategy as computeGoalTextHash in loop-compiler.ts. */
|
|
26
|
-
function deriveItemId(text) {
|
|
27
|
-
const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
|
|
28
|
-
return createHash("sha256").update(normalized).digest("hex").slice(0, 8);
|
|
29
|
-
}
|
|
30
26
|
/** v2.11: Render a bullet list with ID prefixes for constraints/criteria.
|
|
31
27
|
* Each item gets a [prefix-XXXXXXXX] tag. Items that already look like IDs
|
|
32
28
|
* are rendered as-is. When constraint_id_enabled is false, renders plain bullets. */
|
|
@@ -34,22 +30,62 @@ function bulletsWithIds(values, prefix) {
|
|
|
34
30
|
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
35
31
|
if (!idEnabled)
|
|
36
32
|
return bullets(values);
|
|
37
|
-
const idRegex = /^(c|cr|sg)-[a-f0-9]{8}$/;
|
|
38
33
|
return values.map((v) => {
|
|
39
34
|
const trimmed = v.trim();
|
|
40
|
-
if (
|
|
35
|
+
if (STABLE_ID_RE.test(trimmed)) {
|
|
41
36
|
return `- [\`${trimmed}\`] ${trimmed}`;
|
|
42
37
|
}
|
|
43
38
|
const id = prefix + "-" + deriveItemId(trimmed);
|
|
44
39
|
return `- [\`${id}\`] ${trimmed}`;
|
|
45
40
|
}).join("\n");
|
|
46
41
|
}
|
|
47
|
-
|
|
42
|
+
/** v3.2: Short imperative fix/action per verification check, appended to the
|
|
43
|
+
* L1/L2 Verification Gate lines. Deliberately shorter than the rejection-
|
|
44
|
+
* prompt wording in enforcement-gate.ts; keys are the plain check-name
|
|
45
|
+
* strings (NOT imported from verification-gate.ts — it already imports
|
|
46
|
+
* loop-compiler, which imports this module, so importing back would cycle).
|
|
47
|
+
* Full coverage of the 19 CHECK_* constants is enforced by a test. */
|
|
48
|
+
const VERIFICATION_ACTIONS = {
|
|
49
|
+
progress_regression: "Correct progress_estimate or explain the change in output_summary.",
|
|
50
|
+
empty_change_with_passing: "Report the actual files_changed, or set success=false with the real state.",
|
|
51
|
+
success_with_remaining_criteria: "Complete the remaining criteria before claiming success, or set success=false and report what remains.",
|
|
52
|
+
success_without_verified_evidence: "Back the success claim with machine-verifiable evidence: run tests or a required verification command and report the actual output.",
|
|
53
|
+
outcome_success_contradiction: "Reconcile the declared outcome with the success flag and resubmit an honest evaluation.",
|
|
54
|
+
success_claim_conflict: "Align the success claim with your own evaluation data before resubmitting.",
|
|
55
|
+
blocked_without_blocker: "State the concrete blocker, or drop the blocked outcome.",
|
|
56
|
+
retroactive_claim_bad_round: "Correct the referenced round number, or withdraw the retroactive claim.",
|
|
57
|
+
retroactive_claim_unverified: "Provide evidence for the retroactive claim, or withdraw it.",
|
|
58
|
+
duplicate_constraint_discovery: "Update the existing constraint entry instead of duplicating it.",
|
|
59
|
+
recurring_violation: "Explain why this violation keeps recurring and try a different approach — do not repeat the failed strategy.",
|
|
60
|
+
retract_fresh_constraint: "Justify the retraction explicitly, or restore the constraint.",
|
|
61
|
+
evidence_integrity: "Correct the mismatch between your claims and the recorded evidence.",
|
|
62
|
+
required_command_failed: "Re-run the required verification command and report its actual output before claiming success.",
|
|
63
|
+
command_evidence_mismatch: "Report the true command output and reconcile the mismatch.",
|
|
64
|
+
intent_drift: "Explain the pivot in drift_clarification with concrete references, or redo the round as promised.",
|
|
65
|
+
subgoal_drift: "Reconcile the sub-goal list with your reported work before resubmitting.",
|
|
66
|
+
backtrack_workspace_not_restored: "Restore the skipped-round files and the expected git HEAD, then resubmit.",
|
|
67
|
+
criteria_claims_unverified: "Provide evidence for each claimed success criterion.",
|
|
68
|
+
};
|
|
69
|
+
/** Fallback for unmapped check names (forward-compat with future checks). */
|
|
70
|
+
const VERIFICATION_ACTION_FALLBACK = "Re-examine the flagged claim and correct your evaluation before resubmitting.";
|
|
71
|
+
/** v3.2: The actionable instruction for a verification check. Exported for
|
|
72
|
+
* the coverage test (which imports the CHECK_* constants). */
|
|
73
|
+
export function verificationActionFor(check) {
|
|
74
|
+
return VERIFICATION_ACTIONS[check] ?? VERIFICATION_ACTION_FALLBACK;
|
|
75
|
+
}
|
|
76
|
+
function verificationText(state, level) {
|
|
48
77
|
const lines = state.verificationFlags.map((flag) => {
|
|
49
78
|
const icon = flag.severity === "error"
|
|
50
79
|
? "🚫"
|
|
51
80
|
: flag.severity === "warn" ? "⚠️" : "ℹ️";
|
|
52
|
-
|
|
81
|
+
const base = `- ${icon} [${flag.check}] ${flag.detail}`;
|
|
82
|
+
// v3.2: error/warn flags get an actionable continuation line in L1/L2 —
|
|
83
|
+
// the verdict is translated into what to DO. L0 keeps the compact retry
|
|
84
|
+
// prompt byte-identical; info flags stay informational.
|
|
85
|
+
if (level === "l0" || flag.severity === "info")
|
|
86
|
+
return base;
|
|
87
|
+
const label = flag.severity === "error" ? "Fix" : "Action";
|
|
88
|
+
return `${base}\n → ${label}: ${verificationActionFor(flag.check)}`;
|
|
53
89
|
});
|
|
54
90
|
if (state.verificationFlags.some((flag) => flag.severity === "error")) {
|
|
55
91
|
lines.push("- Gate Verdict: CONTRADICTED — resolve every error before claiming success.");
|
|
@@ -60,6 +96,81 @@ function activeNonHardConstraints(state) {
|
|
|
60
96
|
const hard = new Set(state.hardConstraints);
|
|
61
97
|
return state.activeConstraints.filter((value) => !hard.has(value));
|
|
62
98
|
}
|
|
99
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
100
|
+
// v3.2: L1 diff-collapse helpers
|
|
101
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
102
|
+
/** v3.2: Collapse only when at least this many items are unchanged — a tiny
|
|
103
|
+
* unchanged set renders in full (nothing worth saving). */
|
|
104
|
+
export const COLLAPSE_MIN_UNCHANGED = 3;
|
|
105
|
+
/** v3.2: Recent-rounds items kept in full when older ones collapse. */
|
|
106
|
+
export const KEEP_RECENT_ROUNDS = 3;
|
|
107
|
+
/** v3.2: Stable ID of a constraint text as rendered by bulletsWithIds —
|
|
108
|
+
* metadata's own id when present, else the derived hash. */
|
|
109
|
+
function constraintIdOf(text, metadata) {
|
|
110
|
+
const meta = metadata.find((m) => m.text === text);
|
|
111
|
+
return meta?.id ?? `c-${deriveItemId(text)}`;
|
|
112
|
+
}
|
|
113
|
+
export function diffConstraints(baseline, activeTexts, metadata, round, emphasized) {
|
|
114
|
+
if (!baseline)
|
|
115
|
+
return { changed: activeTexts, unchangedCount: 0, removedCount: 0 };
|
|
116
|
+
const baselineIds = new Set(baseline.constraintIds);
|
|
117
|
+
const currentIds = new Set(activeTexts.map((text) => constraintIdOf(text, metadata)));
|
|
118
|
+
const changed = [];
|
|
119
|
+
let unchangedCount = 0;
|
|
120
|
+
for (const text of activeTexts) {
|
|
121
|
+
const id = constraintIdOf(text, metadata);
|
|
122
|
+
const violatedThisRound = metadata.find((m) => m.text === text)
|
|
123
|
+
?.last_violated_at_round === round;
|
|
124
|
+
if (!baselineIds.has(id) || violatedThisRound)
|
|
125
|
+
changed.push(text);
|
|
126
|
+
else
|
|
127
|
+
unchangedCount++;
|
|
128
|
+
}
|
|
129
|
+
let removedCount = 0;
|
|
130
|
+
if (emphasized.size > 0) {
|
|
131
|
+
const emphasizedIds = new Set([...emphasized].map((text) => constraintIdOf(text, metadata)));
|
|
132
|
+
for (const id of baselineIds) {
|
|
133
|
+
if (!currentIds.has(id) && !emphasizedIds.has(id))
|
|
134
|
+
removedCount++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
for (const id of baselineIds) {
|
|
139
|
+
if (!currentIds.has(id))
|
|
140
|
+
removedCount++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { changed, unchangedCount, removedCount };
|
|
144
|
+
}
|
|
145
|
+
export function diffSubGoals(baseline, activeSubs) {
|
|
146
|
+
if (!baseline)
|
|
147
|
+
return { changed: activeSubs, unchangedCount: 0, removedCount: 0 };
|
|
148
|
+
const baselineById = new Map(baseline.subGoals);
|
|
149
|
+
const currentIds = new Set(activeSubs.map((sg) => sg.id));
|
|
150
|
+
const changed = [];
|
|
151
|
+
let unchangedCount = 0;
|
|
152
|
+
for (const sg of activeSubs) {
|
|
153
|
+
const prevStatus = baselineById.get(sg.id);
|
|
154
|
+
if (prevStatus === undefined || prevStatus !== sg.status)
|
|
155
|
+
changed.push(sg);
|
|
156
|
+
else
|
|
157
|
+
unchangedCount++;
|
|
158
|
+
}
|
|
159
|
+
let removedCount = 0;
|
|
160
|
+
for (const [id] of baseline.subGoals) {
|
|
161
|
+
if (!currentIds.has(id))
|
|
162
|
+
removedCount++;
|
|
163
|
+
}
|
|
164
|
+
return { changed, unchangedCount, removedCount };
|
|
165
|
+
}
|
|
166
|
+
/** v3.2: True when a milestone boundary was crossed since the baseline —
|
|
167
|
+
* the Recent Rounds section then renders in full (a phase ended). */
|
|
168
|
+
export function milestoneBoundaryChanged(baseline, milestones) {
|
|
169
|
+
if (!baseline)
|
|
170
|
+
return false;
|
|
171
|
+
const baselineRanges = new Set(baseline.milestoneRanges.map(([s, e]) => `${s}:${e}`));
|
|
172
|
+
return milestones.some((m) => !baselineRanges.has(`${m.round_range.start}:${m.round_range.end}`));
|
|
173
|
+
}
|
|
63
174
|
function commonMandatorySections(state,
|
|
64
175
|
/** v2.11: L0 skips ID rendering to stay within tight budget. */
|
|
65
176
|
level) {
|
|
@@ -89,7 +200,7 @@ level) {
|
|
|
89
200
|
if (state.verificationFlags.length > 0) {
|
|
90
201
|
sections.push({
|
|
91
202
|
id: "verification",
|
|
92
|
-
text: section("Verification Gate", verificationText(state)),
|
|
203
|
+
text: section("Verification Gate", verificationText(state, level)),
|
|
93
204
|
mandatory: true,
|
|
94
205
|
});
|
|
95
206
|
}
|
|
@@ -117,9 +228,10 @@ function l0Sections(state) {
|
|
|
117
228
|
}
|
|
118
229
|
return sections;
|
|
119
230
|
}
|
|
120
|
-
function l1Sections(state) {
|
|
231
|
+
function l1Sections(state, baseline, emphasized) {
|
|
121
232
|
const sections = commonMandatorySections(state, "l1");
|
|
122
233
|
const active = activeNonHardConstraints(state);
|
|
234
|
+
const collapseEnabled = getPolicy().prompt.l1_collapse_enabled;
|
|
123
235
|
if (state.changesSinceLastRound.length > 0) {
|
|
124
236
|
sections.push({
|
|
125
237
|
id: "changes",
|
|
@@ -127,12 +239,36 @@ function l1Sections(state) {
|
|
|
127
239
|
mandatory: false,
|
|
128
240
|
});
|
|
129
241
|
}
|
|
242
|
+
// v3.2: Active Constraints — collapse unchanged items against the previous
|
|
243
|
+
// round's presentation; new and violated-this-round constraints render in
|
|
244
|
+
// full. The collapse line points at the state file (regenerated every
|
|
245
|
+
// round), so no information is lost — only presentation is compacted.
|
|
130
246
|
if (active.length > 0) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
247
|
+
const diff = diffConstraints(baseline, active, state.constraintMetadata, state.round, emphasized);
|
|
248
|
+
const renderFull = !collapseEnabled || !baseline
|
|
249
|
+
|| diff.unchangedCount < COLLAPSE_MIN_UNCHANGED;
|
|
250
|
+
if (renderFull) {
|
|
251
|
+
sections.push({
|
|
252
|
+
id: "active_constraints",
|
|
253
|
+
text: section("Active Constraints / Success Criteria", bulletsWithIds(active, "c")),
|
|
254
|
+
mandatory: false,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
const lines = diff.changed.map((text) => {
|
|
259
|
+
const id = constraintIdOf(text, state.constraintMetadata);
|
|
260
|
+
const violated = state.constraintMetadata.find((m) => m.text === text)
|
|
261
|
+
?.last_violated_at_round === state.round;
|
|
262
|
+
const suffix = violated ? " (violated this round)" : " (new this round)";
|
|
263
|
+
return `- [\`${id}\`] ${text}${suffix}`;
|
|
264
|
+
});
|
|
265
|
+
lines.push(`- … ${diff.unchangedCount} unchanged constraints, ${diff.removedCount} demoted since R${baseline.round} (see state file)`);
|
|
266
|
+
sections.push({
|
|
267
|
+
id: "active_constraints",
|
|
268
|
+
text: section("Active Constraints / Success Criteria", lines.join("\n")),
|
|
269
|
+
mandatory: false,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
136
272
|
}
|
|
137
273
|
if (state.remainingCriteria.length > 0) {
|
|
138
274
|
sections.push({
|
|
@@ -148,22 +284,44 @@ function l1Sections(state) {
|
|
|
148
284
|
mandatory: false,
|
|
149
285
|
});
|
|
150
286
|
}
|
|
151
|
-
// v2.2: Sub-Goal Dashboard (compact — L1)
|
|
287
|
+
// v2.2: Sub-Goal Dashboard (compact — L1). v3.2: unchanged sub-goals
|
|
288
|
+
// collapse to a count line; new/transitioned ones render in full.
|
|
152
289
|
const activeSubs = state.subGoals.filter((sg) => sg.status === "in_progress" || sg.status === "pending");
|
|
153
290
|
if (activeSubs.length > 0) {
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
291
|
+
const diff = diffSubGoals(baseline, activeSubs);
|
|
292
|
+
const renderFull = !collapseEnabled || !baseline
|
|
293
|
+
|| diff.unchangedCount < COLLAPSE_MIN_UNCHANGED;
|
|
294
|
+
if (renderFull) {
|
|
295
|
+
const maxShow = 5;
|
|
296
|
+
const items = activeSubs.slice(0, maxShow).map((sg) => {
|
|
297
|
+
const icon = sg.status === "in_progress" ? "🔄" : "⏳";
|
|
298
|
+
return `${icon} [\`${sg.id}\`] ${sg.description}`;
|
|
299
|
+
});
|
|
300
|
+
if (activeSubs.length > maxShow) {
|
|
301
|
+
items.push(`... and ${activeSubs.length - maxShow} more`);
|
|
302
|
+
}
|
|
303
|
+
sections.push({
|
|
304
|
+
id: "sub_goals",
|
|
305
|
+
text: section("Active Sub-Goals", items.join("\n")),
|
|
306
|
+
mandatory: false,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
const transitionCount = diff.changed.filter((sg) => {
|
|
311
|
+
const prev = baseline.subGoals.find(([id]) => id === sg.id);
|
|
312
|
+
return prev !== undefined && prev[1] !== sg.status;
|
|
313
|
+
}).length;
|
|
314
|
+
const items = diff.changed.slice(0, 5).map((sg) => {
|
|
315
|
+
const icon = sg.status === "in_progress" ? "🔄" : "⏳";
|
|
316
|
+
return `${icon} [\`${sg.id}\`] ${sg.description}`;
|
|
317
|
+
});
|
|
318
|
+
items.push(`- … ${diff.unchangedCount} unchanged sub-goals, ${transitionCount + diff.removedCount} changed since R${baseline.round} (see state file)`);
|
|
319
|
+
sections.push({
|
|
320
|
+
id: "sub_goals",
|
|
321
|
+
text: section("Active Sub-Goals", items.join("\n")),
|
|
322
|
+
mandatory: false,
|
|
323
|
+
});
|
|
161
324
|
}
|
|
162
|
-
sections.push({
|
|
163
|
-
id: "sub_goals",
|
|
164
|
-
text: section("Active Sub-Goals", items.join("\n")),
|
|
165
|
-
mandatory: false,
|
|
166
|
-
});
|
|
167
325
|
}
|
|
168
326
|
if (state.discoveries.length > 0) {
|
|
169
327
|
sections.push({
|
|
@@ -172,17 +330,56 @@ function l1Sections(state) {
|
|
|
172
330
|
mandatory: false,
|
|
173
331
|
});
|
|
174
332
|
}
|
|
333
|
+
// v3.2: Recent Rounds — keep the newest KEEP_RECENT_ROUNDS in full, fold
|
|
334
|
+
// older ones into one pointer line (unless a milestone boundary crossed).
|
|
175
335
|
if (state.rollingOutcomes.length > 0) {
|
|
336
|
+
const boundaryChanged = milestoneBoundaryChanged(baseline, state.milestones);
|
|
337
|
+
const collapseable = collapseEnabled && baseline !== null
|
|
338
|
+
&& state.rollingOutcomes.length > KEEP_RECENT_ROUNDS && !boundaryChanged;
|
|
339
|
+
if (collapseable) {
|
|
340
|
+
const kept = state.rollingOutcomes.slice(-KEEP_RECENT_ROUNDS);
|
|
341
|
+
const earlier = state.rollingOutcomes.length - kept.length;
|
|
342
|
+
const lines = [
|
|
343
|
+
...kept,
|
|
344
|
+
`- … ${earlier} earlier round${earlier === 1 ? "" : "s"} (see state file)`,
|
|
345
|
+
];
|
|
346
|
+
sections.push({
|
|
347
|
+
id: "rolling_outcomes",
|
|
348
|
+
text: section("Recent Rounds", lines.join("\n")),
|
|
349
|
+
mandatory: false,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
sections.push({
|
|
354
|
+
id: "rolling_outcomes",
|
|
355
|
+
text: section("Recent Rounds", bullets(state.rollingOutcomes)),
|
|
356
|
+
mandatory: false,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// v3.2: L1 also renders the explicit external context (previously L2-only).
|
|
361
|
+
// Placed after the changes delta, within the soft budget.
|
|
362
|
+
if (state.externalContext) {
|
|
176
363
|
sections.push({
|
|
177
|
-
id: "
|
|
178
|
-
text: section("
|
|
364
|
+
id: "external_context",
|
|
365
|
+
text: section("External Context", state.externalContext),
|
|
179
366
|
mandatory: false,
|
|
180
367
|
});
|
|
181
368
|
}
|
|
182
|
-
|
|
369
|
+
// v3.2: Recurring Issues carries the deterministic violation lessons (with
|
|
370
|
+
// counts and rounds) when available; falls back to the rolling window text.
|
|
371
|
+
if (state.recurringIssues.length > 0 || state.lessons.length > 0) {
|
|
372
|
+
const violationLessons = state.lessons
|
|
373
|
+
.filter((lesson) => lesson.kind === "constraint_violation")
|
|
374
|
+
.slice(0, 3);
|
|
375
|
+
const body = violationLessons.length > 0
|
|
376
|
+
? violationLessons
|
|
377
|
+
.map((lesson) => `- ${lesson.text} (violated ${lesson.count}×: R${lesson.rounds.join(", R")})`)
|
|
378
|
+
.join("\n")
|
|
379
|
+
: bullets(state.recurringIssues);
|
|
183
380
|
sections.push({
|
|
184
381
|
id: "recurring_issues",
|
|
185
|
-
text: section("Recurring Issues",
|
|
382
|
+
text: section("Recurring Issues", body),
|
|
186
383
|
mandatory: false,
|
|
187
384
|
});
|
|
188
385
|
}
|
|
@@ -226,6 +423,24 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
226
423
|
sections.push({ id, text: section(title, body), mandatory: false });
|
|
227
424
|
}
|
|
228
425
|
}
|
|
426
|
+
// v3.2: Lessons learned — repeated violations / verification failures
|
|
427
|
+
// across the whole loop (not just the rolling window).
|
|
428
|
+
if (state.lessons.length > 0) {
|
|
429
|
+
const lessonLines = state.lessons.slice(0, 8).map((lesson) => {
|
|
430
|
+
const icon = lesson.kind === "constraint_violation"
|
|
431
|
+
? "🚫"
|
|
432
|
+
: lesson.kind === "verification_error" ? "⚠️" : "ℹ️";
|
|
433
|
+
return `- ${icon} ${lesson.text} — ${lesson.count}× (R${lesson.rounds.join(", R")})`;
|
|
434
|
+
});
|
|
435
|
+
if (state.lessons.length > 8) {
|
|
436
|
+
lessonLines.push(`- ... and ${state.lessons.length - 8} more`);
|
|
437
|
+
}
|
|
438
|
+
sections.push({
|
|
439
|
+
id: "lessons",
|
|
440
|
+
text: section("Lessons Learned", lessonLines.join("\n")),
|
|
441
|
+
mandatory: false,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
229
444
|
if (state.milestones.length > 0) {
|
|
230
445
|
const milestoneText = state.milestones
|
|
231
446
|
.map((m) => {
|
|
@@ -340,6 +555,22 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
340
555
|
lines.push(`- ... and ${state.progress.filesChanged.length - 10} more`);
|
|
341
556
|
}
|
|
342
557
|
}
|
|
558
|
+
// v3.2: Goal → criteria → evidence vertical view — per-criterion status
|
|
559
|
+
// with the round it was met and linked sub-goals. IDs follow
|
|
560
|
+
// constraint_id_enabled like every other ID-rendering path.
|
|
561
|
+
if (state.criterionStatuses.length > 0) {
|
|
562
|
+
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
563
|
+
lines.push("", "**Goal → Criteria**:");
|
|
564
|
+
for (const cs of state.criterionStatuses) {
|
|
565
|
+
const icon = cs.status === "met" ? "✅" : cs.status === "remaining" ? "⬜" : "❔";
|
|
566
|
+
const idTag = idEnabled ? ` [\`${cs.id}\`]` : "";
|
|
567
|
+
const met = cs.met_at_round !== undefined ? `(met R${cs.met_at_round})` : "";
|
|
568
|
+
const related = cs.related_subgoal_ids.length > 0
|
|
569
|
+
? ` [↔ ${cs.related_subgoal_ids.join(", ")}]`
|
|
570
|
+
: "";
|
|
571
|
+
lines.push(`- ${icon}${idTag} ${cs.text} ${met}${related}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
343
574
|
sections.push({
|
|
344
575
|
id: "progress",
|
|
345
576
|
text: section("Progress Dashboard", lines.join("\n")),
|
|
@@ -399,14 +630,15 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
399
630
|
}
|
|
400
631
|
return sections;
|
|
401
632
|
}
|
|
402
|
-
function selectSections(input) {
|
|
633
|
+
function selectSections(input, emphasized) {
|
|
403
634
|
if (input.mode === "full") {
|
|
404
635
|
return l2Sections(input.state, input.fullStateMarkdown);
|
|
405
636
|
}
|
|
406
637
|
if (input.level === "l0")
|
|
407
638
|
return l0Sections(input.state);
|
|
408
|
-
if (input.level === "l1")
|
|
409
|
-
return l1Sections(input.state);
|
|
639
|
+
if (input.level === "l1") {
|
|
640
|
+
return l1Sections(input.state, input.presentedBaseline ?? null, emphasized);
|
|
641
|
+
}
|
|
410
642
|
return l2Sections(input.state, input.fullStateMarkdown);
|
|
411
643
|
}
|
|
412
644
|
function renderWithinBudget(sections, fixedText, budget) {
|
|
@@ -434,13 +666,12 @@ function renderWithinBudget(sections, fixedText, budget) {
|
|
|
434
666
|
function matchEmphasize(targets, candidates, cap) {
|
|
435
667
|
if (targets.length === 0 || candidates.length === 0)
|
|
436
668
|
return [];
|
|
437
|
-
const idRegex = /^(c|cr|sg)-[a-f0-9]{8}$/;
|
|
438
669
|
const scored = candidates.map((c) => {
|
|
439
670
|
let best = 0;
|
|
440
671
|
for (const t of targets) {
|
|
441
672
|
let s = 0;
|
|
442
673
|
// Phase 1: exact ID match (v2.11)
|
|
443
|
-
const idMatch = t.match(
|
|
674
|
+
const idMatch = t.match(STABLE_ID_RE);
|
|
444
675
|
if (idMatch) {
|
|
445
676
|
const candidateId = idMatch[1] + "-" + deriveItemId(c);
|
|
446
677
|
if (candidateId === t)
|
|
@@ -483,7 +714,11 @@ function renderConfusionAlerts(points, state, expandAll) {
|
|
|
483
714
|
{ keyword: "objective", label: "Loop Objective", ref: "## Loop Objective" },
|
|
484
715
|
{ keyword: "loop", label: "Loop Summary", ref: "## Loop Summary" },
|
|
485
716
|
];
|
|
486
|
-
|
|
717
|
+
// v2.14: L2 renders up to the policy cap (was: every confusion point,
|
|
718
|
+
// unbounded by max_confusion_points); L1 keeps a single alert.
|
|
719
|
+
const shown = expandAll
|
|
720
|
+
? points.slice(0, getPolicy().prompt.max_confusion_points)
|
|
721
|
+
: points.slice(0, 1);
|
|
487
722
|
for (const point of shown) {
|
|
488
723
|
const truncated = point.length > 200 ? point.slice(0, 197) + "…" : point;
|
|
489
724
|
// Find best-matching state section
|
|
@@ -496,7 +731,7 @@ function renderConfusionAlerts(points, state, expandAll) {
|
|
|
496
731
|
bestSection = sec;
|
|
497
732
|
}
|
|
498
733
|
}
|
|
499
|
-
const pointer = bestSection && bestScore >=
|
|
734
|
+
const pointer = bestSection && bestScore >= getPolicy().prompt.confusion_section_threshold
|
|
500
735
|
? `→ Check **${bestSection.label}** (\`${bestSection.ref}\`) in the state file.`
|
|
501
736
|
: "→ Read the full state file for relevant context.";
|
|
502
737
|
lines.push(`- **"${truncated}"**`);
|
|
@@ -651,8 +886,13 @@ export function assemblePromptArtifact(input) {
|
|
|
651
886
|
|| input.mode === "pointer"
|
|
652
887
|
|| input.reasons.some((r) => fullContextReasons.has(r));
|
|
653
888
|
const stateFilePath = input.state.stateFilePath;
|
|
889
|
+
// v3.2: L1 annotates the pointer with the round the freshly-regenerated
|
|
890
|
+
// state file describes (it is rewritten every compile); L0/L2 keep the
|
|
891
|
+
// plain pointer byte-identical.
|
|
654
892
|
const stateFileRef = stateFilePath
|
|
655
|
-
?
|
|
893
|
+
? (input.level === "l1"
|
|
894
|
+
? `📄 Full state: \`${stateFilePath}\` (updated round ${input.state.round})\n`
|
|
895
|
+
: `📄 Full state: \`${stateFilePath}\`\n`)
|
|
656
896
|
: "";
|
|
657
897
|
const readInstruction = needsFullContext && stateFilePath
|
|
658
898
|
? [
|
|
@@ -672,7 +912,6 @@ export function assemblePromptArtifact(input) {
|
|
|
672
912
|
readInstruction,
|
|
673
913
|
"",
|
|
674
914
|
].join("\n");
|
|
675
|
-
const sections = selectSections(input);
|
|
676
915
|
const pointer = stateFilePath
|
|
677
916
|
? section("Full State", stateFilePath)
|
|
678
917
|
: "";
|
|
@@ -727,7 +966,45 @@ export function assemblePromptArtifact(input) {
|
|
|
727
966
|
...input.state.changesSinceLastRound,
|
|
728
967
|
], emphasizeCap)
|
|
729
968
|
: [];
|
|
969
|
+
// v2.14: emphasize is a pure reorder (zero token overhead) — the matched
|
|
970
|
+
// items are MOVED to the Critical Context section, not duplicated. Filter
|
|
971
|
+
// them out of the source arrays before section rendering; unmatched
|
|
972
|
+
// emphasizes are dropped (nothing to reorder → nothing rendered).
|
|
973
|
+
// v3.2.1: hard constraints and success criteria live in BOTH their own
|
|
974
|
+
// sections (Active Hard Constraints / Success Criteria) and the active
|
|
975
|
+
// constraint list (loop-compiler merges objective.hard_constraints and
|
|
976
|
+
// success_criteria into constraints_active), so they must be filtered
|
|
977
|
+
// from BOTH sources too — otherwise the emphasized item renders twice.
|
|
978
|
+
const emphasizedSet = new Set(emphasized);
|
|
979
|
+
const stateForSections = emphasizedSet.size > 0
|
|
980
|
+
? {
|
|
981
|
+
...input.state,
|
|
982
|
+
activeConstraints: input.state.activeConstraints.filter((item) => !emphasizedSet.has(item)),
|
|
983
|
+
hardConstraints: input.state.hardConstraints.filter((item) => !emphasizedSet.has(item)),
|
|
984
|
+
successCriteria: input.state.successCriteria.filter((item) => !emphasizedSet.has(item)),
|
|
985
|
+
discoveries: input.state.discoveries.filter((item) => !emphasizedSet.has(item)),
|
|
986
|
+
blockers: input.state.blockers.filter((item) => !emphasizedSet.has(item)),
|
|
987
|
+
changesSinceLastRound: input.state.changesSinceLastRound.filter((item) => !emphasizedSet.has(item)),
|
|
988
|
+
}
|
|
989
|
+
: input.state;
|
|
990
|
+
const sections = selectSections({ ...input, state: stateForSections }, emphasizedSet);
|
|
730
991
|
const criticalContextText = renderCriticalContext(emphasized, input.state);
|
|
992
|
+
// v3.2: L1 only — snapshot what this prompt actually presented, so the
|
|
993
|
+
// NEXT L1 compile can diff against it (persisted via PromptArtifact →
|
|
994
|
+
// lineage). L0/L2 leave it undefined: their presentation semantics differ
|
|
995
|
+
// (L2's dashboard includes done/canceled sub-goals, which would
|
|
996
|
+
// contaminate the L1 active-set diff).
|
|
997
|
+
let presentedState;
|
|
998
|
+
if (levelIsL1) {
|
|
999
|
+
const activeSubsForSnapshot = stateForSections.subGoals.filter((sg) => sg.status === "in_progress" || sg.status === "pending");
|
|
1000
|
+
presentedState = {
|
|
1001
|
+
round: input.state.round,
|
|
1002
|
+
constraintIds: activeNonHardConstraints(stateForSections)
|
|
1003
|
+
.map((text) => constraintIdOf(text, stateForSections.constraintMetadata)),
|
|
1004
|
+
subGoals: activeSubsForSnapshot.map((sg) => [sg.id, sg.status]),
|
|
1005
|
+
milestoneRanges: input.state.milestones.map((m) => [m.round_range.start, m.round_range.end]),
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
731
1008
|
// Expand: L1 only, max 1 section (L2 already expanded, L0 ignored)
|
|
732
1009
|
const expandCap = levelIsL1 ? policy.max_expand_l1 : 0;
|
|
733
1010
|
const expandedSection = (pr?.expand?.length && expandCap > 0)
|
|
@@ -775,6 +1052,7 @@ export function assemblePromptArtifact(input) {
|
|
|
775
1052
|
charCount: renderedPrompt.length,
|
|
776
1053
|
budgetExceeded: renderedPrompt.length > budget,
|
|
777
1054
|
generatedAt: Date.now(),
|
|
1055
|
+
...(presentedState ? { presentedState } : {}),
|
|
778
1056
|
};
|
|
779
1057
|
}
|
|
780
1058
|
//# sourceMappingURL=prompt-assembler.js.map
|