loopforge 3.7.1 → 3.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -130
- package/dist/audit.d.ts +53 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +106 -22
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +52 -67
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +134 -147
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +103 -4
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +13 -0
- package/dist/cognitive-facts.d.ts.map +1 -1
- package/dist/cognitive-facts.js +28 -13
- package/dist/cognitive-facts.js.map +1 -1
- package/dist/committed-round.d.ts +103 -21
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +188 -45
- package/dist/committed-round.js.map +1 -1
- package/dist/contract-items.d.ts +55 -0
- package/dist/contract-items.d.ts.map +1 -0
- package/dist/contract-items.js +180 -0
- package/dist/contract-items.js.map +1 -0
- package/dist/enforcement-gate.d.ts +20 -2
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +197 -386
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +53 -38
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts +4 -4
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +16 -14
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +85 -89
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +328 -184
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +76 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +127 -0
- package/dist/explain.js.map +1 -0
- package/dist/generate-schema.js +152 -16
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +40 -53
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +229 -553
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +0 -1
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +0 -4
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts.map +1 -1
- package/dist/loop-projection.js +3 -1
- package/dist/loop-projection.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +30 -21
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +47 -103
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +51 -7
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +39 -23
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +148 -163
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +89 -0
- package/dist/mcp/submission-boundary.d.ts.map +1 -0
- package/dist/mcp/submission-boundary.js +203 -0
- package/dist/mcp/submission-boundary.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +249 -128
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.js +2 -2
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +55 -93
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +149 -38
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +1 -32
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +282 -435
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -4
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +6 -7
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +365 -142
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +21 -66
- package/dist/protocol.js.map +1 -1
- package/dist/replay.js +2 -2
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +69 -57
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +104 -64
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +36 -16
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +166 -102
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +3 -5
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +0 -1
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +96 -0
- package/dist/round-facts.d.ts.map +1 -0
- package/dist/round-facts.js +216 -0
- package/dist/round-facts.js.map +1 -0
- package/dist/round-transaction.d.ts +19 -9
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +90 -22
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +87 -9
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +396 -27
- package/dist/self-eval.js.map +1 -1
- package/dist/subgoal-state.d.ts +77 -0
- package/dist/subgoal-state.d.ts.map +1 -0
- package/dist/subgoal-state.js +198 -0
- package/dist/subgoal-state.js.map +1 -0
- package/dist/token-utils.d.ts +52 -19
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +83 -72
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +14 -31
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +178 -496
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace.d.ts +9 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +17 -0
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +59 -83
- package/package.json +67 -67
- package/skills/perception/SKILL.md +218 -204
package/dist/prompt-assembler.js
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
* alone are over it — truncation of mandatory content never happens.
|
|
11
11
|
*/
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
|
-
import { activeSubGoalView,
|
|
14
|
-
import {
|
|
13
|
+
import { activeSubGoalView, buildPhaseLine, hashCanonicalState, milestoneHeading, } from "./canonical-state.js";
|
|
14
|
+
import { PROMPT_ARTIFACT_SCHEMA_VERSION } from "./protocol.js";
|
|
15
|
+
import { deriveItemId, STABLE_ID_RE, normalizeText } from "./token-utils.js";
|
|
15
16
|
import { getPolicy } from "./policy.js";
|
|
16
|
-
export const PROMPT_ARTIFACT_SCHEMA_VERSION = 1;
|
|
17
17
|
export const DEFAULT_PROMPT_BUDGETS = {
|
|
18
18
|
l0: 3000,
|
|
19
19
|
l1: 7000,
|
|
@@ -29,11 +29,9 @@ function bullets(values, prefix = "- ") {
|
|
|
29
29
|
* Same hash strategy as computeGoalTextHash in loop-compiler.ts. */
|
|
30
30
|
/** v2.11: Render a bullet list with ID prefixes for constraints/criteria.
|
|
31
31
|
* Each item gets a [prefix-XXXXXXXX] tag. Items that already look like IDs
|
|
32
|
-
* are rendered as-is.
|
|
32
|
+
* are rendered as-is. v3.8.1: IDs are always rendered — the
|
|
33
|
+
* `constraint_id_enabled` escape hatch is gone. */
|
|
33
34
|
function bulletsWithIds(values, prefix) {
|
|
34
|
-
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
35
|
-
if (!idEnabled)
|
|
36
|
-
return bullets(values);
|
|
37
35
|
return values.map((v) => {
|
|
38
36
|
const trimmed = v.trim();
|
|
39
37
|
if (STABLE_ID_RE.test(trimmed)) {
|
|
@@ -52,9 +50,6 @@ function bulletsWithIds(values, prefix) {
|
|
|
52
50
|
* one prompt. Falls back to a c- derivation only when metadata is missing
|
|
53
51
|
* (defensive: metadata covers every active text by construction). */
|
|
54
52
|
function bulletsWithIdentities(values, metadata) {
|
|
55
|
-
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
56
|
-
if (!idEnabled)
|
|
57
|
-
return bullets(values);
|
|
58
53
|
const idByText = new Map(metadata.map((meta) => [meta.text, meta.id]));
|
|
59
54
|
return values.map((v) => {
|
|
60
55
|
const trimmed = v.trim();
|
|
@@ -75,7 +70,7 @@ function bulletsWithIdentities(values, metadata) {
|
|
|
75
70
|
* list — a new constant can never silently lack an action entry). */
|
|
76
71
|
const VERIFICATION_ACTIONS = {
|
|
77
72
|
success_with_remaining_criteria: "Complete the remaining criteria before claiming success, or set success=false and report what remains.",
|
|
78
|
-
success_without_verified_evidence: "Back the success claim with machine-verifiable evidence: run tests or a required verification command and report the actual output. If you genuinely performed no work, provide
|
|
73
|
+
success_without_verified_evidence: "Back the success claim with machine-verifiable evidence: run tests or a required verification command and report the actual output. If you genuinely performed no work, provide execution_report and declare no_change_reason.",
|
|
79
74
|
outcome_success_contradiction: "Reconcile the declared outcome with the success flag and resubmit an honest evaluation.",
|
|
80
75
|
blocked_without_blocker: "State the concrete blocker, or drop the blocked outcome.",
|
|
81
76
|
retroactive_claim_bad_round: "Correct the referenced round number, or withdraw the retroactive claim.",
|
|
@@ -86,21 +81,16 @@ const VERIFICATION_ACTIONS = {
|
|
|
86
81
|
evidence_integrity: "Correct the mismatch between your claims and the recorded evidence.",
|
|
87
82
|
required_command_failed: "Re-run the required verification command and report its actual output before claiming success.",
|
|
88
83
|
command_evidence_mismatch: "Report the true command output and reconcile the mismatch.",
|
|
89
|
-
intent_drift: "Explain the pivot in drift_clarification with concrete references, or redo the round as promised.",
|
|
90
|
-
subgoal_drift: "Reconcile the sub-goal list with your reported work before resubmitting.",
|
|
91
84
|
backtrack_workspace_not_restored: "Restore the skipped-round files and the expected git HEAD, then resubmit.",
|
|
92
85
|
criteria_claims_unverified: "Provide evidence for each claimed success criterion.",
|
|
93
86
|
// v3.3 — Round Contract checks (and the three v3.3 checks that predated
|
|
94
87
|
// this table entry and fell back to the generic action).
|
|
95
88
|
verification_entrypoint_modified: "Re-run the verification command on the changed entrypoint, or revert the entrypoint change and re-run.",
|
|
96
89
|
test_files_modified: "Re-run the verification command after the test-file changes and report the fresh output.",
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// v3.5 — contract completion machine-backing + premature-replacement warn
|
|
102
|
-
contract_completion_unverified: "Completion under the ACTIVE Round Contract requires its verification_plan commands to pass this round — fix the underlying failure so they pass and resubmit. Completion claims are not accepted without machine verification (no_change_reason does not apply).",
|
|
103
|
-
contract_premature: "The ACTIVE contract is still open — restate it unchanged to continue it; a different contract is ignored until the active one is completed or blocked.",
|
|
90
|
+
round_scope_drift: "Revert the out-of-scope file changes, or close the active contract and declare the extended scope in a new proposal. Scope drift is a machine fact and is not waivable by explanation.",
|
|
91
|
+
// v3.8 — item model: claimed-but-unverified debt
|
|
92
|
+
contract_items_unverified: "Run the bound command(s) for each claimed item so they are observed passing, or report the item as remaining (or declare outcome=\"blocked\").",
|
|
93
|
+
contract_premature: "The ACTIVE contract is still open — restate it unchanged to continue it; a different contract is ignored until every item is verified or the round reports blocked.",
|
|
104
94
|
// v3.7.1 — opt-in gate layer: cited gates need an approved human decision
|
|
105
95
|
user_gate_unresolved: "The round cites a high-risk action without an approved human decision. Run loopforge_gate_check, present the approval question, call loopforge_gate_resolve after the human decides, and resubmit with the approved gate id in evaluation.gate_ids.",
|
|
106
96
|
};
|
|
@@ -137,78 +127,12 @@ function activeNonHardConstraints(state) {
|
|
|
137
127
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
138
128
|
// v3.2: L1 diff-collapse helpers
|
|
139
129
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
140
|
-
/** v3.2: Collapse only when at least this many items are unchanged — a tiny
|
|
141
|
-
* unchanged set renders in full (nothing worth saving). */
|
|
142
|
-
export const COLLAPSE_MIN_UNCHANGED = 3;
|
|
143
|
-
/** v3.2: Recent-rounds items kept in full when older ones collapse. */
|
|
144
|
-
export const KEEP_RECENT_ROUNDS = 3;
|
|
145
130
|
/** v3.2: Stable ID of a constraint text as rendered by bulletsWithIds —
|
|
146
131
|
* metadata's own id when present, else the derived hash. */
|
|
147
132
|
function constraintIdOf(text, metadata) {
|
|
148
133
|
const meta = metadata.find((m) => m.text === text);
|
|
149
134
|
return meta?.id ?? `c-${deriveItemId(text)}`;
|
|
150
135
|
}
|
|
151
|
-
export function diffConstraints(baseline, activeTexts, metadata, round, emphasized) {
|
|
152
|
-
if (!baseline)
|
|
153
|
-
return { changed: activeTexts, unchangedCount: 0, removedCount: 0 };
|
|
154
|
-
const baselineIds = new Set(baseline.constraintIds);
|
|
155
|
-
const currentIds = new Set(activeTexts.map((text) => constraintIdOf(text, metadata)));
|
|
156
|
-
const changed = [];
|
|
157
|
-
let unchangedCount = 0;
|
|
158
|
-
for (const text of activeTexts) {
|
|
159
|
-
const id = constraintIdOf(text, metadata);
|
|
160
|
-
const violatedThisRound = metadata.find((m) => m.text === text)
|
|
161
|
-
?.last_violated_at_round === round;
|
|
162
|
-
if (!baselineIds.has(id) || violatedThisRound)
|
|
163
|
-
changed.push(text);
|
|
164
|
-
else
|
|
165
|
-
unchangedCount++;
|
|
166
|
-
}
|
|
167
|
-
let removedCount = 0;
|
|
168
|
-
if (emphasized.size > 0) {
|
|
169
|
-
const emphasizedIds = new Set([...emphasized].map((text) => constraintIdOf(text, metadata)));
|
|
170
|
-
for (const id of baselineIds) {
|
|
171
|
-
if (!currentIds.has(id) && !emphasizedIds.has(id))
|
|
172
|
-
removedCount++;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
for (const id of baselineIds) {
|
|
177
|
-
if (!currentIds.has(id))
|
|
178
|
-
removedCount++;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return { changed, unchangedCount, removedCount };
|
|
182
|
-
}
|
|
183
|
-
export function diffSubGoals(baseline, activeSubs) {
|
|
184
|
-
if (!baseline)
|
|
185
|
-
return { changed: activeSubs, unchangedCount: 0, removedCount: 0 };
|
|
186
|
-
const baselineById = new Map(baseline.subGoals);
|
|
187
|
-
const currentIds = new Set(activeSubs.map((sg) => sg.id));
|
|
188
|
-
const changed = [];
|
|
189
|
-
let unchangedCount = 0;
|
|
190
|
-
for (const sg of activeSubs) {
|
|
191
|
-
const prevStatus = baselineById.get(sg.id);
|
|
192
|
-
if (prevStatus === undefined || prevStatus !== sg.status)
|
|
193
|
-
changed.push(sg);
|
|
194
|
-
else
|
|
195
|
-
unchangedCount++;
|
|
196
|
-
}
|
|
197
|
-
let removedCount = 0;
|
|
198
|
-
for (const [id] of baseline.subGoals) {
|
|
199
|
-
if (!currentIds.has(id))
|
|
200
|
-
removedCount++;
|
|
201
|
-
}
|
|
202
|
-
return { changed, unchangedCount, removedCount };
|
|
203
|
-
}
|
|
204
|
-
/** v3.2: True when a milestone boundary was crossed since the baseline —
|
|
205
|
-
* the Recent Rounds section then renders in full (a phase ended). */
|
|
206
|
-
export function milestoneBoundaryChanged(baseline, milestones) {
|
|
207
|
-
if (!baseline)
|
|
208
|
-
return false;
|
|
209
|
-
const baselineRanges = new Set(baseline.milestoneRanges.map(([s, e]) => `${s}:${e}`));
|
|
210
|
-
return milestones.some((m) => !baselineRanges.has(`${m.round_range.start}:${m.round_range.end}`));
|
|
211
|
-
}
|
|
212
136
|
function commonMandatorySections(state,
|
|
213
137
|
/** v2.11: L0 skips ID rendering to stay within tight budget. */
|
|
214
138
|
level) {
|
|
@@ -266,14 +190,38 @@ function l0Sections(state) {
|
|
|
266
190
|
}
|
|
267
191
|
return sections;
|
|
268
192
|
}
|
|
269
|
-
|
|
193
|
+
/** v3.8.1: the committed-round window Active Warnings reports on. */
|
|
194
|
+
const ACTIVE_WARNINGS_WINDOW = 3;
|
|
195
|
+
/** v3.8.1: hard cap on Active Warnings lines. */
|
|
196
|
+
const ACTIVE_WARNINGS_CAP = 5;
|
|
197
|
+
/** v3.8.1: the prompt's slice of the recurring-fact list — the facts still
|
|
198
|
+
* firing inside the last ACTIVE_WARNINGS_WINDOW committed rounds.
|
|
199
|
+
*
|
|
200
|
+
* One list, two windows: this shows what is going wrong NOW (any count), the
|
|
201
|
+
* state file shows what keeps recurring (>= 2 rounds). Before this release
|
|
202
|
+
* the prompt carried a raw, unthresholded violation list AND a whole-history
|
|
203
|
+
* count >= 2 list, under two different headings.
|
|
204
|
+
*
|
|
205
|
+
* Returns "" when nothing is firing, so the caller renders no section. */
|
|
206
|
+
function activeWarnings(flags, currentRound) {
|
|
207
|
+
const cutoff = currentRound - ACTIVE_WARNINGS_WINDOW;
|
|
208
|
+
const shown = flags
|
|
209
|
+
.filter((flag) => flag.rounds.some((round) => round >= cutoff))
|
|
210
|
+
.sort((a, b) => Math.max(...b.rounds) - Math.max(...a.rounds) || b.count - a.count)
|
|
211
|
+
.slice(0, ACTIVE_WARNINGS_CAP);
|
|
212
|
+
if (shown.length === 0)
|
|
213
|
+
return "";
|
|
214
|
+
return shown.map((flag) => {
|
|
215
|
+
const icon = flag.kind === "constraint_violation"
|
|
216
|
+
? "🚫"
|
|
217
|
+
: flag.kind === "verification_error" ? "⚠️" : "ℹ️";
|
|
218
|
+
const subject = flag.ref ? `${flag.subject} [${flag.ref}]` : flag.subject;
|
|
219
|
+
return `- ${icon} ${subject} — ${flag.count}×, latest R${Math.max(...flag.rounds)}`;
|
|
220
|
+
}).join("\n");
|
|
221
|
+
}
|
|
222
|
+
function l1Sections(state) {
|
|
270
223
|
const sections = commonMandatorySections(state, "l1");
|
|
271
224
|
const active = activeNonHardConstraints(state);
|
|
272
|
-
// L7: collapse needs the state file — its collapse line points at the file
|
|
273
|
-
// for the full content. With the file disabled (stateFilePath empty),
|
|
274
|
-
// collapsing would silently truncate the prompt's constraints view.
|
|
275
|
-
const collapseEnabled = getPolicy().prompt.l1_collapse_enabled &&
|
|
276
|
-
state.stateFilePath.length > 0;
|
|
277
225
|
if (state.changesSinceLastRound.length > 0) {
|
|
278
226
|
sections.push({
|
|
279
227
|
id: "changes",
|
|
@@ -281,36 +229,27 @@ function l1Sections(state, baseline, emphasized) {
|
|
|
281
229
|
mandatory: false,
|
|
282
230
|
});
|
|
283
231
|
}
|
|
284
|
-
// v3.
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
232
|
+
// v3.8.1: Active Constraints render IN FULL. v3.2 folded unchanged items
|
|
233
|
+
// into "… N unchanged constraints (see state file)" pointer lines, which
|
|
234
|
+
// made the prompt's content depend on what the PREVIOUS prompt happened to
|
|
235
|
+
// show rather than on committed facts — and those pointer lines fed the
|
|
236
|
+
// prompt hash. The fold needed the previous round's persisted presentation
|
|
237
|
+
// snapshot, which is deleted.
|
|
288
238
|
if (active.length > 0) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
?.last_violated_at_round === state.round;
|
|
304
|
-
const suffix = violated ? " (violated this round)" : " (new this round)";
|
|
305
|
-
return `- [\`${id}\`] ${text}${suffix}`;
|
|
306
|
-
});
|
|
307
|
-
lines.push(`- … ${diff.unchangedCount} unchanged constraints, ${diff.removedCount} demoted since R${baseline.round} (see state file)`);
|
|
308
|
-
sections.push({
|
|
309
|
-
id: "active_constraints",
|
|
310
|
-
text: section("Active Constraints / Success Criteria", lines.join("\n")),
|
|
311
|
-
mandatory: false,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
239
|
+
// The one thing the folded branch carried that a plain bullet list does
|
|
240
|
+
// not: the machine fact that this round broke the constraint. Kept so the
|
|
241
|
+
// annotation is not lost along with the fold.
|
|
242
|
+
const lines = active.map((text) => {
|
|
243
|
+
const violated = state.constraintMetadata.find((m) => m.text === text)
|
|
244
|
+
?.last_violated_at_round === state.round;
|
|
245
|
+
return `- [\`${constraintIdOf(text, state.constraintMetadata)}\`] ${text}` +
|
|
246
|
+
(violated ? " (violated this round)" : "");
|
|
247
|
+
});
|
|
248
|
+
sections.push({
|
|
249
|
+
id: "active_constraints",
|
|
250
|
+
text: section("Active Constraints / Success Criteria", lines.join("\n")),
|
|
251
|
+
mandatory: false,
|
|
252
|
+
});
|
|
314
253
|
}
|
|
315
254
|
if (state.remainingCriteria.length > 0) {
|
|
316
255
|
sections.push({
|
|
@@ -326,42 +265,23 @@ function l1Sections(state, baseline, emphasized) {
|
|
|
326
265
|
mandatory: false,
|
|
327
266
|
});
|
|
328
267
|
}
|
|
329
|
-
// v2.2: Sub-Goal Dashboard (compact — L1). v3.
|
|
330
|
-
//
|
|
331
|
-
// v3.
|
|
332
|
-
// done/canceled never render as rows.
|
|
268
|
+
// v2.2: Sub-Goal Dashboard (compact — L1). v3.7.1: only ACTIVE items
|
|
269
|
+
// (pending/in_progress/blocked) enter the view; done/canceled never render
|
|
270
|
+
// as rows. v3.8.1: rendered in full — the unchanged-count fold is gone.
|
|
333
271
|
const view = activeSubGoalView(state.subGoals, Number.POSITIVE_INFINITY);
|
|
334
272
|
const activeSubs = view.active;
|
|
335
273
|
if (activeSubs.length > 0) {
|
|
336
274
|
const iconOf = (sg) => sg.status === "in_progress" ? "🔄" : sg.status === "blocked" ? "🚫" : "⏳";
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const maxShow = 5;
|
|
342
|
-
const items = activeSubs.slice(0, maxShow).map((sg) => `${iconOf(sg)} [\`${sg.id}\`] ${sg.description}`);
|
|
343
|
-
if (activeSubs.length > maxShow) {
|
|
344
|
-
items.push(`... and ${activeSubs.length - maxShow} more`);
|
|
345
|
-
}
|
|
346
|
-
sections.push({
|
|
347
|
-
id: "sub_goals",
|
|
348
|
-
text: section("Active Sub-Goals", items.join("\n")),
|
|
349
|
-
mandatory: false,
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
const transitionCount = diff.changed.filter((sg) => {
|
|
354
|
-
const prev = baseline.subGoals.find(([id]) => id === sg.id);
|
|
355
|
-
return prev !== undefined && prev[1] !== sg.status;
|
|
356
|
-
}).length;
|
|
357
|
-
const items = diff.changed.slice(0, 5).map((sg) => `${iconOf(sg)} [\`${sg.id}\`] ${sg.description}`);
|
|
358
|
-
items.push(`- … ${diff.unchangedCount} unchanged sub-goals, ${transitionCount + diff.removedCount} changed since R${baseline.round} (see state file)`);
|
|
359
|
-
sections.push({
|
|
360
|
-
id: "sub_goals",
|
|
361
|
-
text: section("Active Sub-Goals", items.join("\n")),
|
|
362
|
-
mandatory: false,
|
|
363
|
-
});
|
|
275
|
+
const maxShow = 5;
|
|
276
|
+
const items = activeSubs.slice(0, maxShow).map((sg) => `${iconOf(sg)} [\`${sg.id}\`] ${sg.description}`);
|
|
277
|
+
if (activeSubs.length > maxShow) {
|
|
278
|
+
items.push(`... and ${activeSubs.length - maxShow} more`);
|
|
364
279
|
}
|
|
280
|
+
sections.push({
|
|
281
|
+
id: "sub_goals",
|
|
282
|
+
text: section("Active Sub-Goals", items.join("\n")),
|
|
283
|
+
mandatory: false,
|
|
284
|
+
});
|
|
365
285
|
}
|
|
366
286
|
if (state.discoveries.length > 0) {
|
|
367
287
|
sections.push({
|
|
@@ -370,32 +290,14 @@ function l1Sections(state, baseline, emphasized) {
|
|
|
370
290
|
mandatory: false,
|
|
371
291
|
});
|
|
372
292
|
}
|
|
373
|
-
// v3.2: Recent Rounds
|
|
374
|
-
//
|
|
293
|
+
// v3.2: Recent Rounds. v3.8.1: rendered in full — the keep-newest-N fold and
|
|
294
|
+
// its milestone-boundary exception are gone.
|
|
375
295
|
if (state.rollingOutcomes.length > 0) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const earlier = state.rollingOutcomes.length - kept.length;
|
|
382
|
-
const lines = [
|
|
383
|
-
...kept,
|
|
384
|
-
`- … ${earlier} earlier round${earlier === 1 ? "" : "s"} (see state file)`,
|
|
385
|
-
];
|
|
386
|
-
sections.push({
|
|
387
|
-
id: "rolling_outcomes",
|
|
388
|
-
text: section("Recent Rounds", lines.join("\n")),
|
|
389
|
-
mandatory: false,
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
else {
|
|
393
|
-
sections.push({
|
|
394
|
-
id: "rolling_outcomes",
|
|
395
|
-
text: section("Recent Rounds", bullets(state.rollingOutcomes)),
|
|
396
|
-
mandatory: false,
|
|
397
|
-
});
|
|
398
|
-
}
|
|
296
|
+
sections.push({
|
|
297
|
+
id: "rolling_outcomes",
|
|
298
|
+
text: section("Recent Rounds", bullets(state.rollingOutcomes)),
|
|
299
|
+
mandatory: false,
|
|
300
|
+
});
|
|
399
301
|
}
|
|
400
302
|
// v3.2: L1 also renders the explicit external context (previously L2-only).
|
|
401
303
|
// Placed after the changes delta, within the soft budget.
|
|
@@ -406,36 +308,26 @@ function l1Sections(state, baseline, emphasized) {
|
|
|
406
308
|
mandatory: false,
|
|
407
309
|
});
|
|
408
310
|
}
|
|
409
|
-
// v3.
|
|
410
|
-
const
|
|
411
|
-
if (
|
|
311
|
+
// v3.8.1: one compact phase line, not the Roadmap prose.
|
|
312
|
+
const l1Phase = buildPhaseLine(state);
|
|
313
|
+
if (l1Phase) {
|
|
412
314
|
sections.push({
|
|
413
|
-
id: "
|
|
414
|
-
text: section("
|
|
315
|
+
id: "phase",
|
|
316
|
+
text: section("Phase", l1Phase),
|
|
415
317
|
mandatory: false,
|
|
416
318
|
});
|
|
417
319
|
}
|
|
418
|
-
// v3.
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
.map((lesson) => `- ${lesson.text} (violated ${lesson.count}×: R${lesson.rounds.join(", R")})`)
|
|
427
|
-
.join("\n")
|
|
428
|
-
: bullets(state.recurringIssues);
|
|
320
|
+
// v3.8.1: Active Warnings — the recent tail of the ONE recurring-fact
|
|
321
|
+
// derivation. It replaces two prompt sections ("Recurring Issues", fed by
|
|
322
|
+
// the last 5 rounds' raw violations with no threshold, and "Lessons
|
|
323
|
+
// Learned", fed by the whole-history count >= 2 view) that were two windows
|
|
324
|
+
// over one fact set. What the agent needs in the prompt is the recent tail;
|
|
325
|
+
// the full recurring set lives in the state file.
|
|
326
|
+
const warnings = activeWarnings(state.recurringFlags, state.round);
|
|
327
|
+
if (warnings) {
|
|
429
328
|
sections.push({
|
|
430
|
-
id: "
|
|
431
|
-
text: section("
|
|
432
|
-
mandatory: false,
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
if (state.nextAction) {
|
|
436
|
-
sections.push({
|
|
437
|
-
id: "next_action",
|
|
438
|
-
text: section("Next Action", state.nextAction),
|
|
329
|
+
id: "active_warnings",
|
|
330
|
+
text: section("Active Warnings", warnings),
|
|
439
331
|
mandatory: false,
|
|
440
332
|
});
|
|
441
333
|
}
|
|
@@ -461,7 +353,6 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
461
353
|
["blockers", "Blockers", state.blockers, null],
|
|
462
354
|
["discoveries", "Discoveries", state.discoveries, null],
|
|
463
355
|
["rolling_outcomes", "Cross-Round Outcomes", state.rollingOutcomes, null],
|
|
464
|
-
["recurring_issues", "Recurring Issues", state.recurringIssues, null],
|
|
465
356
|
["failed_patterns", "Failed Patterns", state.failedPatterns, null],
|
|
466
357
|
];
|
|
467
358
|
for (const [id, title, values, idPrefix] of groups) {
|
|
@@ -479,32 +370,26 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
479
370
|
: bullets(values);
|
|
480
371
|
sections.push({ id, text: section(title, body), mandatory: false });
|
|
481
372
|
}
|
|
482
|
-
// v3.
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
: lesson.kind === "verification_error" ? "⚠️" : "ℹ️";
|
|
489
|
-
return `- ${icon} ${lesson.text} — ${lesson.count}× (R${lesson.rounds.join(", R")})`;
|
|
490
|
-
});
|
|
491
|
-
if (state.lessons.length > 8) {
|
|
492
|
-
lessonLines.push(`- ... and ${state.lessons.length - 8} more`);
|
|
493
|
-
}
|
|
373
|
+
// v3.8.1: Active Warnings — the recent tail of the ONE recurring-fact
|
|
374
|
+
// derivation (see l1Sections). The whole-history recurring set is the state
|
|
375
|
+
// file's job; L2 shows the same recent slice as L1 so the two levels cannot
|
|
376
|
+
// tell different stories about what is going wrong right now.
|
|
377
|
+
const warnings = activeWarnings(state.recurringFlags, state.round);
|
|
378
|
+
if (warnings) {
|
|
494
379
|
sections.push({
|
|
495
|
-
id: "
|
|
496
|
-
text: section("
|
|
380
|
+
id: "active_warnings",
|
|
381
|
+
text: section("Active Warnings", warnings),
|
|
497
382
|
mandatory: false,
|
|
498
383
|
});
|
|
499
384
|
}
|
|
500
|
-
// v3.
|
|
501
|
-
// looking) Phase History so the agent sees where it is
|
|
502
|
-
//
|
|
503
|
-
const
|
|
504
|
-
if (
|
|
385
|
+
// v3.8.1: one compact phase line, not the Roadmap prose — rendered right
|
|
386
|
+
// before the (backward-looking) Phase History so the agent sees where it is
|
|
387
|
+
// before where it has been.
|
|
388
|
+
const l2Phase = buildPhaseLine(state);
|
|
389
|
+
if (l2Phase) {
|
|
505
390
|
sections.push({
|
|
506
|
-
id: "
|
|
507
|
-
text: section("
|
|
391
|
+
id: "phase",
|
|
392
|
+
text: section("Phase", l2Phase),
|
|
508
393
|
mandatory: false,
|
|
509
394
|
});
|
|
510
395
|
}
|
|
@@ -555,13 +440,6 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
555
440
|
mandatory: false,
|
|
556
441
|
});
|
|
557
442
|
}
|
|
558
|
-
if (state.nextAction) {
|
|
559
|
-
sections.push({
|
|
560
|
-
id: "next_action",
|
|
561
|
-
text: section("Next Action", state.nextAction),
|
|
562
|
-
mandatory: false,
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
443
|
if (state.externalContext) {
|
|
566
444
|
sections.push({
|
|
567
445
|
id: "external_context",
|
|
@@ -570,97 +448,13 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
570
448
|
});
|
|
571
449
|
}
|
|
572
450
|
// ── v2.8: Path B gap fills (previously only in full state markdown) ──
|
|
573
|
-
// Progress Dashboard
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const lines = [];
|
|
581
|
-
const total = state.progress.criteriaMet.length + state.progress.criteriaRemaining.length;
|
|
582
|
-
if (total > 0) {
|
|
583
|
-
lines.push(`**Criteria**: ${state.progress.criteriaMet.length}/${total} met`);
|
|
584
|
-
}
|
|
585
|
-
if (state.progress.estimate !== null) {
|
|
586
|
-
lines.push(`**Estimated Completion**: ${(state.progress.estimate * 100).toFixed(0)}%`);
|
|
587
|
-
// v3.3: honest labeling — the estimate is the agent's own number.
|
|
588
|
-
lines.push("**Signal source**: self-reported estimate (unverified until machine-backed)");
|
|
589
|
-
}
|
|
590
|
-
// v3.3: machine side of the comparison — git motion over committed
|
|
591
|
-
// rounds (v3.6: the object always carries definite values — no
|
|
592
|
-
// "unavailable" arm).
|
|
593
|
-
if (state.machineStatus) {
|
|
594
|
-
const ms = state.machineStatus;
|
|
595
|
-
const motion = ms.gitMotion
|
|
596
|
-
? `changes in ${ms.motionRounds}/${ms.windowRounds} recent committed rounds`
|
|
597
|
-
: `no git changes in the last ${ms.windowRounds} committed rounds`;
|
|
598
|
-
lines.push(`**Machine (git)**: ${motion}`);
|
|
599
|
-
}
|
|
600
|
-
if (state.criterionStatuses.length > 0) {
|
|
601
|
-
const metCount = state.criterionStatuses.filter((cs) => cs.status === "met").length;
|
|
602
|
-
lines.push(`**Machine (criteria)**: ${metCount}/${state.criterionStatuses.length} met across committed rounds`);
|
|
603
|
-
}
|
|
604
|
-
if (state.progress.tests) {
|
|
605
|
-
lines.push(`**Tests**: ${state.progress.tests.passed} passed, ` +
|
|
606
|
-
`${state.progress.tests.failed} failed, ${state.progress.tests.skipped} skipped`);
|
|
607
|
-
}
|
|
608
|
-
if (state.progress.filesChanged.length > 0) {
|
|
609
|
-
lines.push("**Files Changed**:");
|
|
610
|
-
for (const f of state.progress.filesChanged.slice(0, 10))
|
|
611
|
-
lines.push(`- ${f}`);
|
|
612
|
-
if (state.progress.filesChanged.length > 10) {
|
|
613
|
-
lines.push(`- ... and ${state.progress.filesChanged.length - 10} more`);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
// v3.2: Goal → criteria → evidence vertical view — per-criterion status
|
|
617
|
-
// with the round it was met and linked sub-goals. IDs follow
|
|
618
|
-
// constraint_id_enabled like every other ID-rendering path.
|
|
619
|
-
if (state.criterionStatuses.length > 0) {
|
|
620
|
-
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
621
|
-
lines.push("", "**Goal → Criteria**:");
|
|
622
|
-
for (const cs of state.criterionStatuses) {
|
|
623
|
-
const icon = cs.status === "met" ? "✅" : cs.status === "remaining" ? "⬜" : "❔";
|
|
624
|
-
const idTag = idEnabled ? ` [\`${cs.id}\`]` : "";
|
|
625
|
-
const met = cs.met_at_round !== undefined ? `(met R${cs.met_at_round})` : "";
|
|
626
|
-
const related = cs.related_subgoal_ids.length > 0
|
|
627
|
-
? ` [↔ ${cs.related_subgoal_ids.join(", ")}]`
|
|
628
|
-
: "";
|
|
629
|
-
lines.push(`- ${icon}${idTag} ${cs.text} ${met}${related}`);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
sections.push({
|
|
633
|
-
id: "progress",
|
|
634
|
-
text: section("Progress Dashboard", lines.join("\n")),
|
|
635
|
-
mandatory: false,
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
// v3.3: Per-round statistics — lets the agent calibrate round granularity
|
|
639
|
-
// (files per round, rejected attempts, self-reported progress deltas).
|
|
640
|
-
// v3.6: source-labeled — files/Δ are agent-reported; only rejected
|
|
641
|
-
// attempts (and the Machine (git) row above) are machine-recorded.
|
|
642
|
-
if (state.roundStats && state.roundStats.length > 0) {
|
|
643
|
-
const statLines = state.roundStats.map((stat) => {
|
|
644
|
-
const parts = [];
|
|
645
|
-
if (stat.filesChangedCount !== null) {
|
|
646
|
-
parts.push(`${stat.filesChangedCount} file${stat.filesChangedCount === 1 ? "" : "s"}`);
|
|
647
|
-
}
|
|
648
|
-
if (stat.rejectedAttempts !== null && stat.rejectedAttempts > 0) {
|
|
649
|
-
parts.push(`${stat.rejectedAttempts} rejected attempt${stat.rejectedAttempts === 1 ? "" : "s"}`);
|
|
650
|
-
}
|
|
651
|
-
if (stat.progressDelta !== null) {
|
|
652
|
-
parts.push(`Δ${stat.progressDelta >= 0 ? "+" : ""}${stat.progressDelta.toFixed(2)}`);
|
|
653
|
-
}
|
|
654
|
-
return `- R${stat.round}: ${parts.length > 0 ? parts.join(", ") : "no data"}`;
|
|
655
|
-
});
|
|
656
|
-
statLines.push("> files & Δ are agent-reported; rejected attempts are machine-recorded " +
|
|
657
|
-
"(the Machine (git) row above is machine-observed)");
|
|
658
|
-
sections.push({
|
|
659
|
-
id: "round_stats",
|
|
660
|
-
text: section("Round Stats", statLines.join("\n")),
|
|
661
|
-
mandatory: false,
|
|
662
|
-
});
|
|
663
|
-
}
|
|
451
|
+
// v3.8.1: the L2 Progress Dashboard and Round Stats sections are gone.
|
|
452
|
+
// The dashboard was `criterionStatuses` re-composed into a table and mixed
|
|
453
|
+
// with the agent’s OWN completion estimate and test counts; Round Stats
|
|
454
|
+
// re-listed per-round files and progress deltas, both self-reported. The
|
|
455
|
+
// criterion list and the machine git/criteria rows still render in the
|
|
456
|
+
// state file, where a human or external tool reads them — the prompt keeps
|
|
457
|
+
// only the current round’s own facts.
|
|
664
458
|
// Retired Constraints
|
|
665
459
|
if (state.retiredConstraints.length > 0) {
|
|
666
460
|
const lines = state.retiredConstraints.map((c) => `- ~${c}~ (retired)`);
|
|
@@ -670,86 +464,166 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
670
464
|
mandatory: false,
|
|
671
465
|
});
|
|
672
466
|
}
|
|
673
|
-
// Agent Trust Score + Trend
|
|
674
|
-
if (state.agentTrustScore !== undefined) {
|
|
675
|
-
const lines = [
|
|
676
|
-
trustBarLine(state.agentTrustScore),
|
|
677
|
-
];
|
|
678
|
-
if (state.agentTrustTrend.length > 0) {
|
|
679
|
-
const avg = (state.agentTrustTrend.reduce((a, b) => a + b, 0)
|
|
680
|
-
/ state.agentTrustTrend.length).toFixed(2);
|
|
681
|
-
lines.push(`Trend (last ${state.agentTrustTrend.length}): ${state.agentTrustTrend.join(" → ")}`);
|
|
682
|
-
lines.push(`Average: ${avg}`);
|
|
683
|
-
}
|
|
684
|
-
sections.push({
|
|
685
|
-
id: "agent_trust",
|
|
686
|
-
text: section("Agent Trust", lines.join("\n")),
|
|
687
|
-
mandatory: false,
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
467
|
return sections;
|
|
691
468
|
}
|
|
692
|
-
function selectSections(input
|
|
469
|
+
function selectSections(input) {
|
|
693
470
|
if (input.level === "l0")
|
|
694
471
|
return l0Sections(input.state);
|
|
695
|
-
if (input.level === "l1")
|
|
696
|
-
return l1Sections(input.state
|
|
697
|
-
}
|
|
472
|
+
if (input.level === "l1")
|
|
473
|
+
return l1Sections(input.state);
|
|
698
474
|
return l2Sections(input.state, input.fullStateMarkdown);
|
|
699
475
|
}
|
|
476
|
+
/** v3.8.1: render order, in ONE place.
|
|
477
|
+
*
|
|
478
|
+
* - A section absent from this table falls to DEFAULT_SECTION_PRIORITY.
|
|
479
|
+
* - A Section's `mandatory` flag means PROTECTED: never dropped, never
|
|
480
|
+
* truncated, rendered first regardless of budget.
|
|
481
|
+
* - Everything else is optional and is cut lowest-priority-first.
|
|
482
|
+
*
|
|
483
|
+
* Priority belongs to the SECTION, not to policy: a budget knob must not be
|
|
484
|
+
* able to change which facts count as more important. (v3.2–v3.8 had no
|
|
485
|
+
* order at all — optional sections were appended in declaration order, so a
|
|
486
|
+
* single large early section silently pushed out every later one.)
|
|
487
|
+
*/
|
|
488
|
+
const SECTION_PRIORITY = {
|
|
489
|
+
// ── protected ───────────────────────────────────────────────────────────
|
|
490
|
+
objective: 0,
|
|
491
|
+
current_task: 1,
|
|
492
|
+
hard_constraints: 2,
|
|
493
|
+
verification: 3,
|
|
494
|
+
retry_requirements: 4,
|
|
495
|
+
// Zero-token pure reorder, but it is the ONLY place emphasized items render
|
|
496
|
+
// (they are MOVED out of their source sections), so dropping it would delete
|
|
497
|
+
// them rather than just deprioritize them.
|
|
498
|
+
critical_context: 5,
|
|
499
|
+
// ── optional, most to least important ───────────────────────────────────
|
|
500
|
+
success_criteria: 10,
|
|
501
|
+
active_constraints: 11,
|
|
502
|
+
remaining: 12,
|
|
503
|
+
sub_goals: 13,
|
|
504
|
+
blockers: 14,
|
|
505
|
+
changes: 15,
|
|
506
|
+
discoveries: 16,
|
|
507
|
+
rolling_outcomes: 17,
|
|
508
|
+
phase: 18,
|
|
509
|
+
active_warnings: 19,
|
|
510
|
+
external_context: 20,
|
|
511
|
+
failed_patterns: 21,
|
|
512
|
+
retired_constraints: 22,
|
|
513
|
+
milestones: 23,
|
|
514
|
+
sub_goals_full: 24,
|
|
515
|
+
full_state: 25,
|
|
516
|
+
};
|
|
517
|
+
const DEFAULT_SECTION_PRIORITY = 50;
|
|
518
|
+
/** v3.8.1: the smallest remaining room worth spending on a truncated
|
|
519
|
+
* section. Below this a partial render is noise, so the section is dropped
|
|
520
|
+
* whole instead. */
|
|
521
|
+
const TRUNCATION_MIN_CHARS = 256;
|
|
522
|
+
/** Cut at a line boundary so a truncated section cannot end mid-sentence. */
|
|
523
|
+
function truncateAtLine(text, room) {
|
|
524
|
+
const clipped = text.slice(0, room);
|
|
525
|
+
const lastBreak = clipped.lastIndexOf("\n");
|
|
526
|
+
return `${lastBreak > 0 ? clipped.slice(0, lastBreak) : clipped}\n`;
|
|
527
|
+
}
|
|
700
528
|
function renderWithinBudget(sections, fixedText, budget) {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
529
|
+
// Total order: priority, then declaration order. Deterministic by
|
|
530
|
+
// construction — no dependence on map iteration or insertion accidents.
|
|
531
|
+
const ranked = sections
|
|
532
|
+
.map((item, index) => ({ item, index }))
|
|
533
|
+
.sort((a, b) => (SECTION_PRIORITY[a.item.id] ?? DEFAULT_SECTION_PRIORITY) -
|
|
534
|
+
(SECTION_PRIORITY[b.item.id] ?? DEFAULT_SECTION_PRIORITY) ||
|
|
535
|
+
a.index - b.index);
|
|
536
|
+
let rendered = fixedText;
|
|
537
|
+
const included = [];
|
|
538
|
+
const dropped = [];
|
|
539
|
+
for (const { item } of ranked) {
|
|
540
|
+
if (!item.text || !item.mandatory)
|
|
707
541
|
continue;
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
542
|
+
rendered += item.text;
|
|
543
|
+
included.push(item.id);
|
|
544
|
+
}
|
|
545
|
+
// Protected content alone can exceed the ceiling. That is RECORDED, never
|
|
546
|
+
// "fixed" by truncating a protected section.
|
|
547
|
+
const protectedOverflow = rendered.length > budget;
|
|
548
|
+
// Optional sections, STRICT priority: find the longest optional PREFIX that
|
|
549
|
+
// fits, and cut only from the tail of that ordering.
|
|
550
|
+
//
|
|
551
|
+
// Greedy first-fit is a different rule, and not the documented one: it can
|
|
552
|
+
// render a small low-priority section into room a larger high-priority
|
|
553
|
+
// section could not use, so "Blockers" could be missing while "Phase" is
|
|
554
|
+
// present. "Cut lowest-priority-first" has to mean what it says.
|
|
555
|
+
const optional = ranked.filter(({ item }) => item.text && !item.mandatory);
|
|
556
|
+
let prefix = optional.length;
|
|
557
|
+
while (prefix > 0) {
|
|
558
|
+
const total = optional
|
|
559
|
+
.slice(0, prefix)
|
|
560
|
+
.reduce((sum, { item }) => sum + item.text.length, 0);
|
|
561
|
+
if (rendered.length + total <= budget)
|
|
562
|
+
break;
|
|
563
|
+
prefix--;
|
|
564
|
+
}
|
|
565
|
+
for (const { item } of optional.slice(0, prefix)) {
|
|
566
|
+
rendered += item.text;
|
|
567
|
+
included.push(item.id);
|
|
568
|
+
}
|
|
569
|
+
for (const { item } of optional.slice(prefix)) {
|
|
570
|
+
dropped.push(item.id);
|
|
571
|
+
}
|
|
572
|
+
// Whatever room is left goes to the FIRST section that did not fit, rendered
|
|
573
|
+
// partially and cut at a line boundary. Exactly one section can be partial,
|
|
574
|
+
// so the cut is explainable ("this one did not fit whole") rather than a
|
|
575
|
+
// scatter of half-rendered sections.
|
|
576
|
+
const next = optional[prefix];
|
|
577
|
+
if (next) {
|
|
578
|
+
const room = budget - rendered.length;
|
|
579
|
+
if (room >= TRUNCATION_MIN_CHARS) {
|
|
580
|
+
rendered += truncateAtLine(next.item.text, room);
|
|
581
|
+
included.push(next.item.id);
|
|
582
|
+
dropped.shift();
|
|
711
583
|
}
|
|
712
584
|
}
|
|
713
|
-
return { rendered, included };
|
|
585
|
+
return { rendered, included, dropped, protectedOverflow };
|
|
714
586
|
}
|
|
715
587
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
716
588
|
// v2.9: Prompt Requests — model-expressed information needs
|
|
717
589
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
718
|
-
/** Match
|
|
719
|
-
* v2.11: ID-first
|
|
720
|
-
*
|
|
721
|
-
*
|
|
590
|
+
/** Match an emphasis target against state items.
|
|
591
|
+
* v2.11: ID-first — a target written as a stable id (`c-`/`cr-`/`sg-` + 8 hex)
|
|
592
|
+
* matches the candidate deriving to the same id.
|
|
593
|
+
* v3.8.1: the other arm is NORMALIZED-EXACT text. The Jaccard fallback is
|
|
594
|
+
* gone, so an emphasis can no longer pull in a near-miss because the two
|
|
595
|
+
* strings happened to share tokens. An unmatched target still renders
|
|
596
|
+
* nothing, keeping this a pure reorder with zero token overhead. */
|
|
722
597
|
function matchEmphasize(targets, candidates, cap) {
|
|
723
598
|
if (targets.length === 0 || candidates.length === 0)
|
|
724
599
|
return [];
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const idMatch = t.match(STABLE_ID_RE);
|
|
600
|
+
const matched = [];
|
|
601
|
+
for (const candidate of candidates) {
|
|
602
|
+
const candidateText = normalizeText(candidate);
|
|
603
|
+
const hit = targets.some((target) => {
|
|
604
|
+
const idMatch = target.match(STABLE_ID_RE);
|
|
731
605
|
if (idMatch) {
|
|
732
|
-
|
|
733
|
-
if (candidateId === t)
|
|
734
|
-
s = 1.0;
|
|
606
|
+
return `${idMatch[1]}-${deriveItemId(candidate)}` === target;
|
|
735
607
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
});
|
|
744
|
-
return scored
|
|
745
|
-
.filter((e) => e.score >= getPolicy().evolution.constraint_match_threshold)
|
|
746
|
-
.sort((a, b) => b.score - a.score)
|
|
747
|
-
.slice(0, cap)
|
|
748
|
-
.map((e) => e.text);
|
|
608
|
+
return normalizeText(target) === candidateText;
|
|
609
|
+
});
|
|
610
|
+
if (hit)
|
|
611
|
+
matched.push(candidate);
|
|
612
|
+
}
|
|
613
|
+
// Candidate order, capped — deterministic, and no score to sort by.
|
|
614
|
+
return matched.slice(0, cap);
|
|
749
615
|
}
|
|
750
616
|
/** Render the "Confusion Alerts" section that appears at the prompt top.
|
|
751
|
-
*
|
|
752
|
-
|
|
617
|
+
*
|
|
618
|
+
* v3.8.1: the points are echoed back, truncated and capped — nothing more.
|
|
619
|
+
* The compiler used to score each point against eight section keywords with
|
|
620
|
+
* Jaccard similarity and point at the argmax. That pointer was both a
|
|
621
|
+
* similarity verdict and, in practice, nearly always wrong: "I don't
|
|
622
|
+
* understand milestone tracking" scores 1/8 = 0.125 against the keyword
|
|
623
|
+
* `milestone`, below the 0.15 default, so it fell through to the generic
|
|
624
|
+
* line anyway. The agent names the state file itself; the prompt does not
|
|
625
|
+
* guess where to look. */
|
|
626
|
+
function renderConfusionAlerts(points, fullDensity) {
|
|
753
627
|
if (points.length === 0)
|
|
754
628
|
return "";
|
|
755
629
|
const lines = [
|
|
@@ -759,16 +633,6 @@ function renderConfusionAlerts(points, state, fullDensity) {
|
|
|
759
633
|
"Address each before proceeding — the state file has full context.",
|
|
760
634
|
"",
|
|
761
635
|
];
|
|
762
|
-
const sections = [
|
|
763
|
-
{ keyword: "milestone", label: "Phase History", ref: "## Phase History" },
|
|
764
|
-
{ keyword: "sub-goal", label: "Sub-Goal Dashboard", ref: "## Sub-Goal Dashboard" },
|
|
765
|
-
{ keyword: "constraint", label: "Active Constraints / Constraint Lifecycle", ref: "## Active Constraints" },
|
|
766
|
-
{ keyword: "trust", label: "Agent Trust", ref: "## Agent Trust" },
|
|
767
|
-
{ keyword: "progress", label: "Progress Dashboard", ref: "## Progress Dashboard" },
|
|
768
|
-
{ keyword: "criteria", label: "Success Criteria", ref: "## Success Criteria" },
|
|
769
|
-
{ keyword: "phase", label: "Phase History", ref: "## Phase History" },
|
|
770
|
-
{ keyword: "objective", label: "Loop Objective", ref: "## Loop Objective" },
|
|
771
|
-
];
|
|
772
636
|
// v2.14: L2 renders up to the policy cap (was: every confusion point,
|
|
773
637
|
// unbounded by max_confusion_points); L1 keeps a single alert.
|
|
774
638
|
const shown = fullDensity
|
|
@@ -776,21 +640,8 @@ function renderConfusionAlerts(points, state, fullDensity) {
|
|
|
776
640
|
: points.slice(0, 1);
|
|
777
641
|
for (const point of shown) {
|
|
778
642
|
const truncated = point.length > 200 ? point.slice(0, 197) + "…" : point;
|
|
779
|
-
// Find best-matching state section
|
|
780
|
-
let bestSection = null;
|
|
781
|
-
let bestScore = 0;
|
|
782
|
-
for (const sec of sections) {
|
|
783
|
-
const score = jaccardSimilarity(truncated.toLowerCase(), sec.keyword);
|
|
784
|
-
if (score > bestScore) {
|
|
785
|
-
bestScore = score;
|
|
786
|
-
bestSection = sec;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
const pointer = bestSection && bestScore >= getPolicy().prompt.confusion_section_threshold
|
|
790
|
-
? `→ Check **${bestSection.label}** (\`${bestSection.ref}\`) in the state file.`
|
|
791
|
-
: "→ Read the full state file for relevant context.";
|
|
792
643
|
lines.push(`- **"${truncated}"**`);
|
|
793
|
-
lines.push(
|
|
644
|
+
lines.push(" → Read the full state file for relevant context.");
|
|
794
645
|
lines.push("");
|
|
795
646
|
}
|
|
796
647
|
if (!fullDensity && points.length > 1) {
|
|
@@ -801,7 +652,7 @@ function renderConfusionAlerts(points, state, fullDensity) {
|
|
|
801
652
|
}
|
|
802
653
|
/** Render the "Critical Context" section — items the model emphasized.
|
|
803
654
|
* Pulled from active constraints, discoveries, and blocking issues. */
|
|
804
|
-
function renderCriticalContext(emphasized
|
|
655
|
+
function renderCriticalContext(emphasized) {
|
|
805
656
|
if (emphasized.length === 0)
|
|
806
657
|
return "";
|
|
807
658
|
const lines = [
|
|
@@ -822,14 +673,13 @@ export function assemblePromptArtifact(input) {
|
|
|
822
673
|
const stateHash = hashCanonicalState(input.state);
|
|
823
674
|
const attempt = Math.max(1, input.attempt ?? 1);
|
|
824
675
|
// ── State file reference + conditional read instruction ────────────
|
|
825
|
-
// L2 rounds (full rehydration), recovery
|
|
826
|
-
//
|
|
827
|
-
//
|
|
676
|
+
// L2 rounds (full rehydration), recovery and rejection all benefit from the
|
|
677
|
+
// model reading the durable state file rather than relying on
|
|
678
|
+
// potentially-compacted conversation history.
|
|
828
679
|
const fullContextReasons = new Set([
|
|
829
680
|
"first_round", "plan_boundary", "checkpoint_boundary", "goal_changed",
|
|
830
681
|
"missing_previous_state", "verification_contradicted",
|
|
831
|
-
"rejection_rehydrate", "recovery_boundary",
|
|
832
|
-
"periodic_refresh",
|
|
682
|
+
"rejection_rehydrate", "recovery_boundary",
|
|
833
683
|
]);
|
|
834
684
|
const needsFullContext = input.level === "l2"
|
|
835
685
|
|| input.reasons.some((r) => fullContextReasons.has(r));
|
|
@@ -881,8 +731,8 @@ export function assemblePromptArtifact(input) {
|
|
|
881
731
|
"- `should_continue`: Set to **`false` ONLY** when the ENTIRE loop task is done.",
|
|
882
732
|
" Partial progress or completed subtasks → `true`.",
|
|
883
733
|
"- `constraint_violations`: Be honest. List every constraint you actually violated.",
|
|
884
|
-
"- `
|
|
885
|
-
"- `
|
|
734
|
+
"- `execution_report.files_changed`: List files you modified — used for verification.",
|
|
735
|
+
"- `execution_report.tests_reported`: Report actual test runner output.",
|
|
886
736
|
"- `progress_estimate`: A number 0.0–1.0 reflecting overall task completion.",
|
|
887
737
|
"",
|
|
888
738
|
"### If the Prompt Says \"REJECTED\"",
|
|
@@ -897,11 +747,10 @@ export function assemblePromptArtifact(input) {
|
|
|
897
747
|
// ── v2.9: Prompt Requests — model-expressed information needs ──────────
|
|
898
748
|
const pr = input.promptRequests;
|
|
899
749
|
const levelIsL0 = input.level === "l0";
|
|
900
|
-
const levelIsL1 = input.level === "l1";
|
|
901
750
|
const levelIsL2 = input.level === "l2";
|
|
902
751
|
// Confusion alerts: L1/L2 only, rendered at the top (before mandatory sections)
|
|
903
752
|
const confusionText = (!levelIsL0 && pr?.confusion_points?.length)
|
|
904
|
-
? renderConfusionAlerts(pr.confusion_points,
|
|
753
|
+
? renderConfusionAlerts(pr.confusion_points, levelIsL2)
|
|
905
754
|
: "";
|
|
906
755
|
// Emphasize: L1/L2 only, matched against active state items
|
|
907
756
|
const policy = getPolicy().prompt;
|
|
@@ -935,24 +784,13 @@ export function assemblePromptArtifact(input) {
|
|
|
935
784
|
changesSinceLastRound: input.state.changesSinceLastRound.filter((item) => !emphasizedSet.has(item)),
|
|
936
785
|
}
|
|
937
786
|
: input.state;
|
|
938
|
-
const sections = selectSections({ ...input, state: stateForSections }
|
|
939
|
-
const criticalContextText = renderCriticalContext(emphasized
|
|
940
|
-
// v3.
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
//
|
|
945
|
-
let presentedState;
|
|
946
|
-
if (levelIsL1) {
|
|
947
|
-
const activeSubsForSnapshot = stateForSections.subGoals.filter((sg) => sg.status === "in_progress" || sg.status === "pending");
|
|
948
|
-
presentedState = {
|
|
949
|
-
round: input.state.round,
|
|
950
|
-
constraintIds: activeNonHardConstraints(stateForSections)
|
|
951
|
-
.map((text) => constraintIdOf(text, stateForSections.constraintMetadata)),
|
|
952
|
-
subGoals: activeSubsForSnapshot.map((sg) => [sg.id, sg.status]),
|
|
953
|
-
milestoneRanges: input.state.milestones.map((m) => [m.round_range.start, m.round_range.end]),
|
|
954
|
-
};
|
|
955
|
-
}
|
|
787
|
+
const sections = selectSections({ ...input, state: stateForSections });
|
|
788
|
+
const criticalContextText = renderCriticalContext(emphasized);
|
|
789
|
+
// v3.8.1: no presentation snapshot. The artifact records what THIS prompt
|
|
790
|
+
// rendered (level, hashes, round identity, the sections it emitted) and
|
|
791
|
+
// nothing about what a LATER prompt should do differently — a prompt's
|
|
792
|
+
// content must follow from committed facts, not from what the previous
|
|
793
|
+
// prompt happened to show.
|
|
956
794
|
// Inject confusion alerts into the header area (before mandatory sections)
|
|
957
795
|
const fixedText = header + confusionText;
|
|
958
796
|
// Inject critical context as a mandatory section
|
|
@@ -967,18 +805,27 @@ export function assemblePromptArtifact(input) {
|
|
|
967
805
|
...sections,
|
|
968
806
|
];
|
|
969
807
|
}
|
|
970
|
-
const
|
|
808
|
+
const sectionBudget = budget - footer.length;
|
|
809
|
+
const selected = renderWithinBudget(augmentedSections, fixedText, sectionBudget);
|
|
971
810
|
const renderedPrompt = selected.rendered + footer;
|
|
972
811
|
const promptHash = createHash("sha256").update(renderedPrompt).digest("hex");
|
|
973
812
|
return {
|
|
974
813
|
schemaVersion: PROMPT_ARTIFACT_SCHEMA_VERSION,
|
|
975
814
|
roundId: `loop:${input.state.loopId}:round:${input.state.round}`,
|
|
815
|
+
round: input.state.round,
|
|
976
816
|
attempt,
|
|
977
817
|
level: input.level,
|
|
978
818
|
renderedPrompt,
|
|
979
819
|
promptHash,
|
|
980
820
|
stateHash,
|
|
981
|
-
|
|
821
|
+
// v3.8.1: the artifact records what THIS prompt did — which sections it
|
|
822
|
+
// emitted, which the budget rule dropped, and whether the protected set
|
|
823
|
+
// overflowed. It records nothing about what a later prompt should do.
|
|
824
|
+
sections: selected.included,
|
|
825
|
+
droppedSections: selected.dropped,
|
|
826
|
+
protectedOverflow: selected.protectedOverflow,
|
|
827
|
+
budget: sectionBudget,
|
|
828
|
+
renderedChars: selected.rendered.length,
|
|
982
829
|
};
|
|
983
830
|
}
|
|
984
831
|
//# sourceMappingURL=prompt-assembler.js.map
|