loopforge 3.7.0 → 3.8.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/LICENSE +21 -21
- package/README.md +171 -157
- 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 +75 -69
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +276 -168
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +111 -12
- 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 +30 -16
- package/dist/cognitive-facts.js.map +1 -1
- package/dist/cognitive-governance.d.ts +30 -6
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +74 -18
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +109 -24
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +235 -52
- 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 +43 -20
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +269 -378
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +11 -6
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +102 -67
- 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 +91 -90
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +349 -160
- 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 +250 -53
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +41 -40
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +275 -515
- 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 +40 -21
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +173 -120
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +63 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +53 -7
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +240 -139
- 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 +377 -125
- 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 +65 -95
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +151 -39
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +7 -33
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +313 -473
- 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 +411 -168
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +23 -69
- 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 +55 -16
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +179 -57
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +7 -5
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +2 -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 +23 -9
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +99 -22
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +127 -11
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +517 -36
- 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 +27 -46
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +308 -557
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- 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 +17 -25
- package/package.json +7 -7
- package/skills/perception/SKILL.md +266 -64
package/dist/prompt-assembler.js
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
*
|
|
3
3
|
* L0/L1/L2 control state density only. Reasoning strategy belongs to the
|
|
4
4
|
* external Agent. Mandatory task, hard-constraint, and verification sections
|
|
5
|
-
* are never truncated
|
|
5
|
+
* are never truncated and always render. Budgets are soft CEILINGS applied
|
|
6
|
+
* to optional sections: optional sections are appended in priority order
|
|
7
|
+
* while the rendered length stays under the level's budget, and the rest are
|
|
8
|
+
* dropped from the prompt (the dropped content stays derivable from the
|
|
9
|
+
* vault). Prompt length can exceed the ceiling when the mandatory sections
|
|
10
|
+
* alone are over it — truncation of mandatory content never happens.
|
|
6
11
|
*/
|
|
7
12
|
import { createHash } from "node:crypto";
|
|
8
|
-
import {
|
|
9
|
-
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";
|
|
10
16
|
import { getPolicy } from "./policy.js";
|
|
11
|
-
export const PROMPT_ARTIFACT_SCHEMA_VERSION = 1;
|
|
12
17
|
export const DEFAULT_PROMPT_BUDGETS = {
|
|
13
18
|
l0: 3000,
|
|
14
19
|
l1: 7000,
|
|
@@ -24,11 +29,9 @@ function bullets(values, prefix = "- ") {
|
|
|
24
29
|
* Same hash strategy as computeGoalTextHash in loop-compiler.ts. */
|
|
25
30
|
/** v2.11: Render a bullet list with ID prefixes for constraints/criteria.
|
|
26
31
|
* Each item gets a [prefix-XXXXXXXX] tag. Items that already look like IDs
|
|
27
|
-
* 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. */
|
|
28
34
|
function bulletsWithIds(values, prefix) {
|
|
29
|
-
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
30
|
-
if (!idEnabled)
|
|
31
|
-
return bullets(values);
|
|
32
35
|
return values.map((v) => {
|
|
33
36
|
const trimmed = v.trim();
|
|
34
37
|
if (STABLE_ID_RE.test(trimmed)) {
|
|
@@ -47,9 +50,6 @@ function bulletsWithIds(values, prefix) {
|
|
|
47
50
|
* one prompt. Falls back to a c- derivation only when metadata is missing
|
|
48
51
|
* (defensive: metadata covers every active text by construction). */
|
|
49
52
|
function bulletsWithIdentities(values, metadata) {
|
|
50
|
-
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
51
|
-
if (!idEnabled)
|
|
52
|
-
return bullets(values);
|
|
53
53
|
const idByText = new Map(metadata.map((meta) => [meta.text, meta.id]));
|
|
54
54
|
return values.map((v) => {
|
|
55
55
|
const trimmed = v.trim();
|
|
@@ -70,7 +70,7 @@ function bulletsWithIdentities(values, metadata) {
|
|
|
70
70
|
* list — a new constant can never silently lack an action entry). */
|
|
71
71
|
const VERIFICATION_ACTIONS = {
|
|
72
72
|
success_with_remaining_criteria: "Complete the remaining criteria before claiming success, or set success=false and report what remains.",
|
|
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
|
|
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.",
|
|
74
74
|
outcome_success_contradiction: "Reconcile the declared outcome with the success flag and resubmit an honest evaluation.",
|
|
75
75
|
blocked_without_blocker: "State the concrete blocker, or drop the blocked outcome.",
|
|
76
76
|
retroactive_claim_bad_round: "Correct the referenced round number, or withdraw the retroactive claim.",
|
|
@@ -81,21 +81,18 @@ const VERIFICATION_ACTIONS = {
|
|
|
81
81
|
evidence_integrity: "Correct the mismatch between your claims and the recorded evidence.",
|
|
82
82
|
required_command_failed: "Re-run the required verification command and report its actual output before claiming success.",
|
|
83
83
|
command_evidence_mismatch: "Report the true command output and reconcile the mismatch.",
|
|
84
|
-
intent_drift: "Explain the pivot in drift_clarification with concrete references, or redo the round as promised.",
|
|
85
|
-
subgoal_drift: "Reconcile the sub-goal list with your reported work before resubmitting.",
|
|
86
84
|
backtrack_workspace_not_restored: "Restore the skipped-round files and the expected git HEAD, then resubmit.",
|
|
87
85
|
criteria_claims_unverified: "Provide evidence for each claimed success criterion.",
|
|
88
86
|
// v3.3 — Round Contract checks (and the three v3.3 checks that predated
|
|
89
87
|
// this table entry and fell back to the generic action).
|
|
90
88
|
verification_entrypoint_modified: "Re-run the verification command on the changed entrypoint, or revert the entrypoint change and re-run.",
|
|
91
89
|
test_files_modified: "Re-run the verification command after the test-file changes and report the fresh output.",
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// v3.
|
|
97
|
-
|
|
98
|
-
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.",
|
|
94
|
+
// v3.7.1 — opt-in gate layer: cited gates need an approved human decision
|
|
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.",
|
|
99
96
|
};
|
|
100
97
|
/** Fallback for unmapped check names (forward-compat with future checks). */
|
|
101
98
|
const VERIFICATION_ACTION_FALLBACK = "Re-examine the flagged claim and correct your evaluation before resubmitting.";
|
|
@@ -130,79 +127,13 @@ function activeNonHardConstraints(state) {
|
|
|
130
127
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
131
128
|
// v3.2: L1 diff-collapse helpers
|
|
132
129
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
133
|
-
/** v3.2: Collapse only when at least this many items are unchanged — a tiny
|
|
134
|
-
* unchanged set renders in full (nothing worth saving). */
|
|
135
|
-
export const COLLAPSE_MIN_UNCHANGED = 3;
|
|
136
|
-
/** v3.2: Recent-rounds items kept in full when older ones collapse. */
|
|
137
|
-
export const KEEP_RECENT_ROUNDS = 3;
|
|
138
130
|
/** v3.2: Stable ID of a constraint text as rendered by bulletsWithIds —
|
|
139
131
|
* metadata's own id when present, else the derived hash. */
|
|
140
132
|
function constraintIdOf(text, metadata) {
|
|
141
133
|
const meta = metadata.find((m) => m.text === text);
|
|
142
134
|
return meta?.id ?? `c-${deriveItemId(text)}`;
|
|
143
135
|
}
|
|
144
|
-
|
|
145
|
-
if (!baseline)
|
|
146
|
-
return { changed: activeTexts, unchangedCount: 0, removedCount: 0 };
|
|
147
|
-
const baselineIds = new Set(baseline.constraintIds);
|
|
148
|
-
const currentIds = new Set(activeTexts.map((text) => constraintIdOf(text, metadata)));
|
|
149
|
-
const changed = [];
|
|
150
|
-
let unchangedCount = 0;
|
|
151
|
-
for (const text of activeTexts) {
|
|
152
|
-
const id = constraintIdOf(text, metadata);
|
|
153
|
-
const violatedThisRound = metadata.find((m) => m.text === text)
|
|
154
|
-
?.last_violated_at_round === round;
|
|
155
|
-
if (!baselineIds.has(id) || violatedThisRound)
|
|
156
|
-
changed.push(text);
|
|
157
|
-
else
|
|
158
|
-
unchangedCount++;
|
|
159
|
-
}
|
|
160
|
-
let removedCount = 0;
|
|
161
|
-
if (emphasized.size > 0) {
|
|
162
|
-
const emphasizedIds = new Set([...emphasized].map((text) => constraintIdOf(text, metadata)));
|
|
163
|
-
for (const id of baselineIds) {
|
|
164
|
-
if (!currentIds.has(id) && !emphasizedIds.has(id))
|
|
165
|
-
removedCount++;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
for (const id of baselineIds) {
|
|
170
|
-
if (!currentIds.has(id))
|
|
171
|
-
removedCount++;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return { changed, unchangedCount, removedCount };
|
|
175
|
-
}
|
|
176
|
-
export function diffSubGoals(baseline, activeSubs) {
|
|
177
|
-
if (!baseline)
|
|
178
|
-
return { changed: activeSubs, unchangedCount: 0, removedCount: 0 };
|
|
179
|
-
const baselineById = new Map(baseline.subGoals);
|
|
180
|
-
const currentIds = new Set(activeSubs.map((sg) => sg.id));
|
|
181
|
-
const changed = [];
|
|
182
|
-
let unchangedCount = 0;
|
|
183
|
-
for (const sg of activeSubs) {
|
|
184
|
-
const prevStatus = baselineById.get(sg.id);
|
|
185
|
-
if (prevStatus === undefined || prevStatus !== sg.status)
|
|
186
|
-
changed.push(sg);
|
|
187
|
-
else
|
|
188
|
-
unchangedCount++;
|
|
189
|
-
}
|
|
190
|
-
let removedCount = 0;
|
|
191
|
-
for (const [id] of baseline.subGoals) {
|
|
192
|
-
if (!currentIds.has(id))
|
|
193
|
-
removedCount++;
|
|
194
|
-
}
|
|
195
|
-
return { changed, unchangedCount, removedCount };
|
|
196
|
-
}
|
|
197
|
-
/** v3.2: True when a milestone boundary was crossed since the baseline —
|
|
198
|
-
* the Recent Rounds section then renders in full (a phase ended). */
|
|
199
|
-
export function milestoneBoundaryChanged(baseline, milestones) {
|
|
200
|
-
if (!baseline)
|
|
201
|
-
return false;
|
|
202
|
-
const baselineRanges = new Set(baseline.milestoneRanges.map(([s, e]) => `${s}:${e}`));
|
|
203
|
-
return milestones.some((m) => !baselineRanges.has(`${m.round_range.start}:${m.round_range.end}`));
|
|
204
|
-
}
|
|
205
|
-
function commonMandatorySections(state,
|
|
136
|
+
function commonMandatorySections(state,
|
|
206
137
|
/** v2.11: L0 skips ID rendering to stay within tight budget. */
|
|
207
138
|
level) {
|
|
208
139
|
const sections = [
|
|
@@ -259,10 +190,38 @@ function l0Sections(state) {
|
|
|
259
190
|
}
|
|
260
191
|
return sections;
|
|
261
192
|
}
|
|
262
|
-
|
|
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) {
|
|
263
223
|
const sections = commonMandatorySections(state, "l1");
|
|
264
224
|
const active = activeNonHardConstraints(state);
|
|
265
|
-
const collapseEnabled = getPolicy().prompt.l1_collapse_enabled;
|
|
266
225
|
if (state.changesSinceLastRound.length > 0) {
|
|
267
226
|
sections.push({
|
|
268
227
|
id: "changes",
|
|
@@ -270,36 +229,27 @@ function l1Sections(state, baseline, emphasized) {
|
|
|
270
229
|
mandatory: false,
|
|
271
230
|
});
|
|
272
231
|
}
|
|
273
|
-
// v3.
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
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.
|
|
277
238
|
if (active.length > 0) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
?.last_violated_at_round === state.round;
|
|
293
|
-
const suffix = violated ? " (violated this round)" : " (new this round)";
|
|
294
|
-
return `- [\`${id}\`] ${text}${suffix}`;
|
|
295
|
-
});
|
|
296
|
-
lines.push(`- … ${diff.unchangedCount} unchanged constraints, ${diff.removedCount} demoted since R${baseline.round} (see state file)`);
|
|
297
|
-
sections.push({
|
|
298
|
-
id: "active_constraints",
|
|
299
|
-
text: section("Active Constraints / Success Criteria", lines.join("\n")),
|
|
300
|
-
mandatory: false,
|
|
301
|
-
});
|
|
302
|
-
}
|
|
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
|
+
});
|
|
303
253
|
}
|
|
304
254
|
if (state.remainingCriteria.length > 0) {
|
|
305
255
|
sections.push({
|
|
@@ -315,44 +265,23 @@ function l1Sections(state, baseline, emphasized) {
|
|
|
315
265
|
mandatory: false,
|
|
316
266
|
});
|
|
317
267
|
}
|
|
318
|
-
// v2.2: Sub-Goal Dashboard (compact — L1). v3.
|
|
319
|
-
//
|
|
320
|
-
|
|
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.
|
|
271
|
+
const view = activeSubGoalView(state.subGoals, Number.POSITIVE_INFINITY);
|
|
272
|
+
const activeSubs = view.active;
|
|
321
273
|
if (activeSubs.length > 0) {
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
const items = activeSubs.slice(0, maxShow).map((sg) => {
|
|
328
|
-
const icon = sg.status === "in_progress" ? "🔄" : "⏳";
|
|
329
|
-
return `${icon} [\`${sg.id}\`] ${sg.description}`;
|
|
330
|
-
});
|
|
331
|
-
if (activeSubs.length > maxShow) {
|
|
332
|
-
items.push(`... and ${activeSubs.length - maxShow} more`);
|
|
333
|
-
}
|
|
334
|
-
sections.push({
|
|
335
|
-
id: "sub_goals",
|
|
336
|
-
text: section("Active Sub-Goals", items.join("\n")),
|
|
337
|
-
mandatory: false,
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
const transitionCount = diff.changed.filter((sg) => {
|
|
342
|
-
const prev = baseline.subGoals.find(([id]) => id === sg.id);
|
|
343
|
-
return prev !== undefined && prev[1] !== sg.status;
|
|
344
|
-
}).length;
|
|
345
|
-
const items = diff.changed.slice(0, 5).map((sg) => {
|
|
346
|
-
const icon = sg.status === "in_progress" ? "🔄" : "⏳";
|
|
347
|
-
return `${icon} [\`${sg.id}\`] ${sg.description}`;
|
|
348
|
-
});
|
|
349
|
-
items.push(`- … ${diff.unchangedCount} unchanged sub-goals, ${transitionCount + diff.removedCount} changed since R${baseline.round} (see state file)`);
|
|
350
|
-
sections.push({
|
|
351
|
-
id: "sub_goals",
|
|
352
|
-
text: section("Active Sub-Goals", items.join("\n")),
|
|
353
|
-
mandatory: false,
|
|
354
|
-
});
|
|
274
|
+
const iconOf = (sg) => sg.status === "in_progress" ? "🔄" : sg.status === "blocked" ? "🚫" : "⏳";
|
|
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`);
|
|
355
279
|
}
|
|
280
|
+
sections.push({
|
|
281
|
+
id: "sub_goals",
|
|
282
|
+
text: section("Active Sub-Goals", items.join("\n")),
|
|
283
|
+
mandatory: false,
|
|
284
|
+
});
|
|
356
285
|
}
|
|
357
286
|
if (state.discoveries.length > 0) {
|
|
358
287
|
sections.push({
|
|
@@ -361,32 +290,14 @@ function l1Sections(state, baseline, emphasized) {
|
|
|
361
290
|
mandatory: false,
|
|
362
291
|
});
|
|
363
292
|
}
|
|
364
|
-
// v3.2: Recent Rounds
|
|
365
|
-
//
|
|
293
|
+
// v3.2: Recent Rounds. v3.8.1: rendered in full — the keep-newest-N fold and
|
|
294
|
+
// its milestone-boundary exception are gone.
|
|
366
295
|
if (state.rollingOutcomes.length > 0) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const earlier = state.rollingOutcomes.length - kept.length;
|
|
373
|
-
const lines = [
|
|
374
|
-
...kept,
|
|
375
|
-
`- … ${earlier} earlier round${earlier === 1 ? "" : "s"} (see state file)`,
|
|
376
|
-
];
|
|
377
|
-
sections.push({
|
|
378
|
-
id: "rolling_outcomes",
|
|
379
|
-
text: section("Recent Rounds", lines.join("\n")),
|
|
380
|
-
mandatory: false,
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
sections.push({
|
|
385
|
-
id: "rolling_outcomes",
|
|
386
|
-
text: section("Recent Rounds", bullets(state.rollingOutcomes)),
|
|
387
|
-
mandatory: false,
|
|
388
|
-
});
|
|
389
|
-
}
|
|
296
|
+
sections.push({
|
|
297
|
+
id: "rolling_outcomes",
|
|
298
|
+
text: section("Recent Rounds", bullets(state.rollingOutcomes)),
|
|
299
|
+
mandatory: false,
|
|
300
|
+
});
|
|
390
301
|
}
|
|
391
302
|
// v3.2: L1 also renders the explicit external context (previously L2-only).
|
|
392
303
|
// Placed after the changes delta, within the soft budget.
|
|
@@ -397,36 +308,26 @@ function l1Sections(state, baseline, emphasized) {
|
|
|
397
308
|
mandatory: false,
|
|
398
309
|
});
|
|
399
310
|
}
|
|
400
|
-
// v3.
|
|
401
|
-
const
|
|
402
|
-
if (
|
|
403
|
-
sections.push({
|
|
404
|
-
id: "roadmap",
|
|
405
|
-
text: section("Roadmap", roadmapLines.join("\n")),
|
|
406
|
-
mandatory: false,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
// v3.2: Recurring Issues carries the deterministic violation lessons (with
|
|
410
|
-
// counts and rounds) when available; falls back to the rolling window text.
|
|
411
|
-
if (state.recurringIssues.length > 0 || state.lessons.length > 0) {
|
|
412
|
-
const violationLessons = state.lessons
|
|
413
|
-
.filter((lesson) => lesson.kind === "constraint_violation")
|
|
414
|
-
.slice(0, 3);
|
|
415
|
-
const body = violationLessons.length > 0
|
|
416
|
-
? violationLessons
|
|
417
|
-
.map((lesson) => `- ${lesson.text} (violated ${lesson.count}×: R${lesson.rounds.join(", R")})`)
|
|
418
|
-
.join("\n")
|
|
419
|
-
: bullets(state.recurringIssues);
|
|
311
|
+
// v3.8.1: one compact phase line, not the Roadmap prose.
|
|
312
|
+
const l1Phase = buildPhaseLine(state);
|
|
313
|
+
if (l1Phase) {
|
|
420
314
|
sections.push({
|
|
421
|
-
id: "
|
|
422
|
-
text: section("
|
|
315
|
+
id: "phase",
|
|
316
|
+
text: section("Phase", l1Phase),
|
|
423
317
|
mandatory: false,
|
|
424
318
|
});
|
|
425
319
|
}
|
|
426
|
-
|
|
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) {
|
|
427
328
|
sections.push({
|
|
428
|
-
id: "
|
|
429
|
-
text: section("
|
|
329
|
+
id: "active_warnings",
|
|
330
|
+
text: section("Active Warnings", warnings),
|
|
430
331
|
mandatory: false,
|
|
431
332
|
});
|
|
432
333
|
}
|
|
@@ -452,7 +353,6 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
452
353
|
["blockers", "Blockers", state.blockers, null],
|
|
453
354
|
["discoveries", "Discoveries", state.discoveries, null],
|
|
454
355
|
["rolling_outcomes", "Cross-Round Outcomes", state.rollingOutcomes, null],
|
|
455
|
-
["recurring_issues", "Recurring Issues", state.recurringIssues, null],
|
|
456
356
|
["failed_patterns", "Failed Patterns", state.failedPatterns, null],
|
|
457
357
|
];
|
|
458
358
|
for (const [id, title, values, idPrefix] of groups) {
|
|
@@ -470,32 +370,26 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
470
370
|
: bullets(values);
|
|
471
371
|
sections.push({ id, text: section(title, body), mandatory: false });
|
|
472
372
|
}
|
|
473
|
-
// v3.
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
: lesson.kind === "verification_error" ? "⚠️" : "ℹ️";
|
|
480
|
-
return `- ${icon} ${lesson.text} — ${lesson.count}× (R${lesson.rounds.join(", R")})`;
|
|
481
|
-
});
|
|
482
|
-
if (state.lessons.length > 8) {
|
|
483
|
-
lessonLines.push(`- ... and ${state.lessons.length - 8} more`);
|
|
484
|
-
}
|
|
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) {
|
|
485
379
|
sections.push({
|
|
486
|
-
id: "
|
|
487
|
-
text: section("
|
|
380
|
+
id: "active_warnings",
|
|
381
|
+
text: section("Active Warnings", warnings),
|
|
488
382
|
mandatory: false,
|
|
489
383
|
});
|
|
490
384
|
}
|
|
491
|
-
// v3.
|
|
492
|
-
// looking) Phase History so the agent sees where it is
|
|
493
|
-
//
|
|
494
|
-
const
|
|
495
|
-
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) {
|
|
496
390
|
sections.push({
|
|
497
|
-
id: "
|
|
498
|
-
text: section("
|
|
391
|
+
id: "phase",
|
|
392
|
+
text: section("Phase", l2Phase),
|
|
499
393
|
mandatory: false,
|
|
500
394
|
});
|
|
501
395
|
}
|
|
@@ -519,56 +413,33 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
519
413
|
});
|
|
520
414
|
}
|
|
521
415
|
if (state.subGoals.length > 0) {
|
|
416
|
+
// v3.7.1: active-only rows ordered blocked → in_progress → pending
|
|
417
|
+
// (priority asc, recently changed first), capped by policy; done and
|
|
418
|
+
// canceled appear only in the stats line.
|
|
419
|
+
const cap = getPolicy().evolution.max_active_subgoals;
|
|
420
|
+
const view = activeSubGoalView(state.subGoals, cap);
|
|
522
421
|
const lines = [];
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
if (pending.length > 10) {
|
|
537
|
-
lines.push(`... and ${pending.length - 10} more pending`);
|
|
538
|
-
}
|
|
539
|
-
// Blocked
|
|
540
|
-
const blocked = state.subGoals.filter((sg) => sg.status === "blocked");
|
|
541
|
-
for (const sg of blocked) {
|
|
542
|
-
lines.push(`🚫 [\`${sg.id}\`] ${sg.description} (blocked since R${sg.status_changed_at_round})`);
|
|
543
|
-
}
|
|
544
|
-
// Recently done
|
|
545
|
-
const done = state.subGoals.filter((sg) => sg.status === "done");
|
|
546
|
-
const recentDone = done.slice(0, 5);
|
|
547
|
-
for (const sg of recentDone) {
|
|
548
|
-
lines.push(`✅ [\`${sg.id}\`] ${sg.description} (done R${sg.completed_at_round ?? sg.status_changed_at_round})`);
|
|
549
|
-
}
|
|
550
|
-
if (done.length > 5) {
|
|
551
|
-
lines.push(`... and ${done.length - 5} more completed`);
|
|
422
|
+
for (const sg of view.active) {
|
|
423
|
+
if (sg.status === "in_progress") {
|
|
424
|
+
lines.push(`🔄 [\`${sg.id}\`] ${sg.description} (since R${sg.status_changed_at_round})`);
|
|
425
|
+
}
|
|
426
|
+
else if (sg.status === "blocked") {
|
|
427
|
+
lines.push(`🚫 [\`${sg.id}\`] ${sg.description} (blocked since R${sg.status_changed_at_round})`);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
const age = state.round - sg.declared_at_round;
|
|
431
|
+
const stale = age >= 10 ? " ⚠️ stale" : "";
|
|
432
|
+
lines.push(`⏳ [\`${sg.id}\`] ${sg.description} (pending ${age} rounds)${stale}`);
|
|
433
|
+
}
|
|
552
434
|
}
|
|
553
|
-
// Stats line
|
|
554
|
-
|
|
555
|
-
const doneCount = done.length;
|
|
556
|
-
const canceledCount = state.subGoals.filter((sg) => sg.status === "canceled").length;
|
|
557
|
-
const activeCount = inProgress.length + pending.length + blocked.length;
|
|
558
|
-
lines.push(`─── ${total} total: ${activeCount} active, ${doneCount} done, ${canceledCount} canceled`);
|
|
435
|
+
// Stats line (full counts — rows may be capped)
|
|
436
|
+
lines.push(`─── ${view.total} total: ${view.activeTotal} active, ${view.done} done, ${view.canceled} canceled`);
|
|
559
437
|
sections.push({
|
|
560
438
|
id: "sub_goals_full",
|
|
561
439
|
text: section("Sub-Goal Dashboard", lines.join("\n")),
|
|
562
440
|
mandatory: false,
|
|
563
441
|
});
|
|
564
442
|
}
|
|
565
|
-
if (state.nextAction) {
|
|
566
|
-
sections.push({
|
|
567
|
-
id: "next_action",
|
|
568
|
-
text: section("Next Action", state.nextAction),
|
|
569
|
-
mandatory: false,
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
443
|
if (state.externalContext) {
|
|
573
444
|
sections.push({
|
|
574
445
|
id: "external_context",
|
|
@@ -577,97 +448,13 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
577
448
|
});
|
|
578
449
|
}
|
|
579
450
|
// ── v2.8: Path B gap fills (previously only in full state markdown) ──
|
|
580
|
-
// Progress Dashboard
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const lines = [];
|
|
588
|
-
const total = state.progress.criteriaMet.length + state.progress.criteriaRemaining.length;
|
|
589
|
-
if (total > 0) {
|
|
590
|
-
lines.push(`**Criteria**: ${state.progress.criteriaMet.length}/${total} met`);
|
|
591
|
-
}
|
|
592
|
-
if (state.progress.estimate !== null) {
|
|
593
|
-
lines.push(`**Estimated Completion**: ${(state.progress.estimate * 100).toFixed(0)}%`);
|
|
594
|
-
// v3.3: honest labeling — the estimate is the agent's own number.
|
|
595
|
-
lines.push("**Signal source**: self-reported estimate (unverified until machine-backed)");
|
|
596
|
-
}
|
|
597
|
-
// v3.3: machine side of the comparison — git motion over committed
|
|
598
|
-
// rounds (v3.6: the object always carries definite values — no
|
|
599
|
-
// "unavailable" arm).
|
|
600
|
-
if (state.machineStatus) {
|
|
601
|
-
const ms = state.machineStatus;
|
|
602
|
-
const motion = ms.gitMotion
|
|
603
|
-
? `changes in ${ms.motionRounds}/${ms.windowRounds} recent committed rounds`
|
|
604
|
-
: `no git changes in the last ${ms.windowRounds} committed rounds`;
|
|
605
|
-
lines.push(`**Machine (git)**: ${motion}`);
|
|
606
|
-
}
|
|
607
|
-
if (state.criterionStatuses.length > 0) {
|
|
608
|
-
const metCount = state.criterionStatuses.filter((cs) => cs.status === "met").length;
|
|
609
|
-
lines.push(`**Machine (criteria)**: ${metCount}/${state.criterionStatuses.length} met across committed rounds`);
|
|
610
|
-
}
|
|
611
|
-
if (state.progress.tests) {
|
|
612
|
-
lines.push(`**Tests**: ${state.progress.tests.passed} passed, ` +
|
|
613
|
-
`${state.progress.tests.failed} failed, ${state.progress.tests.skipped} skipped`);
|
|
614
|
-
}
|
|
615
|
-
if (state.progress.filesChanged.length > 0) {
|
|
616
|
-
lines.push("**Files Changed**:");
|
|
617
|
-
for (const f of state.progress.filesChanged.slice(0, 10))
|
|
618
|
-
lines.push(`- ${f}`);
|
|
619
|
-
if (state.progress.filesChanged.length > 10) {
|
|
620
|
-
lines.push(`- ... and ${state.progress.filesChanged.length - 10} more`);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
// v3.2: Goal → criteria → evidence vertical view — per-criterion status
|
|
624
|
-
// with the round it was met and linked sub-goals. IDs follow
|
|
625
|
-
// constraint_id_enabled like every other ID-rendering path.
|
|
626
|
-
if (state.criterionStatuses.length > 0) {
|
|
627
|
-
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
628
|
-
lines.push("", "**Goal → Criteria**:");
|
|
629
|
-
for (const cs of state.criterionStatuses) {
|
|
630
|
-
const icon = cs.status === "met" ? "✅" : cs.status === "remaining" ? "⬜" : "❔";
|
|
631
|
-
const idTag = idEnabled ? ` [\`${cs.id}\`]` : "";
|
|
632
|
-
const met = cs.met_at_round !== undefined ? `(met R${cs.met_at_round})` : "";
|
|
633
|
-
const related = cs.related_subgoal_ids.length > 0
|
|
634
|
-
? ` [↔ ${cs.related_subgoal_ids.join(", ")}]`
|
|
635
|
-
: "";
|
|
636
|
-
lines.push(`- ${icon}${idTag} ${cs.text} ${met}${related}`);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
sections.push({
|
|
640
|
-
id: "progress",
|
|
641
|
-
text: section("Progress Dashboard", lines.join("\n")),
|
|
642
|
-
mandatory: false,
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
// v3.3: Per-round statistics — lets the agent calibrate round granularity
|
|
646
|
-
// (files per round, rejected attempts, self-reported progress deltas).
|
|
647
|
-
// v3.6: source-labeled — files/Δ are agent-reported; only rejected
|
|
648
|
-
// attempts (and the Machine (git) row above) are machine-recorded.
|
|
649
|
-
if (state.roundStats && state.roundStats.length > 0) {
|
|
650
|
-
const statLines = state.roundStats.map((stat) => {
|
|
651
|
-
const parts = [];
|
|
652
|
-
if (stat.filesChangedCount !== null) {
|
|
653
|
-
parts.push(`${stat.filesChangedCount} file${stat.filesChangedCount === 1 ? "" : "s"}`);
|
|
654
|
-
}
|
|
655
|
-
if (stat.rejectedAttempts !== null && stat.rejectedAttempts > 0) {
|
|
656
|
-
parts.push(`${stat.rejectedAttempts} rejected attempt${stat.rejectedAttempts === 1 ? "" : "s"}`);
|
|
657
|
-
}
|
|
658
|
-
if (stat.progressDelta !== null) {
|
|
659
|
-
parts.push(`Δ${stat.progressDelta >= 0 ? "+" : ""}${stat.progressDelta.toFixed(2)}`);
|
|
660
|
-
}
|
|
661
|
-
return `- R${stat.round}: ${parts.length > 0 ? parts.join(", ") : "no data"}`;
|
|
662
|
-
});
|
|
663
|
-
statLines.push("> files & Δ are agent-reported; rejected attempts are machine-recorded " +
|
|
664
|
-
"(the Machine (git) row above is machine-observed)");
|
|
665
|
-
sections.push({
|
|
666
|
-
id: "round_stats",
|
|
667
|
-
text: section("Round Stats", statLines.join("\n")),
|
|
668
|
-
mandatory: false,
|
|
669
|
-
});
|
|
670
|
-
}
|
|
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.
|
|
671
458
|
// Retired Constraints
|
|
672
459
|
if (state.retiredConstraints.length > 0) {
|
|
673
460
|
const lines = state.retiredConstraints.map((c) => `- ~${c}~ (retired)`);
|
|
@@ -677,86 +464,166 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
677
464
|
mandatory: false,
|
|
678
465
|
});
|
|
679
466
|
}
|
|
680
|
-
// Agent Trust Score + Trend
|
|
681
|
-
if (state.agentTrustScore !== undefined) {
|
|
682
|
-
const lines = [
|
|
683
|
-
trustBarLine(state.agentTrustScore),
|
|
684
|
-
];
|
|
685
|
-
if (state.agentTrustTrend.length > 0) {
|
|
686
|
-
const avg = (state.agentTrustTrend.reduce((a, b) => a + b, 0)
|
|
687
|
-
/ state.agentTrustTrend.length).toFixed(2);
|
|
688
|
-
lines.push(`Trend (last ${state.agentTrustTrend.length}): ${state.agentTrustTrend.join(" → ")}`);
|
|
689
|
-
lines.push(`Average: ${avg}`);
|
|
690
|
-
}
|
|
691
|
-
sections.push({
|
|
692
|
-
id: "agent_trust",
|
|
693
|
-
text: section("Agent Trust", lines.join("\n")),
|
|
694
|
-
mandatory: false,
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
467
|
return sections;
|
|
698
468
|
}
|
|
699
|
-
function selectSections(input
|
|
469
|
+
function selectSections(input) {
|
|
700
470
|
if (input.level === "l0")
|
|
701
471
|
return l0Sections(input.state);
|
|
702
|
-
if (input.level === "l1")
|
|
703
|
-
return l1Sections(input.state
|
|
704
|
-
}
|
|
472
|
+
if (input.level === "l1")
|
|
473
|
+
return l1Sections(input.state);
|
|
705
474
|
return l2Sections(input.state, input.fullStateMarkdown);
|
|
706
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
|
+
}
|
|
707
528
|
function renderWithinBudget(sections, fixedText, budget) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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)
|
|
714
541
|
continue;
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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();
|
|
718
583
|
}
|
|
719
584
|
}
|
|
720
|
-
return { rendered, included };
|
|
585
|
+
return { rendered, included, dropped, protectedOverflow };
|
|
721
586
|
}
|
|
722
587
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
723
588
|
// v2.9: Prompt Requests — model-expressed information needs
|
|
724
589
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
725
|
-
/** Match
|
|
726
|
-
* v2.11: ID-first
|
|
727
|
-
*
|
|
728
|
-
*
|
|
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. */
|
|
729
597
|
function matchEmphasize(targets, candidates, cap) {
|
|
730
598
|
if (targets.length === 0 || candidates.length === 0)
|
|
731
599
|
return [];
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
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);
|
|
738
605
|
if (idMatch) {
|
|
739
|
-
|
|
740
|
-
if (candidateId === t)
|
|
741
|
-
s = 1.0;
|
|
606
|
+
return `${idMatch[1]}-${deriveItemId(candidate)}` === target;
|
|
742
607
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
});
|
|
751
|
-
return scored
|
|
752
|
-
.filter((e) => e.score >= getPolicy().evolution.constraint_match_threshold)
|
|
753
|
-
.sort((a, b) => b.score - a.score)
|
|
754
|
-
.slice(0, cap)
|
|
755
|
-
.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);
|
|
756
615
|
}
|
|
757
616
|
/** Render the "Confusion Alerts" section that appears at the prompt top.
|
|
758
|
-
*
|
|
759
|
-
|
|
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) {
|
|
760
627
|
if (points.length === 0)
|
|
761
628
|
return "";
|
|
762
629
|
const lines = [
|
|
@@ -766,16 +633,6 @@ function renderConfusionAlerts(points, state, fullDensity) {
|
|
|
766
633
|
"Address each before proceeding — the state file has full context.",
|
|
767
634
|
"",
|
|
768
635
|
];
|
|
769
|
-
const sections = [
|
|
770
|
-
{ keyword: "milestone", label: "Phase History", ref: "## Phase History" },
|
|
771
|
-
{ keyword: "sub-goal", label: "Sub-Goal Dashboard", ref: "## Sub-Goal Dashboard" },
|
|
772
|
-
{ keyword: "constraint", label: "Active Constraints / Constraint Lifecycle", ref: "## Active Constraints" },
|
|
773
|
-
{ keyword: "trust", label: "Agent Trust", ref: "## Agent Trust" },
|
|
774
|
-
{ keyword: "progress", label: "Progress Dashboard", ref: "## Progress Dashboard" },
|
|
775
|
-
{ keyword: "criteria", label: "Success Criteria", ref: "## Success Criteria" },
|
|
776
|
-
{ keyword: "phase", label: "Phase History", ref: "## Phase History" },
|
|
777
|
-
{ keyword: "objective", label: "Loop Objective", ref: "## Loop Objective" },
|
|
778
|
-
];
|
|
779
636
|
// v2.14: L2 renders up to the policy cap (was: every confusion point,
|
|
780
637
|
// unbounded by max_confusion_points); L1 keeps a single alert.
|
|
781
638
|
const shown = fullDensity
|
|
@@ -783,21 +640,8 @@ function renderConfusionAlerts(points, state, fullDensity) {
|
|
|
783
640
|
: points.slice(0, 1);
|
|
784
641
|
for (const point of shown) {
|
|
785
642
|
const truncated = point.length > 200 ? point.slice(0, 197) + "…" : point;
|
|
786
|
-
// Find best-matching state section
|
|
787
|
-
let bestSection = null;
|
|
788
|
-
let bestScore = 0;
|
|
789
|
-
for (const sec of sections) {
|
|
790
|
-
const score = jaccardSimilarity(truncated.toLowerCase(), sec.keyword);
|
|
791
|
-
if (score > bestScore) {
|
|
792
|
-
bestScore = score;
|
|
793
|
-
bestSection = sec;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
const pointer = bestSection && bestScore >= getPolicy().prompt.confusion_section_threshold
|
|
797
|
-
? `→ Check **${bestSection.label}** (\`${bestSection.ref}\`) in the state file.`
|
|
798
|
-
: "→ Read the full state file for relevant context.";
|
|
799
643
|
lines.push(`- **"${truncated}"**`);
|
|
800
|
-
lines.push(
|
|
644
|
+
lines.push(" → Read the full state file for relevant context.");
|
|
801
645
|
lines.push("");
|
|
802
646
|
}
|
|
803
647
|
if (!fullDensity && points.length > 1) {
|
|
@@ -808,7 +652,7 @@ function renderConfusionAlerts(points, state, fullDensity) {
|
|
|
808
652
|
}
|
|
809
653
|
/** Render the "Critical Context" section — items the model emphasized.
|
|
810
654
|
* Pulled from active constraints, discoveries, and blocking issues. */
|
|
811
|
-
function renderCriticalContext(emphasized
|
|
655
|
+
function renderCriticalContext(emphasized) {
|
|
812
656
|
if (emphasized.length === 0)
|
|
813
657
|
return "";
|
|
814
658
|
const lines = [
|
|
@@ -829,14 +673,13 @@ export function assemblePromptArtifact(input) {
|
|
|
829
673
|
const stateHash = hashCanonicalState(input.state);
|
|
830
674
|
const attempt = Math.max(1, input.attempt ?? 1);
|
|
831
675
|
// ── State file reference + conditional read instruction ────────────
|
|
832
|
-
// L2 rounds (full rehydration), recovery
|
|
833
|
-
//
|
|
834
|
-
//
|
|
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.
|
|
835
679
|
const fullContextReasons = new Set([
|
|
836
680
|
"first_round", "plan_boundary", "checkpoint_boundary", "goal_changed",
|
|
837
681
|
"missing_previous_state", "verification_contradicted",
|
|
838
|
-
"rejection_rehydrate", "recovery_boundary",
|
|
839
|
-
"periodic_refresh",
|
|
682
|
+
"rejection_rehydrate", "recovery_boundary",
|
|
840
683
|
]);
|
|
841
684
|
const needsFullContext = input.level === "l2"
|
|
842
685
|
|| input.reasons.some((r) => fullContextReasons.has(r));
|
|
@@ -888,8 +731,8 @@ export function assemblePromptArtifact(input) {
|
|
|
888
731
|
"- `should_continue`: Set to **`false` ONLY** when the ENTIRE loop task is done.",
|
|
889
732
|
" Partial progress or completed subtasks → `true`.",
|
|
890
733
|
"- `constraint_violations`: Be honest. List every constraint you actually violated.",
|
|
891
|
-
"- `
|
|
892
|
-
"- `
|
|
734
|
+
"- `execution_report.files_changed`: List files you modified — used for verification.",
|
|
735
|
+
"- `execution_report.tests_reported`: Report actual test runner output.",
|
|
893
736
|
"- `progress_estimate`: A number 0.0–1.0 reflecting overall task completion.",
|
|
894
737
|
"",
|
|
895
738
|
"### If the Prompt Says \"REJECTED\"",
|
|
@@ -904,11 +747,10 @@ export function assemblePromptArtifact(input) {
|
|
|
904
747
|
// ── v2.9: Prompt Requests — model-expressed information needs ──────────
|
|
905
748
|
const pr = input.promptRequests;
|
|
906
749
|
const levelIsL0 = input.level === "l0";
|
|
907
|
-
const levelIsL1 = input.level === "l1";
|
|
908
750
|
const levelIsL2 = input.level === "l2";
|
|
909
751
|
// Confusion alerts: L1/L2 only, rendered at the top (before mandatory sections)
|
|
910
752
|
const confusionText = (!levelIsL0 && pr?.confusion_points?.length)
|
|
911
|
-
? renderConfusionAlerts(pr.confusion_points,
|
|
753
|
+
? renderConfusionAlerts(pr.confusion_points, levelIsL2)
|
|
912
754
|
: "";
|
|
913
755
|
// Emphasize: L1/L2 only, matched against active state items
|
|
914
756
|
const policy = getPolicy().prompt;
|
|
@@ -942,24 +784,13 @@ export function assemblePromptArtifact(input) {
|
|
|
942
784
|
changesSinceLastRound: input.state.changesSinceLastRound.filter((item) => !emphasizedSet.has(item)),
|
|
943
785
|
}
|
|
944
786
|
: input.state;
|
|
945
|
-
const sections = selectSections({ ...input, state: stateForSections }
|
|
946
|
-
const criticalContextText = renderCriticalContext(emphasized
|
|
947
|
-
// v3.
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
//
|
|
951
|
-
//
|
|
952
|
-
let presentedState;
|
|
953
|
-
if (levelIsL1) {
|
|
954
|
-
const activeSubsForSnapshot = stateForSections.subGoals.filter((sg) => sg.status === "in_progress" || sg.status === "pending");
|
|
955
|
-
presentedState = {
|
|
956
|
-
round: input.state.round,
|
|
957
|
-
constraintIds: activeNonHardConstraints(stateForSections)
|
|
958
|
-
.map((text) => constraintIdOf(text, stateForSections.constraintMetadata)),
|
|
959
|
-
subGoals: activeSubsForSnapshot.map((sg) => [sg.id, sg.status]),
|
|
960
|
-
milestoneRanges: input.state.milestones.map((m) => [m.round_range.start, m.round_range.end]),
|
|
961
|
-
};
|
|
962
|
-
}
|
|
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.
|
|
963
794
|
// Inject confusion alerts into the header area (before mandatory sections)
|
|
964
795
|
const fixedText = header + confusionText;
|
|
965
796
|
// Inject critical context as a mandatory section
|
|
@@ -974,18 +805,27 @@ export function assemblePromptArtifact(input) {
|
|
|
974
805
|
...sections,
|
|
975
806
|
];
|
|
976
807
|
}
|
|
977
|
-
const
|
|
808
|
+
const sectionBudget = budget - footer.length;
|
|
809
|
+
const selected = renderWithinBudget(augmentedSections, fixedText, sectionBudget);
|
|
978
810
|
const renderedPrompt = selected.rendered + footer;
|
|
979
811
|
const promptHash = createHash("sha256").update(renderedPrompt).digest("hex");
|
|
980
812
|
return {
|
|
981
813
|
schemaVersion: PROMPT_ARTIFACT_SCHEMA_VERSION,
|
|
982
814
|
roundId: `loop:${input.state.loopId}:round:${input.state.round}`,
|
|
815
|
+
round: input.state.round,
|
|
983
816
|
attempt,
|
|
984
817
|
level: input.level,
|
|
985
818
|
renderedPrompt,
|
|
986
819
|
promptHash,
|
|
987
820
|
stateHash,
|
|
988
|
-
|
|
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,
|
|
989
829
|
};
|
|
990
830
|
}
|
|
991
831
|
//# sourceMappingURL=prompt-assembler.js.map
|