loopforge 3.0.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -145
- package/dist/audit.d.ts +50 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +158 -0
- package/dist/audit.js.map +1 -0
- package/dist/canonical-state.d.ts +28 -20
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +82 -173
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +25 -16
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +25 -33
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +38 -138
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/enforcement-gate.d.ts +76 -0
- package/dist/enforcement-gate.d.ts.map +1 -0
- package/dist/enforcement-gate.js +1071 -0
- package/dist/enforcement-gate.js.map +1 -0
- package/dist/engine.d.ts +99 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +839 -0
- package/dist/engine.js.map +1 -0
- package/dist/evidence-claims.d.ts +50 -0
- package/dist/evidence-claims.d.ts.map +1 -0
- package/dist/evidence-claims.js +134 -0
- package/dist/evidence-claims.js.map +1 -0
- package/dist/evidence-provider.d.ts +21 -23
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +74 -34
- package/dist/evidence-provider.js.map +1 -1
- package/dist/generate-schema.js +1 -84
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +19 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -15
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +41 -3
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +632 -273
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +2 -2
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +2 -2
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts +26 -0
- package/dist/loop-projection.d.ts.map +1 -0
- package/dist/loop-projection.js +142 -0
- package/dist/loop-projection.js.map +1 -0
- package/dist/loop-store.d.ts +112 -24
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +365 -218
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +216 -0
- package/dist/mcp/round-lifecycle.d.ts.map +1 -0
- package/dist/mcp/round-lifecycle.js +926 -0
- package/dist/mcp/round-lifecycle.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +28 -42
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +53 -101
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +306 -1105
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +13 -16
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +479 -413
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +12 -17
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +95 -161
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +44 -35
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +31 -115
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +37 -6
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +389 -160
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -8
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +8 -26
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +357 -229
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +139 -15
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +5 -13
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +19 -55
- package/dist/replay.js.map +1 -1
- package/dist/round-coordinator.d.ts +20 -24
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +99 -92
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +14 -13
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +26 -14
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +15 -18
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +38 -47
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +53 -0
- package/dist/self-eval.d.ts.map +1 -0
- package/dist/self-eval.js +284 -0
- package/dist/self-eval.js.map +1 -0
- package/dist/storage.d.ts +9 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +18 -6
- package/dist/storage.js.map +1 -1
- package/dist/token-utils.d.ts +15 -3
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +24 -5
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +108 -0
- package/dist/verification-gate.d.ts.map +1 -0
- package/dist/verification-gate.js +971 -0
- package/dist/verification-gate.js.map +1 -0
- package/loop_policy.json +44 -15
- package/package.json +5 -10
- package/skills/perception/SKILL.md +182 -199
- package/dist/canonical-event-writer.d.ts +0 -20
- package/dist/canonical-event-writer.d.ts.map +0 -1
- package/dist/canonical-event-writer.js +0 -131
- package/dist/canonical-event-writer.js.map +0 -1
- package/dist/compiler-subgoals.d.ts +0 -7
- package/dist/compiler-subgoals.d.ts.map +0 -1
- package/dist/compiler-subgoals.js +0 -93
- package/dist/compiler-subgoals.js.map +0 -1
- package/dist/derive-id.d.ts +0 -12
- package/dist/derive-id.d.ts.map +0 -1
- package/dist/derive-id.js +0 -31
- package/dist/derive-id.js.map +0 -1
- package/dist/mcp/session-projection.d.ts +0 -5
- package/dist/mcp/session-projection.d.ts.map +0 -1
- package/dist/mcp/session-projection.js +0 -18
- package/dist/mcp/session-projection.js.map +0 -1
- package/dist/mcp/session-queue.d.ts +0 -3
- package/dist/mcp/session-queue.d.ts.map +0 -1
- package/dist/mcp/session-queue.js +0 -18
- package/dist/mcp/session-queue.js.map +0 -1
- package/dist/mcp/session-types.d.ts +0 -72
- package/dist/mcp/session-types.d.ts.map +0 -1
- package/dist/mcp/session-types.js +0 -2
- package/dist/mcp/session-types.js.map +0 -1
- package/dist/round-commit-store.d.ts +0 -12
- package/dist/round-commit-store.d.ts.map +0 -1
- package/dist/round-commit-store.js +0 -13
- package/dist/round-commit-store.js.map +0 -1
- package/dist/round-gate.d.ts +0 -31
- package/dist/round-gate.d.ts.map +0 -1
- package/dist/round-gate.js +0 -259
- package/dist/round-gate.js.map +0 -1
- package/dist/round-recovery.d.ts +0 -17
- package/dist/round-recovery.d.ts.map +0 -1
- package/dist/round-recovery.js +0 -117
- package/dist/round-recovery.js.map +0 -1
- package/dist/round-report.d.ts +0 -16
- package/dist/round-report.d.ts.map +0 -1
- package/dist/round-report.js +0 -401
- package/dist/round-report.js.map +0 -1
- package/dist/runtime-services.d.ts +0 -59
- package/dist/runtime-services.d.ts.map +0 -1
- package/dist/runtime-services.js +0 -460
- package/dist/runtime-services.js.map +0 -1
- package/dist/vault-format.d.ts +0 -4
- package/dist/vault-format.d.ts.map +0 -1
- package/dist/vault-format.js +0 -4
- package/dist/vault-format.js.map +0 -1
- package/dist/vault-types.d.ts +0 -24
- package/dist/vault-types.d.ts.map +0 -1
- package/dist/vault-types.js +0 -2
- package/dist/vault-types.js.map +0 -1
- package/loopforge-policy.json +0 -340
package/dist/prompt-assembler.js
CHANGED
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
* are never truncated; budgets are soft and overflow is recorded.
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
-
import { deriveItemId } from "./derive-id.js";
|
|
9
8
|
import { hashCanonicalState } from "./canonical-state.js";
|
|
10
|
-
import { jaccardSimilarity } from "./token-utils.js";
|
|
9
|
+
import { deriveItemId, STABLE_ID_RE, jaccardSimilarity } from "./token-utils.js";
|
|
11
10
|
import { getPolicy } from "./policy.js";
|
|
12
|
-
export const
|
|
13
|
-
export const BASE_PROMPT_VERSION = "
|
|
11
|
+
export const PROMPT_ARTIFACT_SCHEMA_VERSION = 1;
|
|
12
|
+
export const BASE_PROMPT_VERSION = "2.0.0";
|
|
14
13
|
export const DEFAULT_PROMPT_BUDGETS = {
|
|
15
14
|
l0: 3000,
|
|
16
15
|
l1: 7000,
|
|
@@ -31,22 +30,62 @@ function bulletsWithIds(values, prefix) {
|
|
|
31
30
|
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
32
31
|
if (!idEnabled)
|
|
33
32
|
return bullets(values);
|
|
34
|
-
const idRegex = /^(c|cr|sg)-[a-f0-9]{8}$/;
|
|
35
33
|
return values.map((v) => {
|
|
36
34
|
const trimmed = v.trim();
|
|
37
|
-
if (
|
|
35
|
+
if (STABLE_ID_RE.test(trimmed)) {
|
|
38
36
|
return `- [\`${trimmed}\`] ${trimmed}`;
|
|
39
37
|
}
|
|
40
38
|
const id = prefix + "-" + deriveItemId(trimmed);
|
|
41
39
|
return `- [\`${id}\`] ${trimmed}`;
|
|
42
40
|
}).join("\n");
|
|
43
41
|
}
|
|
44
|
-
|
|
42
|
+
/** v3.2: Short imperative fix/action per verification check, appended to the
|
|
43
|
+
* L1/L2 Verification Gate lines. Deliberately shorter than the rejection-
|
|
44
|
+
* prompt wording in enforcement-gate.ts; keys are the plain check-name
|
|
45
|
+
* strings (NOT imported from verification-gate.ts — it already imports
|
|
46
|
+
* loop-compiler, which imports this module, so importing back would cycle).
|
|
47
|
+
* Full coverage of the 19 CHECK_* constants is enforced by a test. */
|
|
48
|
+
const VERIFICATION_ACTIONS = {
|
|
49
|
+
progress_regression: "Correct progress_estimate or explain the change in output_summary.",
|
|
50
|
+
empty_change_with_passing: "Report the actual files_changed, or set success=false with the real state.",
|
|
51
|
+
success_with_remaining_criteria: "Complete the remaining criteria before claiming success, or set success=false and report what remains.",
|
|
52
|
+
success_without_verified_evidence: "Back the success claim with machine-verifiable evidence: run tests or a required verification command and report the actual output.",
|
|
53
|
+
outcome_success_contradiction: "Reconcile the declared outcome with the success flag and resubmit an honest evaluation.",
|
|
54
|
+
success_claim_conflict: "Align the success claim with your own evaluation data before resubmitting.",
|
|
55
|
+
blocked_without_blocker: "State the concrete blocker, or drop the blocked outcome.",
|
|
56
|
+
retroactive_claim_bad_round: "Correct the referenced round number, or withdraw the retroactive claim.",
|
|
57
|
+
retroactive_claim_unverified: "Provide evidence for the retroactive claim, or withdraw it.",
|
|
58
|
+
duplicate_constraint_discovery: "Update the existing constraint entry instead of duplicating it.",
|
|
59
|
+
recurring_violation: "Explain why this violation keeps recurring and try a different approach — do not repeat the failed strategy.",
|
|
60
|
+
retract_fresh_constraint: "Justify the retraction explicitly, or restore the constraint.",
|
|
61
|
+
evidence_integrity: "Correct the mismatch between your claims and the recorded evidence.",
|
|
62
|
+
required_command_failed: "Re-run the required verification command and report its actual output before claiming success.",
|
|
63
|
+
command_evidence_mismatch: "Report the true command output and reconcile the mismatch.",
|
|
64
|
+
intent_drift: "Explain the pivot in drift_clarification with concrete references, or redo the round as promised.",
|
|
65
|
+
subgoal_drift: "Reconcile the sub-goal list with your reported work before resubmitting.",
|
|
66
|
+
backtrack_workspace_not_restored: "Restore the skipped-round files and the expected git HEAD, then resubmit.",
|
|
67
|
+
criteria_claims_unverified: "Provide evidence for each claimed success criterion.",
|
|
68
|
+
};
|
|
69
|
+
/** Fallback for unmapped check names (forward-compat with future checks). */
|
|
70
|
+
const VERIFICATION_ACTION_FALLBACK = "Re-examine the flagged claim and correct your evaluation before resubmitting.";
|
|
71
|
+
/** v3.2: The actionable instruction for a verification check. Exported for
|
|
72
|
+
* the coverage test (which imports the CHECK_* constants). */
|
|
73
|
+
export function verificationActionFor(check) {
|
|
74
|
+
return VERIFICATION_ACTIONS[check] ?? VERIFICATION_ACTION_FALLBACK;
|
|
75
|
+
}
|
|
76
|
+
function verificationText(state, level) {
|
|
45
77
|
const lines = state.verificationFlags.map((flag) => {
|
|
46
78
|
const icon = flag.severity === "error"
|
|
47
79
|
? "🚫"
|
|
48
80
|
: flag.severity === "warn" ? "⚠️" : "ℹ️";
|
|
49
|
-
|
|
81
|
+
const base = `- ${icon} [${flag.check}] ${flag.detail}`;
|
|
82
|
+
// v3.2: error/warn flags get an actionable continuation line in L1/L2 —
|
|
83
|
+
// the verdict is translated into what to DO. L0 keeps the compact retry
|
|
84
|
+
// prompt byte-identical; info flags stay informational.
|
|
85
|
+
if (level === "l0" || flag.severity === "info")
|
|
86
|
+
return base;
|
|
87
|
+
const label = flag.severity === "error" ? "Fix" : "Action";
|
|
88
|
+
return `${base}\n → ${label}: ${verificationActionFor(flag.check)}`;
|
|
50
89
|
});
|
|
51
90
|
if (state.verificationFlags.some((flag) => flag.severity === "error")) {
|
|
52
91
|
lines.push("- Gate Verdict: CONTRADICTED — resolve every error before claiming success.");
|
|
@@ -57,6 +96,81 @@ function activeNonHardConstraints(state) {
|
|
|
57
96
|
const hard = new Set(state.hardConstraints);
|
|
58
97
|
return state.activeConstraints.filter((value) => !hard.has(value));
|
|
59
98
|
}
|
|
99
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
100
|
+
// v3.2: L1 diff-collapse helpers
|
|
101
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
102
|
+
/** v3.2: Collapse only when at least this many items are unchanged — a tiny
|
|
103
|
+
* unchanged set renders in full (nothing worth saving). */
|
|
104
|
+
export const COLLAPSE_MIN_UNCHANGED = 3;
|
|
105
|
+
/** v3.2: Recent-rounds items kept in full when older ones collapse. */
|
|
106
|
+
export const KEEP_RECENT_ROUNDS = 3;
|
|
107
|
+
/** v3.2: Stable ID of a constraint text as rendered by bulletsWithIds —
|
|
108
|
+
* metadata's own id when present, else the derived hash. */
|
|
109
|
+
function constraintIdOf(text, metadata) {
|
|
110
|
+
const meta = metadata.find((m) => m.text === text);
|
|
111
|
+
return meta?.id ?? `c-${deriveItemId(text)}`;
|
|
112
|
+
}
|
|
113
|
+
export function diffConstraints(baseline, activeTexts, metadata, round, emphasized) {
|
|
114
|
+
if (!baseline)
|
|
115
|
+
return { changed: activeTexts, unchangedCount: 0, removedCount: 0 };
|
|
116
|
+
const baselineIds = new Set(baseline.constraintIds);
|
|
117
|
+
const currentIds = new Set(activeTexts.map((text) => constraintIdOf(text, metadata)));
|
|
118
|
+
const changed = [];
|
|
119
|
+
let unchangedCount = 0;
|
|
120
|
+
for (const text of activeTexts) {
|
|
121
|
+
const id = constraintIdOf(text, metadata);
|
|
122
|
+
const violatedThisRound = metadata.find((m) => m.text === text)
|
|
123
|
+
?.last_violated_at_round === round;
|
|
124
|
+
if (!baselineIds.has(id) || violatedThisRound)
|
|
125
|
+
changed.push(text);
|
|
126
|
+
else
|
|
127
|
+
unchangedCount++;
|
|
128
|
+
}
|
|
129
|
+
let removedCount = 0;
|
|
130
|
+
if (emphasized.size > 0) {
|
|
131
|
+
const emphasizedIds = new Set([...emphasized].map((text) => constraintIdOf(text, metadata)));
|
|
132
|
+
for (const id of baselineIds) {
|
|
133
|
+
if (!currentIds.has(id) && !emphasizedIds.has(id))
|
|
134
|
+
removedCount++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
for (const id of baselineIds) {
|
|
139
|
+
if (!currentIds.has(id))
|
|
140
|
+
removedCount++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { changed, unchangedCount, removedCount };
|
|
144
|
+
}
|
|
145
|
+
export function diffSubGoals(baseline, activeSubs) {
|
|
146
|
+
if (!baseline)
|
|
147
|
+
return { changed: activeSubs, unchangedCount: 0, removedCount: 0 };
|
|
148
|
+
const baselineById = new Map(baseline.subGoals);
|
|
149
|
+
const currentIds = new Set(activeSubs.map((sg) => sg.id));
|
|
150
|
+
const changed = [];
|
|
151
|
+
let unchangedCount = 0;
|
|
152
|
+
for (const sg of activeSubs) {
|
|
153
|
+
const prevStatus = baselineById.get(sg.id);
|
|
154
|
+
if (prevStatus === undefined || prevStatus !== sg.status)
|
|
155
|
+
changed.push(sg);
|
|
156
|
+
else
|
|
157
|
+
unchangedCount++;
|
|
158
|
+
}
|
|
159
|
+
let removedCount = 0;
|
|
160
|
+
for (const [id] of baseline.subGoals) {
|
|
161
|
+
if (!currentIds.has(id))
|
|
162
|
+
removedCount++;
|
|
163
|
+
}
|
|
164
|
+
return { changed, unchangedCount, removedCount };
|
|
165
|
+
}
|
|
166
|
+
/** v3.2: True when a milestone boundary was crossed since the baseline —
|
|
167
|
+
* the Recent Rounds section then renders in full (a phase ended). */
|
|
168
|
+
export function milestoneBoundaryChanged(baseline, milestones) {
|
|
169
|
+
if (!baseline)
|
|
170
|
+
return false;
|
|
171
|
+
const baselineRanges = new Set(baseline.milestoneRanges.map(([s, e]) => `${s}:${e}`));
|
|
172
|
+
return milestones.some((m) => !baselineRanges.has(`${m.round_range.start}:${m.round_range.end}`));
|
|
173
|
+
}
|
|
60
174
|
function commonMandatorySections(state,
|
|
61
175
|
/** v2.11: L0 skips ID rendering to stay within tight budget. */
|
|
62
176
|
level) {
|
|
@@ -66,41 +180,12 @@ level) {
|
|
|
66
180
|
text: section("Objective", state.objective),
|
|
67
181
|
mandatory: true,
|
|
68
182
|
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
sections.push({
|
|
73
|
-
id: "current_phase",
|
|
74
|
-
text: section("Current Phase", `${phase.label} (${phase.status})\n${phase.outcome}`),
|
|
75
|
-
mandatory: false,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
const focus = state.currentFocus || state.objective;
|
|
79
|
-
if (focus.trim()) {
|
|
80
|
-
sections.push({
|
|
81
|
-
id: "current_focus",
|
|
82
|
-
text: section("Current Focus", focus),
|
|
183
|
+
{
|
|
184
|
+
id: "current_task",
|
|
185
|
+
text: section("Current Task", state.currentTask),
|
|
83
186
|
mandatory: true,
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (state.todoProjection) {
|
|
87
|
-
sections.push({
|
|
88
|
-
id: "todo_projection",
|
|
89
|
-
text: section("Active Todo Projection", `${state.todoProjection.action}\nCompletion: ${state.todoProjection.completionSignal}`),
|
|
90
|
-
mandatory: false,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
sections.push({
|
|
94
|
-
id: "verification",
|
|
95
|
-
text: section("Verification Gate", verificationText(state) || "No verification contradiction is currently open."),
|
|
96
|
-
mandatory: true,
|
|
97
|
-
});
|
|
98
|
-
if (state.activeGate) {
|
|
99
|
-
const body = state.activeGate.kind === "user"
|
|
100
|
-
? [`Owner: user`, `Question: ${state.activeGate.question}`, `Blocked scope: ${state.activeGate.blockedScope.join("; ")}`, `Safe work: ${state.activeGate.allowedWork.join("; ")}`].join("\n")
|
|
101
|
-
: [`Owner: agent`, `Problem: ${state.activeGate.problem}`, `Required evidence: ${state.activeGate.requiredEvidence.join("; ")}`, `Resolution: ${state.activeGate.suggestedResolution}`].join("\n");
|
|
102
|
-
sections.push({ id: "active_gate", text: section("Active Gate", body), mandatory: true });
|
|
103
|
-
}
|
|
187
|
+
},
|
|
188
|
+
];
|
|
104
189
|
if (state.hardConstraints.length > 0) {
|
|
105
190
|
// v2.11: L0 uses plain bullets (tight budget); L1/L2 show constraint IDs
|
|
106
191
|
const body = level === "l0"
|
|
@@ -112,6 +197,13 @@ level) {
|
|
|
112
197
|
mandatory: true,
|
|
113
198
|
});
|
|
114
199
|
}
|
|
200
|
+
if (state.verificationFlags.length > 0) {
|
|
201
|
+
sections.push({
|
|
202
|
+
id: "verification",
|
|
203
|
+
text: section("Verification Gate", verificationText(state, level)),
|
|
204
|
+
mandatory: true,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
115
207
|
return sections;
|
|
116
208
|
}
|
|
117
209
|
function l0Sections(state) {
|
|
@@ -136,17 +228,10 @@ function l0Sections(state) {
|
|
|
136
228
|
}
|
|
137
229
|
return sections;
|
|
138
230
|
}
|
|
139
|
-
function l1Sections(state) {
|
|
231
|
+
function l1Sections(state, baseline, emphasized) {
|
|
140
232
|
const sections = commonMandatorySections(state, "l1");
|
|
141
|
-
const openRegression = state.regressionObligations.filter((obligation) => obligation.status !== "satisfied");
|
|
142
|
-
if (openRegression.length > 0) {
|
|
143
|
-
sections.push({
|
|
144
|
-
id: "regression_obligations",
|
|
145
|
-
text: section("Regression Obligations", bullets(openRegression.map((item) => `[${item.status}] ${item.description}`))),
|
|
146
|
-
mandatory: true,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
233
|
const active = activeNonHardConstraints(state);
|
|
234
|
+
const collapseEnabled = getPolicy().prompt.l1_collapse_enabled;
|
|
150
235
|
if (state.changesSinceLastRound.length > 0) {
|
|
151
236
|
sections.push({
|
|
152
237
|
id: "changes",
|
|
@@ -154,12 +239,36 @@ function l1Sections(state) {
|
|
|
154
239
|
mandatory: false,
|
|
155
240
|
});
|
|
156
241
|
}
|
|
242
|
+
// v3.2: Active Constraints — collapse unchanged items against the previous
|
|
243
|
+
// round's presentation; new and violated-this-round constraints render in
|
|
244
|
+
// full. The collapse line points at the state file (regenerated every
|
|
245
|
+
// round), so no information is lost — only presentation is compacted.
|
|
157
246
|
if (active.length > 0) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
247
|
+
const diff = diffConstraints(baseline, active, state.constraintMetadata, state.round, emphasized);
|
|
248
|
+
const renderFull = !collapseEnabled || !baseline
|
|
249
|
+
|| diff.unchangedCount < COLLAPSE_MIN_UNCHANGED;
|
|
250
|
+
if (renderFull) {
|
|
251
|
+
sections.push({
|
|
252
|
+
id: "active_constraints",
|
|
253
|
+
text: section("Active Constraints / Success Criteria", bulletsWithIds(active, "c")),
|
|
254
|
+
mandatory: false,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
const lines = diff.changed.map((text) => {
|
|
259
|
+
const id = constraintIdOf(text, state.constraintMetadata);
|
|
260
|
+
const violated = state.constraintMetadata.find((m) => m.text === text)
|
|
261
|
+
?.last_violated_at_round === state.round;
|
|
262
|
+
const suffix = violated ? " (violated this round)" : " (new this round)";
|
|
263
|
+
return `- [\`${id}\`] ${text}${suffix}`;
|
|
264
|
+
});
|
|
265
|
+
lines.push(`- … ${diff.unchangedCount} unchanged constraints, ${diff.removedCount} demoted since R${baseline.round} (see state file)`);
|
|
266
|
+
sections.push({
|
|
267
|
+
id: "active_constraints",
|
|
268
|
+
text: section("Active Constraints / Success Criteria", lines.join("\n")),
|
|
269
|
+
mandatory: false,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
163
272
|
}
|
|
164
273
|
if (state.remainingCriteria.length > 0) {
|
|
165
274
|
sections.push({
|
|
@@ -172,32 +281,47 @@ function l1Sections(state) {
|
|
|
172
281
|
sections.push({
|
|
173
282
|
id: "blockers",
|
|
174
283
|
text: section("Blockers", bullets(state.blockers)),
|
|
175
|
-
mandatory:
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
if (state.handoff.verifiedFacts.length > 0) {
|
|
179
|
-
sections.push({
|
|
180
|
-
id: "verified_facts",
|
|
181
|
-
text: section("Latest Verified Facts", bulletsWithIds(state.handoff.verifiedFacts, "cr")),
|
|
182
|
-
mandatory: true,
|
|
284
|
+
mandatory: false,
|
|
183
285
|
});
|
|
184
286
|
}
|
|
185
|
-
// v2.2: Sub-Goal Dashboard (compact — L1)
|
|
287
|
+
// v2.2: Sub-Goal Dashboard (compact — L1). v3.2: unchanged sub-goals
|
|
288
|
+
// collapse to a count line; new/transitioned ones render in full.
|
|
186
289
|
const activeSubs = state.subGoals.filter((sg) => sg.status === "in_progress" || sg.status === "pending");
|
|
187
290
|
if (activeSubs.length > 0) {
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
291
|
+
const diff = diffSubGoals(baseline, activeSubs);
|
|
292
|
+
const renderFull = !collapseEnabled || !baseline
|
|
293
|
+
|| diff.unchangedCount < COLLAPSE_MIN_UNCHANGED;
|
|
294
|
+
if (renderFull) {
|
|
295
|
+
const maxShow = 5;
|
|
296
|
+
const items = activeSubs.slice(0, maxShow).map((sg) => {
|
|
297
|
+
const icon = sg.status === "in_progress" ? "🔄" : "⏳";
|
|
298
|
+
return `${icon} [\`${sg.id}\`] ${sg.description}`;
|
|
299
|
+
});
|
|
300
|
+
if (activeSubs.length > maxShow) {
|
|
301
|
+
items.push(`... and ${activeSubs.length - maxShow} more`);
|
|
302
|
+
}
|
|
303
|
+
sections.push({
|
|
304
|
+
id: "sub_goals",
|
|
305
|
+
text: section("Active Sub-Goals", items.join("\n")),
|
|
306
|
+
mandatory: false,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
const transitionCount = diff.changed.filter((sg) => {
|
|
311
|
+
const prev = baseline.subGoals.find(([id]) => id === sg.id);
|
|
312
|
+
return prev !== undefined && prev[1] !== sg.status;
|
|
313
|
+
}).length;
|
|
314
|
+
const items = diff.changed.slice(0, 5).map((sg) => {
|
|
315
|
+
const icon = sg.status === "in_progress" ? "🔄" : "⏳";
|
|
316
|
+
return `${icon} [\`${sg.id}\`] ${sg.description}`;
|
|
317
|
+
});
|
|
318
|
+
items.push(`- … ${diff.unchangedCount} unchanged sub-goals, ${transitionCount + diff.removedCount} changed since R${baseline.round} (see state file)`);
|
|
319
|
+
sections.push({
|
|
320
|
+
id: "sub_goals",
|
|
321
|
+
text: section("Active Sub-Goals", items.join("\n")),
|
|
322
|
+
mandatory: false,
|
|
323
|
+
});
|
|
195
324
|
}
|
|
196
|
-
sections.push({
|
|
197
|
-
id: "sub_goals",
|
|
198
|
-
text: section("Active Sub-Goals", items.join("\n")),
|
|
199
|
-
mandatory: false,
|
|
200
|
-
});
|
|
201
325
|
}
|
|
202
326
|
if (state.discoveries.length > 0) {
|
|
203
327
|
sections.push({
|
|
@@ -206,24 +330,63 @@ function l1Sections(state) {
|
|
|
206
330
|
mandatory: false,
|
|
207
331
|
});
|
|
208
332
|
}
|
|
333
|
+
// v3.2: Recent Rounds — keep the newest KEEP_RECENT_ROUNDS in full, fold
|
|
334
|
+
// older ones into one pointer line (unless a milestone boundary crossed).
|
|
209
335
|
if (state.rollingOutcomes.length > 0) {
|
|
336
|
+
const boundaryChanged = milestoneBoundaryChanged(baseline, state.milestones);
|
|
337
|
+
const collapseable = collapseEnabled && baseline !== null
|
|
338
|
+
&& state.rollingOutcomes.length > KEEP_RECENT_ROUNDS && !boundaryChanged;
|
|
339
|
+
if (collapseable) {
|
|
340
|
+
const kept = state.rollingOutcomes.slice(-KEEP_RECENT_ROUNDS);
|
|
341
|
+
const earlier = state.rollingOutcomes.length - kept.length;
|
|
342
|
+
const lines = [
|
|
343
|
+
...kept,
|
|
344
|
+
`- … ${earlier} earlier round${earlier === 1 ? "" : "s"} (see state file)`,
|
|
345
|
+
];
|
|
346
|
+
sections.push({
|
|
347
|
+
id: "rolling_outcomes",
|
|
348
|
+
text: section("Recent Rounds", lines.join("\n")),
|
|
349
|
+
mandatory: false,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
sections.push({
|
|
354
|
+
id: "rolling_outcomes",
|
|
355
|
+
text: section("Recent Rounds", bullets(state.rollingOutcomes)),
|
|
356
|
+
mandatory: false,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// v3.2: L1 also renders the explicit external context (previously L2-only).
|
|
361
|
+
// Placed after the changes delta, within the soft budget.
|
|
362
|
+
if (state.externalContext) {
|
|
210
363
|
sections.push({
|
|
211
|
-
id: "
|
|
212
|
-
text: section("
|
|
364
|
+
id: "external_context",
|
|
365
|
+
text: section("External Context", state.externalContext),
|
|
213
366
|
mandatory: false,
|
|
214
367
|
});
|
|
215
368
|
}
|
|
216
|
-
|
|
369
|
+
// v3.2: Recurring Issues carries the deterministic violation lessons (with
|
|
370
|
+
// counts and rounds) when available; falls back to the rolling window text.
|
|
371
|
+
if (state.recurringIssues.length > 0 || state.lessons.length > 0) {
|
|
372
|
+
const violationLessons = state.lessons
|
|
373
|
+
.filter((lesson) => lesson.kind === "constraint_violation")
|
|
374
|
+
.slice(0, 3);
|
|
375
|
+
const body = violationLessons.length > 0
|
|
376
|
+
? violationLessons
|
|
377
|
+
.map((lesson) => `- ${lesson.text} (violated ${lesson.count}×: R${lesson.rounds.join(", R")})`)
|
|
378
|
+
.join("\n")
|
|
379
|
+
: bullets(state.recurringIssues);
|
|
217
380
|
sections.push({
|
|
218
381
|
id: "recurring_issues",
|
|
219
|
-
text: section("Recurring Issues",
|
|
382
|
+
text: section("Recurring Issues", body),
|
|
220
383
|
mandatory: false,
|
|
221
384
|
});
|
|
222
385
|
}
|
|
223
|
-
if (state.
|
|
386
|
+
if (state.nextAction) {
|
|
224
387
|
sections.push({
|
|
225
|
-
id: "
|
|
226
|
-
text: section("
|
|
388
|
+
id: "next_action",
|
|
389
|
+
text: section("Next Action", state.nextAction),
|
|
227
390
|
mandatory: false,
|
|
228
391
|
});
|
|
229
392
|
}
|
|
@@ -231,14 +394,6 @@ function l1Sections(state) {
|
|
|
231
394
|
}
|
|
232
395
|
function l2Sections(state, fullStateMarkdown) {
|
|
233
396
|
const sections = commonMandatorySections(state, "l2");
|
|
234
|
-
const openRegression = state.regressionObligations.filter((obligation) => obligation.status !== "satisfied");
|
|
235
|
-
if (openRegression.length > 0) {
|
|
236
|
-
sections.push({
|
|
237
|
-
id: "regression_obligations",
|
|
238
|
-
text: section("Regression Obligations", bullets(openRegression.map((item) => `[${item.status}] ${item.description}`))),
|
|
239
|
-
mandatory: true,
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
397
|
const fullState = fullStateMarkdown?.trim();
|
|
243
398
|
if (fullState) {
|
|
244
399
|
sections.push({
|
|
@@ -255,7 +410,6 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
255
410
|
["changes", "Changes Since Last Round", state.changesSinceLastRound, null],
|
|
256
411
|
["remaining", "Remaining", state.remainingCriteria, "cr"],
|
|
257
412
|
["blockers", "Blockers", state.blockers, null],
|
|
258
|
-
["verified_facts", "Latest Verified Facts", state.handoff.verifiedFacts, "cr"],
|
|
259
413
|
["discoveries", "Discoveries", state.discoveries, null],
|
|
260
414
|
["rolling_outcomes", "Cross-Round Outcomes", state.rollingOutcomes, null],
|
|
261
415
|
["recurring_issues", "Recurring Issues", state.recurringIssues, null],
|
|
@@ -266,25 +420,46 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
266
420
|
const body = idPrefix
|
|
267
421
|
? bulletsWithIds(values, idPrefix)
|
|
268
422
|
: bullets(values);
|
|
269
|
-
sections.push({
|
|
270
|
-
id,
|
|
271
|
-
text: section(title, body),
|
|
272
|
-
mandatory: id === "blockers" || id === "verified_facts",
|
|
273
|
-
});
|
|
423
|
+
sections.push({ id, text: section(title, body), mandatory: false });
|
|
274
424
|
}
|
|
275
425
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
426
|
+
// v3.2: Lessons learned — repeated violations / verification failures
|
|
427
|
+
// across the whole loop (not just the rolling window).
|
|
428
|
+
if (state.lessons.length > 0) {
|
|
429
|
+
const lessonLines = state.lessons.slice(0, 8).map((lesson) => {
|
|
430
|
+
const icon = lesson.kind === "constraint_violation"
|
|
431
|
+
? "🚫"
|
|
432
|
+
: lesson.kind === "verification_error" ? "⚠️" : "ℹ️";
|
|
433
|
+
return `- ${icon} ${lesson.text} — ${lesson.count}× (R${lesson.rounds.join(", R")})`;
|
|
434
|
+
});
|
|
435
|
+
if (state.lessons.length > 8) {
|
|
436
|
+
lessonLines.push(`- ... and ${state.lessons.length - 8} more`);
|
|
437
|
+
}
|
|
438
|
+
sections.push({
|
|
439
|
+
id: "lessons",
|
|
440
|
+
text: section("Lessons Learned", lessonLines.join("\n")),
|
|
441
|
+
mandatory: false,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
if (state.milestones.length > 0) {
|
|
445
|
+
const milestoneText = state.milestones
|
|
446
|
+
.map((m) => {
|
|
447
|
+
const kindIcon = m.kind === "agent_declared"
|
|
448
|
+
? "🏁"
|
|
449
|
+
: m.kind === "criteria_milestone" ? "✅" : "📍";
|
|
450
|
+
const lines = [
|
|
451
|
+
`**${kindIcon} ${m.label}** (Rounds ${m.round_range.start}–${m.round_range.end}, ${(m.progress_at_boundary * 100).toFixed(0)}%)`,
|
|
452
|
+
` Outcome: ${m.outcome}`,
|
|
453
|
+
];
|
|
454
|
+
if (m.resolved_constraints.length > 0) {
|
|
455
|
+
lines.push(` Resolved: ${m.resolved_constraints.slice(0, 3).join("; ")}`);
|
|
456
|
+
}
|
|
457
|
+
return lines.join("\n");
|
|
283
458
|
})
|
|
284
459
|
.join("\n\n");
|
|
285
460
|
sections.push({
|
|
286
|
-
id: "
|
|
287
|
-
text: section("Phase History",
|
|
461
|
+
id: "milestones",
|
|
462
|
+
text: section("Phase History", milestoneText),
|
|
288
463
|
mandatory: false,
|
|
289
464
|
});
|
|
290
465
|
}
|
|
@@ -339,10 +514,10 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
339
514
|
mandatory: false,
|
|
340
515
|
});
|
|
341
516
|
}
|
|
342
|
-
if (state.
|
|
517
|
+
if (state.nextAction) {
|
|
343
518
|
sections.push({
|
|
344
|
-
id: "
|
|
345
|
-
text: section("
|
|
519
|
+
id: "next_action",
|
|
520
|
+
text: section("Next Action", state.nextAction),
|
|
346
521
|
mandatory: false,
|
|
347
522
|
});
|
|
348
523
|
}
|
|
@@ -380,6 +555,22 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
380
555
|
lines.push(`- ... and ${state.progress.filesChanged.length - 10} more`);
|
|
381
556
|
}
|
|
382
557
|
}
|
|
558
|
+
// v3.2: Goal → criteria → evidence vertical view — per-criterion status
|
|
559
|
+
// with the round it was met and linked sub-goals. IDs follow
|
|
560
|
+
// constraint_id_enabled like every other ID-rendering path.
|
|
561
|
+
if (state.criterionStatuses.length > 0) {
|
|
562
|
+
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
563
|
+
lines.push("", "**Goal → Criteria**:");
|
|
564
|
+
for (const cs of state.criterionStatuses) {
|
|
565
|
+
const icon = cs.status === "met" ? "✅" : cs.status === "remaining" ? "⬜" : "❔";
|
|
566
|
+
const idTag = idEnabled ? ` [\`${cs.id}\`]` : "";
|
|
567
|
+
const met = cs.met_at_round !== undefined ? `(met R${cs.met_at_round})` : "";
|
|
568
|
+
const related = cs.related_subgoal_ids.length > 0
|
|
569
|
+
? ` [↔ ${cs.related_subgoal_ids.join(", ")}]`
|
|
570
|
+
: "";
|
|
571
|
+
lines.push(`- ${icon}${idTag} ${cs.text} ${met}${related}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
383
574
|
sections.push({
|
|
384
575
|
id: "progress",
|
|
385
576
|
text: section("Progress Dashboard", lines.join("\n")),
|
|
@@ -439,27 +630,22 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
439
630
|
}
|
|
440
631
|
return sections;
|
|
441
632
|
}
|
|
442
|
-
function selectSections(input) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
: input.level === "l0" ? l0Sections(input.state)
|
|
446
|
-
: input.level === "l1" ? l1Sections(input.state)
|
|
447
|
-
: l2Sections(input.state, input.fullStateMarkdown);
|
|
448
|
-
if (input.agentIntent?.trim()) {
|
|
449
|
-
sections.push({
|
|
450
|
-
id: "agent_intent",
|
|
451
|
-
text: section("Agent Intent (non-authoritative)", `${input.agentIntent.trim()}\nRuntime-derived Focus, Todo and Suggested Next Action take precedence.`),
|
|
452
|
-
mandatory: false,
|
|
453
|
-
});
|
|
633
|
+
function selectSections(input, emphasized) {
|
|
634
|
+
if (input.mode === "full") {
|
|
635
|
+
return l2Sections(input.state, input.fullStateMarkdown);
|
|
454
636
|
}
|
|
455
|
-
|
|
637
|
+
if (input.level === "l0")
|
|
638
|
+
return l0Sections(input.state);
|
|
639
|
+
if (input.level === "l1") {
|
|
640
|
+
return l1Sections(input.state, input.presentedBaseline ?? null, emphasized);
|
|
641
|
+
}
|
|
642
|
+
return l2Sections(input.state, input.fullStateMarkdown);
|
|
456
643
|
}
|
|
457
644
|
function renderWithinBudget(sections, fixedText, budget) {
|
|
458
645
|
const mandatory = sections.filter((item) => item.mandatory);
|
|
459
646
|
const optional = sections.filter((item) => !item.mandatory);
|
|
460
647
|
let rendered = fixedText + mandatory.map((item) => item.text).join("");
|
|
461
648
|
const included = mandatory.filter((item) => item.text).map((item) => item.id);
|
|
462
|
-
const omitted = [];
|
|
463
649
|
for (const item of optional) {
|
|
464
650
|
if (!item.text)
|
|
465
651
|
continue;
|
|
@@ -467,11 +653,8 @@ function renderWithinBudget(sections, fixedText, budget) {
|
|
|
467
653
|
rendered += item.text;
|
|
468
654
|
included.push(item.id);
|
|
469
655
|
}
|
|
470
|
-
else {
|
|
471
|
-
omitted.push(item.id);
|
|
472
|
-
}
|
|
473
656
|
}
|
|
474
|
-
return { rendered, included
|
|
657
|
+
return { rendered, included };
|
|
475
658
|
}
|
|
476
659
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
477
660
|
// v2.9: Prompt Requests — model-expressed information needs
|
|
@@ -483,13 +666,12 @@ function renderWithinBudget(sections, fixedText, budget) {
|
|
|
483
666
|
function matchEmphasize(targets, candidates, cap) {
|
|
484
667
|
if (targets.length === 0 || candidates.length === 0)
|
|
485
668
|
return [];
|
|
486
|
-
const idRegex = /^(c|cr|sg)-[a-f0-9]{8}$/;
|
|
487
669
|
const scored = candidates.map((c) => {
|
|
488
670
|
let best = 0;
|
|
489
671
|
for (const t of targets) {
|
|
490
672
|
let s = 0;
|
|
491
673
|
// Phase 1: exact ID match (v2.11)
|
|
492
|
-
const idMatch = t.match(
|
|
674
|
+
const idMatch = t.match(STABLE_ID_RE);
|
|
493
675
|
if (idMatch) {
|
|
494
676
|
const candidateId = idMatch[1] + "-" + deriveItemId(c);
|
|
495
677
|
if (candidateId === t)
|
|
@@ -532,7 +714,11 @@ function renderConfusionAlerts(points, state, expandAll) {
|
|
|
532
714
|
{ keyword: "objective", label: "Loop Objective", ref: "## Loop Objective" },
|
|
533
715
|
{ keyword: "loop", label: "Loop Summary", ref: "## Loop Summary" },
|
|
534
716
|
];
|
|
535
|
-
|
|
717
|
+
// v2.14: L2 renders up to the policy cap (was: every confusion point,
|
|
718
|
+
// unbounded by max_confusion_points); L1 keeps a single alert.
|
|
719
|
+
const shown = expandAll
|
|
720
|
+
? points.slice(0, getPolicy().prompt.max_confusion_points)
|
|
721
|
+
: points.slice(0, 1);
|
|
536
722
|
for (const point of shown) {
|
|
537
723
|
const truncated = point.length > 200 ? point.slice(0, 197) + "…" : point;
|
|
538
724
|
// Find best-matching state section
|
|
@@ -545,7 +731,7 @@ function renderConfusionAlerts(points, state, expandAll) {
|
|
|
545
731
|
bestSection = sec;
|
|
546
732
|
}
|
|
547
733
|
}
|
|
548
|
-
const pointer = bestSection && bestScore >=
|
|
734
|
+
const pointer = bestSection && bestScore >= getPolicy().prompt.confusion_section_threshold
|
|
549
735
|
? `→ Check **${bestSection.label}** (\`${bestSection.ref}\`) in the state file.`
|
|
550
736
|
: "→ Read the full state file for relevant context.";
|
|
551
737
|
lines.push(`- **"${truncated}"**`);
|
|
@@ -581,14 +767,20 @@ function renderCriticalContext(emphasized, state) {
|
|
|
581
767
|
function renderExpandedSection(sectionName, state) {
|
|
582
768
|
switch (sectionName) {
|
|
583
769
|
case "milestones": {
|
|
584
|
-
if (state.
|
|
770
|
+
if (state.milestones.length === 0)
|
|
585
771
|
return "";
|
|
586
|
-
const text = state.
|
|
587
|
-
.map((
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
772
|
+
const text = state.milestones
|
|
773
|
+
.map((m) => {
|
|
774
|
+
const icon = m.kind === "agent_declared" ? "🏁" : m.kind === "criteria_milestone" ? "✅" : "📍";
|
|
775
|
+
const lines = [
|
|
776
|
+
`**${icon} ${m.label}** (R${m.round_range.start}–R${m.round_range.end}, ${(m.progress_at_boundary * 100).toFixed(0)}%)`,
|
|
777
|
+
` Outcome: ${m.outcome}`,
|
|
778
|
+
];
|
|
779
|
+
if (m.carried_constraints.length > 0)
|
|
780
|
+
lines.push(` Carried: ${m.carried_constraints.slice(0, 3).join("; ")}`);
|
|
781
|
+
if (m.resolved_constraints.length > 0)
|
|
782
|
+
lines.push(` Resolved: ${m.resolved_constraints.slice(0, 3).join("; ")}`);
|
|
783
|
+
return lines.join("\n");
|
|
592
784
|
})
|
|
593
785
|
.join("\n\n");
|
|
594
786
|
return section("Expanded Phase History", text);
|
|
@@ -686,7 +878,6 @@ export function assemblePromptArtifact(input) {
|
|
|
686
878
|
// than relying on potentially-compacted conversation history.
|
|
687
879
|
const fullContextReasons = new Set([
|
|
688
880
|
"first_round", "plan_boundary", "checkpoint_boundary", "goal_changed",
|
|
689
|
-
"constraint_changed",
|
|
690
881
|
"missing_previous_state", "verification_contradicted",
|
|
691
882
|
"rejection_rehydrate", "recovery_boundary", "state_drift",
|
|
692
883
|
"periodic_refresh",
|
|
@@ -695,8 +886,13 @@ export function assemblePromptArtifact(input) {
|
|
|
695
886
|
|| input.mode === "pointer"
|
|
696
887
|
|| input.reasons.some((r) => fullContextReasons.has(r));
|
|
697
888
|
const stateFilePath = input.state.stateFilePath;
|
|
889
|
+
// v3.2: L1 annotates the pointer with the round the freshly-regenerated
|
|
890
|
+
// state file describes (it is rewritten every compile); L0/L2 keep the
|
|
891
|
+
// plain pointer byte-identical.
|
|
698
892
|
const stateFileRef = stateFilePath
|
|
699
|
-
?
|
|
893
|
+
? (input.level === "l1"
|
|
894
|
+
? `📄 Full state: \`${stateFilePath}\` (updated round ${input.state.round})\n`
|
|
895
|
+
: `📄 Full state: \`${stateFilePath}\`\n`)
|
|
700
896
|
: "";
|
|
701
897
|
const readInstruction = needsFullContext && stateFilePath
|
|
702
898
|
? [
|
|
@@ -716,13 +912,11 @@ export function assemblePromptArtifact(input) {
|
|
|
716
912
|
readInstruction,
|
|
717
913
|
"",
|
|
718
914
|
].join("\n");
|
|
719
|
-
const sections = selectSections(input);
|
|
720
915
|
const pointer = stateFilePath
|
|
721
916
|
? section("Full State", stateFilePath)
|
|
722
917
|
: "";
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
? `${reportBlock.trim()}\n`
|
|
918
|
+
const evaluation = input.selfEvaluationBlock.trim()
|
|
919
|
+
? `${input.selfEvaluationBlock.trim()}\n`
|
|
726
920
|
: "";
|
|
727
921
|
const howToRespond = [
|
|
728
922
|
"## How to Complete This Round",
|
|
@@ -730,20 +924,24 @@ export function assemblePromptArtifact(input) {
|
|
|
730
924
|
"1. Execute the task described above. Do NOT generate another prompt or plan — act.",
|
|
731
925
|
"2. When finished, call the MCP tool **`loopforge_next`** with these parameters:",
|
|
732
926
|
" - `sessionId`: the session ID from `loopforge_start`.",
|
|
733
|
-
" - `
|
|
927
|
+
" - `evaluation`: a structured self-assessment object (see schema below).",
|
|
734
928
|
"",
|
|
735
929
|
"### Evaluation Rules",
|
|
736
930
|
"",
|
|
737
|
-
"-
|
|
738
|
-
"
|
|
739
|
-
"-
|
|
740
|
-
"
|
|
931
|
+
"- `success`: Set to **`true` ONLY** when ALL hard constraints are met AND",
|
|
932
|
+
" the task goal is fully achieved. If anything remains incomplete, set `false`.",
|
|
933
|
+
"- `should_continue`: Set to **`false` ONLY** when the ENTIRE loop task is done.",
|
|
934
|
+
" Partial progress or completed subtasks → `true`.",
|
|
935
|
+
"- `constraint_violations`: Be honest. List every constraint you actually violated.",
|
|
936
|
+
"- `execution_evidence.files_changed`: List files you modified — used for verification.",
|
|
937
|
+
"- `execution_evidence.test_results`: Report actual test runner output.",
|
|
938
|
+
"- `progress_estimate`: A number 0.0–1.0 reflecting overall task completion.",
|
|
741
939
|
"",
|
|
742
940
|
"### If the Prompt Says \"REJECTED\"",
|
|
743
941
|
"",
|
|
744
942
|
"- Re-execute the **same round**. Do NOT advance the round counter.",
|
|
745
943
|
"- Read the Required Fix section, address every issue, then submit again.",
|
|
746
|
-
"- Use `loopforge_next` with a corrected
|
|
944
|
+
"- Use `loopforge_next` with a corrected evaluation.",
|
|
747
945
|
"",
|
|
748
946
|
evaluation,
|
|
749
947
|
].join("\n");
|
|
@@ -768,7 +966,45 @@ export function assemblePromptArtifact(input) {
|
|
|
768
966
|
...input.state.changesSinceLastRound,
|
|
769
967
|
], emphasizeCap)
|
|
770
968
|
: [];
|
|
969
|
+
// v2.14: emphasize is a pure reorder (zero token overhead) — the matched
|
|
970
|
+
// items are MOVED to the Critical Context section, not duplicated. Filter
|
|
971
|
+
// them out of the source arrays before section rendering; unmatched
|
|
972
|
+
// emphasizes are dropped (nothing to reorder → nothing rendered).
|
|
973
|
+
// v3.2.1: hard constraints and success criteria live in BOTH their own
|
|
974
|
+
// sections (Active Hard Constraints / Success Criteria) and the active
|
|
975
|
+
// constraint list (loop-compiler merges objective.hard_constraints and
|
|
976
|
+
// success_criteria into constraints_active), so they must be filtered
|
|
977
|
+
// from BOTH sources too — otherwise the emphasized item renders twice.
|
|
978
|
+
const emphasizedSet = new Set(emphasized);
|
|
979
|
+
const stateForSections = emphasizedSet.size > 0
|
|
980
|
+
? {
|
|
981
|
+
...input.state,
|
|
982
|
+
activeConstraints: input.state.activeConstraints.filter((item) => !emphasizedSet.has(item)),
|
|
983
|
+
hardConstraints: input.state.hardConstraints.filter((item) => !emphasizedSet.has(item)),
|
|
984
|
+
successCriteria: input.state.successCriteria.filter((item) => !emphasizedSet.has(item)),
|
|
985
|
+
discoveries: input.state.discoveries.filter((item) => !emphasizedSet.has(item)),
|
|
986
|
+
blockers: input.state.blockers.filter((item) => !emphasizedSet.has(item)),
|
|
987
|
+
changesSinceLastRound: input.state.changesSinceLastRound.filter((item) => !emphasizedSet.has(item)),
|
|
988
|
+
}
|
|
989
|
+
: input.state;
|
|
990
|
+
const sections = selectSections({ ...input, state: stateForSections }, emphasizedSet);
|
|
771
991
|
const criticalContextText = renderCriticalContext(emphasized, input.state);
|
|
992
|
+
// v3.2: L1 only — snapshot what this prompt actually presented, so the
|
|
993
|
+
// NEXT L1 compile can diff against it (persisted via PromptArtifact →
|
|
994
|
+
// lineage). L0/L2 leave it undefined: their presentation semantics differ
|
|
995
|
+
// (L2's dashboard includes done/canceled sub-goals, which would
|
|
996
|
+
// contaminate the L1 active-set diff).
|
|
997
|
+
let presentedState;
|
|
998
|
+
if (levelIsL1) {
|
|
999
|
+
const activeSubsForSnapshot = stateForSections.subGoals.filter((sg) => sg.status === "in_progress" || sg.status === "pending");
|
|
1000
|
+
presentedState = {
|
|
1001
|
+
round: input.state.round,
|
|
1002
|
+
constraintIds: activeNonHardConstraints(stateForSections)
|
|
1003
|
+
.map((text) => constraintIdOf(text, stateForSections.constraintMetadata)),
|
|
1004
|
+
subGoals: activeSubsForSnapshot.map((sg) => [sg.id, sg.status]),
|
|
1005
|
+
milestoneRanges: input.state.milestones.map((m) => [m.round_range.start, m.round_range.end]),
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
772
1008
|
// Expand: L1 only, max 1 section (L2 already expanded, L0 ignored)
|
|
773
1009
|
const expandCap = levelIsL1 ? policy.max_expand_l1 : 0;
|
|
774
1010
|
const expandedSection = (pr?.expand?.length && expandCap > 0)
|
|
@@ -802,7 +1038,7 @@ export function assemblePromptArtifact(input) {
|
|
|
802
1038
|
];
|
|
803
1039
|
const promptHash = createHash("sha256").update(renderedPrompt).digest("hex");
|
|
804
1040
|
return {
|
|
805
|
-
|
|
1041
|
+
schemaVersion: PROMPT_ARTIFACT_SCHEMA_VERSION,
|
|
806
1042
|
roundId: `loop:${input.state.loopId}:round:${input.state.round}`,
|
|
807
1043
|
attempt,
|
|
808
1044
|
level: input.level,
|
|
@@ -812,18 +1048,11 @@ export function assemblePromptArtifact(input) {
|
|
|
812
1048
|
stateHash,
|
|
813
1049
|
basePromptVersion: BASE_PROMPT_VERSION,
|
|
814
1050
|
includedSections,
|
|
815
|
-
omittedSections: selected.omitted,
|
|
816
1051
|
budgetChars: budget,
|
|
817
1052
|
charCount: renderedPrompt.length,
|
|
818
1053
|
budgetExceeded: renderedPrompt.length > budget,
|
|
819
|
-
// lastL2Round / lastMeaningfulProgressRound / stateAgeRounds are NOT
|
|
820
|
-
// assembled here: the assembler has no vault history and can only guess,
|
|
821
|
-
// so the values were always immediately overwritten by the compiler's
|
|
822
|
-
// vault-derived backfill. They stay undefined at this layer.
|
|
823
|
-
phaseId: input.state.phaseHistory.at(-1)?.id,
|
|
824
|
-
focusId: input.state.todoProjection?.sourceId,
|
|
825
|
-
todoId: input.state.todoProjection?.id,
|
|
826
1054
|
generatedAt: Date.now(),
|
|
1055
|
+
...(presentedState ? { presentedState } : {}),
|
|
827
1056
|
};
|
|
828
1057
|
}
|
|
829
1058
|
//# sourceMappingURL=prompt-assembler.js.map
|