loopforge 2.7.0 → 3.0.0
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 +136 -182
- package/dist/canonical-event-writer.d.ts +20 -0
- package/dist/canonical-event-writer.d.ts.map +1 -0
- package/dist/canonical-event-writer.js +131 -0
- package/dist/canonical-event-writer.js.map +1 -0
- package/dist/canonical-state.d.ts +20 -16
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +211 -64
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -10
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +48 -0
- package/dist/cognitive-governance.d.ts.map +1 -0
- package/dist/cognitive-governance.js +226 -0
- package/dist/cognitive-governance.js.map +1 -0
- package/dist/compiler-subgoals.d.ts +7 -0
- package/dist/compiler-subgoals.d.ts.map +1 -0
- package/dist/compiler-subgoals.js +93 -0
- package/dist/compiler-subgoals.js.map +1 -0
- package/dist/derive-id.d.ts +12 -0
- package/dist/derive-id.d.ts.map +1 -0
- package/dist/derive-id.js +31 -0
- package/dist/derive-id.js.map +1 -0
- package/dist/evidence-provider.d.ts +26 -26
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +45 -71
- package/dist/evidence-provider.js.map +1 -1
- package/dist/generate-schema.js +90 -2
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +23 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -26
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +2 -2
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +321 -315
- 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-store.d.ts +20 -58
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +232 -311
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/server.d.ts +3 -3
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +123 -19
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session-projection.d.ts +5 -0
- package/dist/mcp/session-projection.d.ts.map +1 -0
- package/dist/mcp/session-projection.js +18 -0
- package/dist/mcp/session-projection.js.map +1 -0
- package/dist/mcp/session-queue.d.ts +3 -0
- package/dist/mcp/session-queue.d.ts.map +1 -0
- package/dist/mcp/session-queue.js +18 -0
- package/dist/mcp/session-queue.js.map +1 -0
- package/dist/mcp/session-types.d.ts +72 -0
- package/dist/mcp/session-types.d.ts.map +1 -0
- package/dist/mcp/session-types.js +2 -0
- package/dist/mcp/session-types.js.map +1 -0
- package/dist/mcp/session.d.ts +65 -84
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +667 -224
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +26 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +401 -254
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +24 -0
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +174 -0
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +74 -12
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +124 -19
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +8 -4
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +533 -86
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +8 -1
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +26 -8
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +259 -173
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +17 -96
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +11 -3
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +51 -14
- package/dist/replay.js.map +1 -1
- package/dist/round-commit-store.d.ts +12 -0
- package/dist/round-commit-store.d.ts.map +1 -0
- package/dist/round-commit-store.js +13 -0
- package/dist/round-commit-store.js.map +1 -0
- package/dist/round-coordinator.d.ts +53 -22
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +140 -44
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +18 -10
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +11 -13
- package/dist/round-driver.js.map +1 -1
- package/dist/round-gate.d.ts +31 -0
- package/dist/round-gate.d.ts.map +1 -0
- package/dist/round-gate.js +259 -0
- package/dist/round-gate.js.map +1 -0
- package/dist/round-recovery.d.ts +17 -0
- package/dist/round-recovery.d.ts.map +1 -0
- package/dist/round-recovery.js +117 -0
- package/dist/round-recovery.js.map +1 -0
- package/dist/round-report.d.ts +16 -0
- package/dist/round-report.d.ts.map +1 -0
- package/dist/round-report.js +401 -0
- package/dist/round-report.js.map +1 -0
- package/dist/round-transaction.d.ts +23 -12
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +104 -91
- package/dist/round-transaction.js.map +1 -1
- package/dist/runtime-services.d.ts +59 -0
- package/dist/runtime-services.d.ts.map +1 -0
- package/dist/{engine.js → runtime-services.js} +157 -275
- package/dist/runtime-services.js.map +1 -0
- package/dist/storage.d.ts +3 -9
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +6 -17
- package/dist/storage.js.map +1 -1
- package/dist/token-utils.d.ts +12 -8
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +56 -10
- package/dist/token-utils.js.map +1 -1
- package/dist/vault-format.d.ts +4 -0
- package/dist/vault-format.d.ts.map +1 -0
- package/dist/vault-format.js +4 -0
- package/dist/vault-format.js.map +1 -0
- package/dist/vault-types.d.ts +24 -0
- package/dist/vault-types.d.ts.map +1 -0
- package/dist/vault-types.js +2 -0
- package/dist/vault-types.js.map +1 -0
- package/loop_policy.json +10 -12
- package/loopforge-policy.json +340 -0
- package/package.json +13 -7
- package/skills/perception/SKILL.md +200 -90
- package/dist/backends/interface.d.ts +0 -39
- package/dist/backends/interface.d.ts.map +0 -1
- package/dist/backends/interface.js +0 -6
- package/dist/backends/interface.js.map +0 -1
- package/dist/enforcement-gate.d.ts +0 -50
- package/dist/enforcement-gate.d.ts.map +0 -1
- package/dist/enforcement-gate.js +0 -501
- package/dist/enforcement-gate.js.map +0 -1
- package/dist/engine.d.ts +0 -59
- package/dist/engine.d.ts.map +0 -1
- package/dist/engine.js.map +0 -1
- package/dist/interop.d.ts +0 -35
- package/dist/interop.d.ts.map +0 -1
- package/dist/interop.js +0 -34
- package/dist/interop.js.map +0 -1
- package/dist/run.d.ts +0 -51
- package/dist/run.d.ts.map +0 -1
- package/dist/run.js +0 -200
- package/dist/run.js.map +0 -1
- package/dist/self-eval.d.ts +0 -23
- package/dist/self-eval.d.ts.map +0 -1
- package/dist/self-eval.js +0 -151
- package/dist/self-eval.js.map +0 -1
- package/dist/verification-gate.d.ts +0 -52
- package/dist/verification-gate.d.ts.map +0 -1
- package/dist/verification-gate.js +0 -527
- package/dist/verification-gate.js.map +0 -1
package/dist/prompt-assembler.js
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
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";
|
|
8
9
|
import { hashCanonicalState } from "./canonical-state.js";
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
import { jaccardSimilarity } from "./token-utils.js";
|
|
11
|
+
import { getPolicy } from "./policy.js";
|
|
12
|
+
export const PROMPT_ARTIFACT_FORMAT = "loopforge.prompt";
|
|
13
|
+
export const BASE_PROMPT_VERSION = "stable";
|
|
11
14
|
export const DEFAULT_PROMPT_BUDGETS = {
|
|
12
15
|
l0: 3000,
|
|
13
16
|
l1: 7000,
|
|
@@ -19,6 +22,25 @@ function section(title, body) {
|
|
|
19
22
|
function bullets(values, prefix = "- ") {
|
|
20
23
|
return values.map((value) => `${prefix}${value}`).join("\n");
|
|
21
24
|
}
|
|
25
|
+
/** v2.11: Derive a stable item ID from text content.
|
|
26
|
+
* Same hash strategy as computeGoalTextHash in loop-compiler.ts. */
|
|
27
|
+
/** v2.11: Render a bullet list with ID prefixes for constraints/criteria.
|
|
28
|
+
* Each item gets a [prefix-XXXXXXXX] tag. Items that already look like IDs
|
|
29
|
+
* are rendered as-is. When constraint_id_enabled is false, renders plain bullets. */
|
|
30
|
+
function bulletsWithIds(values, prefix) {
|
|
31
|
+
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
32
|
+
if (!idEnabled)
|
|
33
|
+
return bullets(values);
|
|
34
|
+
const idRegex = /^(c|cr|sg)-[a-f0-9]{8}$/;
|
|
35
|
+
return values.map((v) => {
|
|
36
|
+
const trimmed = v.trim();
|
|
37
|
+
if (idRegex.test(trimmed)) {
|
|
38
|
+
return `- [\`${trimmed}\`] ${trimmed}`;
|
|
39
|
+
}
|
|
40
|
+
const id = prefix + "-" + deriveItemId(trimmed);
|
|
41
|
+
return `- [\`${id}\`] ${trimmed}`;
|
|
42
|
+
}).join("\n");
|
|
43
|
+
}
|
|
22
44
|
function verificationText(state) {
|
|
23
45
|
const lines = state.verificationFlags.map((flag) => {
|
|
24
46
|
const icon = flag.severity === "error"
|
|
@@ -35,30 +57,58 @@ function activeNonHardConstraints(state) {
|
|
|
35
57
|
const hard = new Set(state.hardConstraints);
|
|
36
58
|
return state.activeConstraints.filter((value) => !hard.has(value));
|
|
37
59
|
}
|
|
38
|
-
function commonMandatorySections(state
|
|
60
|
+
function commonMandatorySections(state,
|
|
61
|
+
/** v2.11: L0 skips ID rendering to stay within tight budget. */
|
|
62
|
+
level) {
|
|
39
63
|
const sections = [
|
|
40
64
|
{
|
|
41
65
|
id: "objective",
|
|
42
66
|
text: section("Objective", state.objective),
|
|
43
67
|
mandatory: true,
|
|
44
68
|
},
|
|
45
|
-
{
|
|
46
|
-
id: "current_task",
|
|
47
|
-
text: section("Current Task", state.currentTask),
|
|
48
|
-
mandatory: true,
|
|
49
|
-
},
|
|
50
69
|
];
|
|
51
|
-
|
|
70
|
+
const phase = state.phaseHistory.at(-1);
|
|
71
|
+
if (phase) {
|
|
52
72
|
sections.push({
|
|
53
|
-
id: "
|
|
54
|
-
text: section("
|
|
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),
|
|
55
83
|
mandatory: true,
|
|
56
84
|
});
|
|
57
85
|
}
|
|
58
|
-
if (state.
|
|
86
|
+
if (state.todoProjection) {
|
|
59
87
|
sections.push({
|
|
60
|
-
id: "
|
|
61
|
-
text: section("
|
|
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
|
+
}
|
|
104
|
+
if (state.hardConstraints.length > 0) {
|
|
105
|
+
// v2.11: L0 uses plain bullets (tight budget); L1/L2 show constraint IDs
|
|
106
|
+
const body = level === "l0"
|
|
107
|
+
? bullets(state.hardConstraints)
|
|
108
|
+
: bulletsWithIds(state.hardConstraints, "c");
|
|
109
|
+
sections.push({
|
|
110
|
+
id: "hard_constraints",
|
|
111
|
+
text: section("Active Hard Constraints", body),
|
|
62
112
|
mandatory: true,
|
|
63
113
|
});
|
|
64
114
|
}
|
|
@@ -69,7 +119,7 @@ function l0Sections(state) {
|
|
|
69
119
|
...state.blockers,
|
|
70
120
|
...state.verificationFlags.map((flag) => flag.detail),
|
|
71
121
|
];
|
|
72
|
-
const sections = commonMandatorySections(state);
|
|
122
|
+
const sections = commonMandatorySections(state, "l0");
|
|
73
123
|
if (retryRequirements.length > 0) {
|
|
74
124
|
sections.push({
|
|
75
125
|
id: "retry_requirements",
|
|
@@ -87,7 +137,15 @@ function l0Sections(state) {
|
|
|
87
137
|
return sections;
|
|
88
138
|
}
|
|
89
139
|
function l1Sections(state) {
|
|
90
|
-
const sections = commonMandatorySections(state);
|
|
140
|
+
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
|
+
}
|
|
91
149
|
const active = activeNonHardConstraints(state);
|
|
92
150
|
if (state.changesSinceLastRound.length > 0) {
|
|
93
151
|
sections.push({
|
|
@@ -99,14 +157,14 @@ function l1Sections(state) {
|
|
|
99
157
|
if (active.length > 0) {
|
|
100
158
|
sections.push({
|
|
101
159
|
id: "active_constraints",
|
|
102
|
-
text: section("Active Constraints / Success Criteria",
|
|
160
|
+
text: section("Active Constraints / Success Criteria", bulletsWithIds(active, "c")),
|
|
103
161
|
mandatory: false,
|
|
104
162
|
});
|
|
105
163
|
}
|
|
106
164
|
if (state.remainingCriteria.length > 0) {
|
|
107
165
|
sections.push({
|
|
108
166
|
id: "remaining",
|
|
109
|
-
text: section("Remaining",
|
|
167
|
+
text: section("Remaining", bulletsWithIds(state.remainingCriteria, "cr")),
|
|
110
168
|
mandatory: false,
|
|
111
169
|
});
|
|
112
170
|
}
|
|
@@ -114,7 +172,14 @@ function l1Sections(state) {
|
|
|
114
172
|
sections.push({
|
|
115
173
|
id: "blockers",
|
|
116
174
|
text: section("Blockers", bullets(state.blockers)),
|
|
117
|
-
mandatory:
|
|
175
|
+
mandatory: true,
|
|
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,
|
|
118
183
|
});
|
|
119
184
|
}
|
|
120
185
|
// v2.2: Sub-Goal Dashboard (compact — L1)
|
|
@@ -123,7 +188,7 @@ function l1Sections(state) {
|
|
|
123
188
|
const maxShow = 5;
|
|
124
189
|
const items = activeSubs.slice(0, maxShow).map((sg) => {
|
|
125
190
|
const icon = sg.status === "in_progress" ? "🔄" : "⏳";
|
|
126
|
-
return `${icon} ${sg.description}`;
|
|
191
|
+
return `${icon} [\`${sg.id}\`] ${sg.description}`;
|
|
127
192
|
});
|
|
128
193
|
if (activeSubs.length > maxShow) {
|
|
129
194
|
items.push(`... and ${activeSubs.length - maxShow} more`);
|
|
@@ -155,17 +220,25 @@ function l1Sections(state) {
|
|
|
155
220
|
mandatory: false,
|
|
156
221
|
});
|
|
157
222
|
}
|
|
158
|
-
if (state.
|
|
223
|
+
if (state.suggestedNextAction) {
|
|
159
224
|
sections.push({
|
|
160
|
-
id: "
|
|
161
|
-
text: section("Next Action", state.
|
|
225
|
+
id: "agent_intent",
|
|
226
|
+
text: section("Suggested Next Action", state.suggestedNextAction),
|
|
162
227
|
mandatory: false,
|
|
163
228
|
});
|
|
164
229
|
}
|
|
165
230
|
return sections;
|
|
166
231
|
}
|
|
167
232
|
function l2Sections(state, fullStateMarkdown) {
|
|
168
|
-
const sections = commonMandatorySections(state);
|
|
233
|
+
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
|
+
}
|
|
169
242
|
const fullState = fullStateMarkdown?.trim();
|
|
170
243
|
if (fullState) {
|
|
171
244
|
sections.push({
|
|
@@ -175,41 +248,43 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
175
248
|
});
|
|
176
249
|
return sections;
|
|
177
250
|
}
|
|
251
|
+
// v2.11: Groups with an idPrefix use ID-aware rendering; others use plain bullets.
|
|
178
252
|
const groups = [
|
|
179
|
-
["success_criteria", "Success Criteria", state.successCriteria],
|
|
180
|
-
["active_constraints", "Active Constraints", activeNonHardConstraints(state)],
|
|
181
|
-
["changes", "Changes Since Last Round", state.changesSinceLastRound],
|
|
182
|
-
["remaining", "Remaining", state.remainingCriteria],
|
|
183
|
-
["blockers", "Blockers", state.blockers],
|
|
184
|
-
["
|
|
185
|
-
["
|
|
186
|
-
["
|
|
187
|
-
["
|
|
253
|
+
["success_criteria", "Success Criteria", state.successCriteria, "cr"],
|
|
254
|
+
["active_constraints", "Active Constraints", activeNonHardConstraints(state), "c"],
|
|
255
|
+
["changes", "Changes Since Last Round", state.changesSinceLastRound, null],
|
|
256
|
+
["remaining", "Remaining", state.remainingCriteria, "cr"],
|
|
257
|
+
["blockers", "Blockers", state.blockers, null],
|
|
258
|
+
["verified_facts", "Latest Verified Facts", state.handoff.verifiedFacts, "cr"],
|
|
259
|
+
["discoveries", "Discoveries", state.discoveries, null],
|
|
260
|
+
["rolling_outcomes", "Cross-Round Outcomes", state.rollingOutcomes, null],
|
|
261
|
+
["recurring_issues", "Recurring Issues", state.recurringIssues, null],
|
|
262
|
+
["failed_patterns", "Failed Patterns", state.failedPatterns, null],
|
|
188
263
|
];
|
|
189
|
-
for (const [id, title, values] of groups) {
|
|
264
|
+
for (const [id, title, values, idPrefix] of groups) {
|
|
190
265
|
if (values.length > 0) {
|
|
191
|
-
|
|
266
|
+
const body = idPrefix
|
|
267
|
+
? bulletsWithIds(values, idPrefix)
|
|
268
|
+
: bullets(values);
|
|
269
|
+
sections.push({
|
|
270
|
+
id,
|
|
271
|
+
text: section(title, body),
|
|
272
|
+
mandatory: id === "blockers" || id === "verified_facts",
|
|
273
|
+
});
|
|
192
274
|
}
|
|
193
275
|
}
|
|
194
|
-
if (state.
|
|
195
|
-
const
|
|
196
|
-
.map((
|
|
197
|
-
const
|
|
198
|
-
?
|
|
199
|
-
:
|
|
200
|
-
|
|
201
|
-
`**${kindIcon} ${m.label}** (Rounds ${m.round_range.start}–${m.round_range.end}, ${(m.progress_at_boundary * 100).toFixed(0)}%)`,
|
|
202
|
-
` Outcome: ${m.outcome}`,
|
|
203
|
-
];
|
|
204
|
-
if (m.resolved_constraints.length > 0) {
|
|
205
|
-
lines.push(` Resolved: ${m.resolved_constraints.slice(0, 3).join("; ")}`);
|
|
206
|
-
}
|
|
207
|
-
return lines.join("\n");
|
|
276
|
+
if (state.phaseHistory.length > 0) {
|
|
277
|
+
const phaseText = state.phaseHistory
|
|
278
|
+
.map((phase) => {
|
|
279
|
+
const range = phase.closedAtRound
|
|
280
|
+
? `R${phase.enteredAtRound}-R${phase.closedAtRound}`
|
|
281
|
+
: `R${phase.enteredAtRound}-current`;
|
|
282
|
+
return `**${phase.label}** (${range}, ${phase.status})\n Outcome: ${phase.outcome}`;
|
|
208
283
|
})
|
|
209
284
|
.join("\n\n");
|
|
210
285
|
sections.push({
|
|
211
|
-
id: "
|
|
212
|
-
text: section("Phase History",
|
|
286
|
+
id: "phase_history",
|
|
287
|
+
text: section("Phase History", phaseText),
|
|
213
288
|
mandatory: false,
|
|
214
289
|
});
|
|
215
290
|
}
|
|
@@ -218,7 +293,7 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
218
293
|
// In progress
|
|
219
294
|
const inProgress = state.subGoals.filter((sg) => sg.status === "in_progress");
|
|
220
295
|
for (const sg of inProgress) {
|
|
221
|
-
lines.push(`🔄 ${sg.description} (since R${sg.status_changed_at_round})`);
|
|
296
|
+
lines.push(`🔄 [\`${sg.id}\`] ${sg.description} (since R${sg.status_changed_at_round})`);
|
|
222
297
|
}
|
|
223
298
|
// Pending (by age)
|
|
224
299
|
const pending = state.subGoals.filter((sg) => sg.status === "pending");
|
|
@@ -226,7 +301,7 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
226
301
|
for (const sg of shownPending) {
|
|
227
302
|
const age = state.round - sg.declared_at_round;
|
|
228
303
|
const stale = age >= 10 ? " ⚠️ stale" : "";
|
|
229
|
-
lines.push(`⏳ ${sg.description} (pending ${age} rounds)${stale}`);
|
|
304
|
+
lines.push(`⏳ [\`${sg.id}\`] ${sg.description} (pending ${age} rounds)${stale}`);
|
|
230
305
|
}
|
|
231
306
|
if (pending.length > 10) {
|
|
232
307
|
lines.push(`... and ${pending.length - 10} more pending`);
|
|
@@ -234,13 +309,13 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
234
309
|
// Blocked
|
|
235
310
|
const blocked = state.subGoals.filter((sg) => sg.status === "blocked");
|
|
236
311
|
for (const sg of blocked) {
|
|
237
|
-
lines.push(`🚫 ${sg.description} (blocked since R${sg.status_changed_at_round})`);
|
|
312
|
+
lines.push(`🚫 [\`${sg.id}\`] ${sg.description} (blocked since R${sg.status_changed_at_round})`);
|
|
238
313
|
}
|
|
239
314
|
// Recently done
|
|
240
315
|
const done = state.subGoals.filter((sg) => sg.status === "done");
|
|
241
316
|
const recentDone = done.slice(0, 5);
|
|
242
317
|
for (const sg of recentDone) {
|
|
243
|
-
lines.push(`✅ ${sg.description} (done R${sg.completed_at_round ?? sg.status_changed_at_round})`);
|
|
318
|
+
lines.push(`✅ [\`${sg.id}\`] ${sg.description} (done R${sg.completed_at_round ?? sg.status_changed_at_round})`);
|
|
244
319
|
}
|
|
245
320
|
if (done.length > 5) {
|
|
246
321
|
lines.push(`... and ${done.length - 5} more completed`);
|
|
@@ -264,10 +339,10 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
264
339
|
mandatory: false,
|
|
265
340
|
});
|
|
266
341
|
}
|
|
267
|
-
if (state.
|
|
342
|
+
if (state.suggestedNextAction) {
|
|
268
343
|
sections.push({
|
|
269
|
-
id: "
|
|
270
|
-
text: section("Next Action", state.
|
|
344
|
+
id: "agent_intent",
|
|
345
|
+
text: section("Suggested Next Action", state.suggestedNextAction),
|
|
271
346
|
mandatory: false,
|
|
272
347
|
});
|
|
273
348
|
}
|
|
@@ -278,23 +353,113 @@ function l2Sections(state, fullStateMarkdown) {
|
|
|
278
353
|
mandatory: false,
|
|
279
354
|
});
|
|
280
355
|
}
|
|
356
|
+
// ── v2.8: Path B gap fills (previously only in full state markdown) ──
|
|
357
|
+
// Progress Dashboard
|
|
358
|
+
if (state.progress.estimate !== null ||
|
|
359
|
+
state.progress.criteriaMet.length > 0 ||
|
|
360
|
+
state.progress.criteriaRemaining.length > 0 ||
|
|
361
|
+
state.progress.filesChanged.length > 0 ||
|
|
362
|
+
state.progress.tests !== null) {
|
|
363
|
+
const lines = [];
|
|
364
|
+
const total = state.progress.criteriaMet.length + state.progress.criteriaRemaining.length;
|
|
365
|
+
if (total > 0) {
|
|
366
|
+
lines.push(`**Criteria**: ${state.progress.criteriaMet.length}/${total} met`);
|
|
367
|
+
}
|
|
368
|
+
if (state.progress.estimate !== null) {
|
|
369
|
+
lines.push(`**Estimated Completion**: ${(state.progress.estimate * 100).toFixed(0)}%`);
|
|
370
|
+
}
|
|
371
|
+
if (state.progress.tests) {
|
|
372
|
+
lines.push(`**Tests**: ${state.progress.tests.passed} passed, ` +
|
|
373
|
+
`${state.progress.tests.failed} failed, ${state.progress.tests.skipped} skipped`);
|
|
374
|
+
}
|
|
375
|
+
if (state.progress.filesChanged.length > 0) {
|
|
376
|
+
lines.push("**Files Changed**:");
|
|
377
|
+
for (const f of state.progress.filesChanged.slice(0, 10))
|
|
378
|
+
lines.push(`- ${f}`);
|
|
379
|
+
if (state.progress.filesChanged.length > 10) {
|
|
380
|
+
lines.push(`- ... and ${state.progress.filesChanged.length - 10} more`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
sections.push({
|
|
384
|
+
id: "progress",
|
|
385
|
+
text: section("Progress Dashboard", lines.join("\n")),
|
|
386
|
+
mandatory: false,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
// Retired Constraints
|
|
390
|
+
if (state.retiredConstraints.length > 0) {
|
|
391
|
+
const lines = state.retiredConstraints.map((c) => `- ~${c}~ (retired)`);
|
|
392
|
+
sections.push({
|
|
393
|
+
id: "retired_constraints",
|
|
394
|
+
text: section("Retired Constraints", lines.join("\n")),
|
|
395
|
+
mandatory: false,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
// Inactive Constraints (with decay metadata)
|
|
399
|
+
if (state.inactiveConstraints.length > 0) {
|
|
400
|
+
const lines = [
|
|
401
|
+
"> Demoted after prolonged inactivity. Auto-reactivated if violated again.",
|
|
402
|
+
"",
|
|
403
|
+
];
|
|
404
|
+
for (const c of state.inactiveConstraints) {
|
|
405
|
+
const meta = state.constraintMetadata.find((m) => m.text === c);
|
|
406
|
+
const age = meta
|
|
407
|
+
? state.round - (meta.last_violated_at_round || meta.discovered_at_round || 0)
|
|
408
|
+
: "?";
|
|
409
|
+
const lastV = meta?.last_violated_at_round
|
|
410
|
+
? `last violated R${meta.last_violated_at_round}`
|
|
411
|
+
: "never violated";
|
|
412
|
+
lines.push(`- ${c} (${lastV}, ${age} rounds inactive)`);
|
|
413
|
+
}
|
|
414
|
+
sections.push({
|
|
415
|
+
id: "inactive_constraints",
|
|
416
|
+
text: section("Inactive Constraints", lines.join("\n")),
|
|
417
|
+
mandatory: false,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
// Agent Trust Score + Trend
|
|
421
|
+
if (state.agentTrustScore !== undefined) {
|
|
422
|
+
const barLen = 10;
|
|
423
|
+
const filled = Math.round(state.agentTrustScore * barLen);
|
|
424
|
+
const bar = "█".repeat(filled) + "░".repeat(barLen - filled);
|
|
425
|
+
const lines = [
|
|
426
|
+
`${bar} ${(state.agentTrustScore * 100).toFixed(0)}%`,
|
|
427
|
+
];
|
|
428
|
+
if (state.agentTrustTrend.length > 0) {
|
|
429
|
+
const avg = (state.agentTrustTrend.reduce((a, b) => a + b, 0)
|
|
430
|
+
/ state.agentTrustTrend.length).toFixed(2);
|
|
431
|
+
lines.push(`Trend (last ${state.agentTrustTrend.length}): ${state.agentTrustTrend.join(" → ")}`);
|
|
432
|
+
lines.push(`Average: ${avg}`);
|
|
433
|
+
}
|
|
434
|
+
sections.push({
|
|
435
|
+
id: "agent_trust",
|
|
436
|
+
text: section("Agent Trust", lines.join("\n")),
|
|
437
|
+
mandatory: false,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
281
440
|
return sections;
|
|
282
441
|
}
|
|
283
442
|
function selectSections(input) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (input.
|
|
290
|
-
|
|
291
|
-
|
|
443
|
+
const sections = input.mode === "full"
|
|
444
|
+
? l2Sections(input.state, input.fullStateMarkdown)
|
|
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
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return sections;
|
|
292
456
|
}
|
|
293
457
|
function renderWithinBudget(sections, fixedText, budget) {
|
|
294
458
|
const mandatory = sections.filter((item) => item.mandatory);
|
|
295
459
|
const optional = sections.filter((item) => !item.mandatory);
|
|
296
460
|
let rendered = fixedText + mandatory.map((item) => item.text).join("");
|
|
297
461
|
const included = mandatory.filter((item) => item.text).map((item) => item.id);
|
|
462
|
+
const omitted = [];
|
|
298
463
|
for (const item of optional) {
|
|
299
464
|
if (!item.text)
|
|
300
465
|
continue;
|
|
@@ -302,8 +467,212 @@ function renderWithinBudget(sections, fixedText, budget) {
|
|
|
302
467
|
rendered += item.text;
|
|
303
468
|
included.push(item.id);
|
|
304
469
|
}
|
|
470
|
+
else {
|
|
471
|
+
omitted.push(item.id);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return { rendered, included, omitted };
|
|
475
|
+
}
|
|
476
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
477
|
+
// v2.9: Prompt Requests — model-expressed information needs
|
|
478
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
479
|
+
/** Match a free-text emphasis target against state items.
|
|
480
|
+
* v2.11: ID-first matching. When a target is an ID (c-/cr-/sg-XXXXXXXX),
|
|
481
|
+
* candidates that derive to the same ID get score 1.0. Falls back to
|
|
482
|
+
* Jaccard similarity otherwise. */
|
|
483
|
+
function matchEmphasize(targets, candidates, cap) {
|
|
484
|
+
if (targets.length === 0 || candidates.length === 0)
|
|
485
|
+
return [];
|
|
486
|
+
const idRegex = /^(c|cr|sg)-[a-f0-9]{8}$/;
|
|
487
|
+
const scored = candidates.map((c) => {
|
|
488
|
+
let best = 0;
|
|
489
|
+
for (const t of targets) {
|
|
490
|
+
let s = 0;
|
|
491
|
+
// Phase 1: exact ID match (v2.11)
|
|
492
|
+
const idMatch = t.match(idRegex);
|
|
493
|
+
if (idMatch) {
|
|
494
|
+
const candidateId = idMatch[1] + "-" + deriveItemId(c);
|
|
495
|
+
if (candidateId === t)
|
|
496
|
+
s = 1.0;
|
|
497
|
+
}
|
|
498
|
+
// Phase 2: Jaccard fallback
|
|
499
|
+
if (s === 0)
|
|
500
|
+
s = jaccardSimilarity(t, c);
|
|
501
|
+
if (s > best)
|
|
502
|
+
best = s;
|
|
503
|
+
}
|
|
504
|
+
return { text: c, score: best };
|
|
505
|
+
});
|
|
506
|
+
return scored
|
|
507
|
+
.filter((e) => e.score >= getPolicy().evolution.constraint_match_threshold)
|
|
508
|
+
.sort((a, b) => b.score - a.score)
|
|
509
|
+
.slice(0, cap)
|
|
510
|
+
.map((e) => e.text);
|
|
511
|
+
}
|
|
512
|
+
/** Render the "Confusion Alerts" section that appears at the prompt top.
|
|
513
|
+
* Compiler auto-matches each confusion point against state sections. */
|
|
514
|
+
function renderConfusionAlerts(points, state, expandAll) {
|
|
515
|
+
if (points.length === 0)
|
|
516
|
+
return "";
|
|
517
|
+
const lines = [
|
|
518
|
+
"## ⚠️ Confusion Alerts",
|
|
519
|
+
"",
|
|
520
|
+
"You flagged these as confusing in the previous round.",
|
|
521
|
+
"Address each before proceeding — the state file has full context.",
|
|
522
|
+
"",
|
|
523
|
+
];
|
|
524
|
+
const sections = [
|
|
525
|
+
{ keyword: "milestone", label: "Phase History", ref: "## Phase History" },
|
|
526
|
+
{ keyword: "sub-goal", label: "Sub-Goal Dashboard", ref: "## Sub-Goal Dashboard" },
|
|
527
|
+
{ keyword: "constraint", label: "Active Constraints / Constraint Lifecycle", ref: "## Active Constraints" },
|
|
528
|
+
{ keyword: "trust", label: "Agent Trust", ref: "## Agent Trust" },
|
|
529
|
+
{ keyword: "progress", label: "Progress Dashboard", ref: "## Progress Dashboard" },
|
|
530
|
+
{ keyword: "criteria", label: "Success Criteria", ref: "## Success Criteria" },
|
|
531
|
+
{ keyword: "phase", label: "Phase History", ref: "## Phase History" },
|
|
532
|
+
{ keyword: "objective", label: "Loop Objective", ref: "## Loop Objective" },
|
|
533
|
+
{ keyword: "loop", label: "Loop Summary", ref: "## Loop Summary" },
|
|
534
|
+
];
|
|
535
|
+
const shown = expandAll ? points : points.slice(0, 1);
|
|
536
|
+
for (const point of shown) {
|
|
537
|
+
const truncated = point.length > 200 ? point.slice(0, 197) + "…" : point;
|
|
538
|
+
// Find best-matching state section
|
|
539
|
+
let bestSection = null;
|
|
540
|
+
let bestScore = 0;
|
|
541
|
+
for (const sec of sections) {
|
|
542
|
+
const score = jaccardSimilarity(truncated.toLowerCase(), sec.keyword);
|
|
543
|
+
if (score > bestScore) {
|
|
544
|
+
bestScore = score;
|
|
545
|
+
bestSection = sec;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const pointer = bestSection && bestScore >= 0.15
|
|
549
|
+
? `→ Check **${bestSection.label}** (\`${bestSection.ref}\`) in the state file.`
|
|
550
|
+
: "→ Read the full state file for relevant context.";
|
|
551
|
+
lines.push(`- **"${truncated}"**`);
|
|
552
|
+
lines.push(` ${pointer}`);
|
|
553
|
+
lines.push("");
|
|
554
|
+
}
|
|
555
|
+
if (!expandAll && points.length > 1) {
|
|
556
|
+
lines.push(`*… and ${points.length - 1} more confusion point(s) — see state file.*`);
|
|
557
|
+
lines.push("");
|
|
558
|
+
}
|
|
559
|
+
return lines.join("\n");
|
|
560
|
+
}
|
|
561
|
+
/** Render the "Critical Context" section — items the model emphasized.
|
|
562
|
+
* Pulled from active constraints, discoveries, and blocking issues. */
|
|
563
|
+
function renderCriticalContext(emphasized, state) {
|
|
564
|
+
if (emphasized.length === 0)
|
|
565
|
+
return "";
|
|
566
|
+
const lines = [
|
|
567
|
+
"## 🔴 Critical Context",
|
|
568
|
+
"",
|
|
569
|
+
"You flagged these as important for this round. Prioritize them.",
|
|
570
|
+
"",
|
|
571
|
+
];
|
|
572
|
+
for (const item of emphasized) {
|
|
573
|
+
lines.push(`- ${item}`);
|
|
574
|
+
}
|
|
575
|
+
lines.push("");
|
|
576
|
+
return lines.join("\n");
|
|
577
|
+
}
|
|
578
|
+
/** Expand a specific section to L2-level detail within a L1 prompt.
|
|
579
|
+
* Returns the expanded section text, or empty string if the section
|
|
580
|
+
* has no content to expand. */
|
|
581
|
+
function renderExpandedSection(sectionName, state) {
|
|
582
|
+
switch (sectionName) {
|
|
583
|
+
case "milestones": {
|
|
584
|
+
if (state.phaseHistory.length === 0)
|
|
585
|
+
return "";
|
|
586
|
+
const text = state.phaseHistory
|
|
587
|
+
.map((phase) => {
|
|
588
|
+
const range = phase.closedAtRound
|
|
589
|
+
? `R${phase.enteredAtRound}-R${phase.closedAtRound}`
|
|
590
|
+
: `R${phase.enteredAtRound}-current`;
|
|
591
|
+
return `**${phase.label}** (${range}, ${phase.status})\n Outcome: ${phase.outcome}`;
|
|
592
|
+
})
|
|
593
|
+
.join("\n\n");
|
|
594
|
+
return section("Expanded Phase History", text);
|
|
595
|
+
}
|
|
596
|
+
case "sub_goals": {
|
|
597
|
+
if (state.subGoals.length === 0)
|
|
598
|
+
return "";
|
|
599
|
+
const lines = [];
|
|
600
|
+
for (const sg of state.subGoals) {
|
|
601
|
+
const icon = sg.status === "in_progress" ? "🔄" :
|
|
602
|
+
sg.status === "done" ? "✅" :
|
|
603
|
+
sg.status === "blocked" ? "🚫" :
|
|
604
|
+
sg.status === "canceled" ? "❌" : "⏳";
|
|
605
|
+
const stale = sg.status === "pending" && state.round - sg.declared_at_round >= 10 ? " ⚠️ stale" : "";
|
|
606
|
+
const detail = sg.status === "done" && sg.completed_at_round
|
|
607
|
+
? ` (done, R${sg.completed_at_round})`
|
|
608
|
+
: sg.status === "in_progress" ? ` (since R${sg.status_changed_at_round})`
|
|
609
|
+
: sg.status === "pending" ? ` (since R${sg.declared_at_round})`
|
|
610
|
+
: sg.status === "blocked" ? ` (blocked R${sg.status_changed_at_round})`
|
|
611
|
+
: ` (canceled R${sg.status_changed_at_round})`;
|
|
612
|
+
lines.push(`- ${icon} [\`${sg.id}\`] ${sg.description}${detail}${stale}`);
|
|
613
|
+
}
|
|
614
|
+
return section("Expanded Sub-Goal Dashboard", lines.join("\n"));
|
|
615
|
+
}
|
|
616
|
+
case "constraint_lifecycle": {
|
|
617
|
+
if (state.inactiveConstraints.length === 0 && state.retiredConstraints.length === 0)
|
|
618
|
+
return "";
|
|
619
|
+
const lines = [];
|
|
620
|
+
if (state.inactiveConstraints.length > 0) {
|
|
621
|
+
lines.push("**Inactive (auto-decayed):**");
|
|
622
|
+
for (const c of state.inactiveConstraints) {
|
|
623
|
+
const meta = state.constraintMetadata.find((m) => m.text === c);
|
|
624
|
+
const age = meta ? state.round - (meta.last_violated_at_round || meta.discovered_at_round || 0) : "?";
|
|
625
|
+
lines.push(`- ${c} (${age} rounds inactive)`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (state.retiredConstraints.length > 0) {
|
|
629
|
+
lines.push("**Retired:**");
|
|
630
|
+
for (const c of state.retiredConstraints)
|
|
631
|
+
lines.push(`- ~${c}~`);
|
|
632
|
+
}
|
|
633
|
+
return section("Constraint Lifecycle", lines.join("\n"));
|
|
634
|
+
}
|
|
635
|
+
case "agent_trust": {
|
|
636
|
+
if (state.agentTrustScore === undefined)
|
|
637
|
+
return "";
|
|
638
|
+
const barLen = 10;
|
|
639
|
+
const filled = Math.round(state.agentTrustScore * barLen);
|
|
640
|
+
const bar = "█".repeat(filled) + "░".repeat(barLen - filled);
|
|
641
|
+
const lines = [
|
|
642
|
+
`${bar} ${(state.agentTrustScore * 100).toFixed(0)}%`,
|
|
643
|
+
];
|
|
644
|
+
if (state.agentTrustTrend.length > 0) {
|
|
645
|
+
lines.push(`Trend: ${state.agentTrustTrend.join(" → ")}`);
|
|
646
|
+
}
|
|
647
|
+
return section("Agent Trust (Expanded)", lines.join("\n"));
|
|
648
|
+
}
|
|
649
|
+
case "progress": {
|
|
650
|
+
const p = state.progress;
|
|
651
|
+
if (p.estimate === null && p.criteriaMet.length === 0 && p.criteriaRemaining.length === 0)
|
|
652
|
+
return "";
|
|
653
|
+
const lines = [];
|
|
654
|
+
const total = p.criteriaMet.length + p.criteriaRemaining.length;
|
|
655
|
+
if (total > 0)
|
|
656
|
+
lines.push(`**Criteria**: ${p.criteriaMet.length}/${total} met`);
|
|
657
|
+
if (p.estimate !== null)
|
|
658
|
+
lines.push(`**Estimate**: ${(p.estimate * 100).toFixed(0)}%`);
|
|
659
|
+
if (p.tests)
|
|
660
|
+
lines.push(`**Tests**: ${p.tests.passed} passed, ${p.tests.failed} failed, ${p.tests.skipped} skipped`);
|
|
661
|
+
if (p.filesChanged.length > 0) {
|
|
662
|
+
lines.push("**Files Changed**:");
|
|
663
|
+
for (const f of p.filesChanged.slice(0, 10))
|
|
664
|
+
lines.push(`- ${f}`);
|
|
665
|
+
}
|
|
666
|
+
return section("Progress Dashboard (Expanded)", lines.join("\n"));
|
|
667
|
+
}
|
|
668
|
+
case "loop_synthesis": {
|
|
669
|
+
if (!state.loopSynthesis)
|
|
670
|
+
return "";
|
|
671
|
+
return section("Loop Summary", state.loopSynthesis);
|
|
672
|
+
}
|
|
673
|
+
default:
|
|
674
|
+
return "";
|
|
305
675
|
}
|
|
306
|
-
return { rendered, included };
|
|
307
676
|
}
|
|
308
677
|
export function assemblePromptArtifact(input) {
|
|
309
678
|
const mode = input.mode ?? "adaptive";
|
|
@@ -311,17 +680,49 @@ export function assemblePromptArtifact(input) {
|
|
|
311
680
|
const budget = budgets[input.level];
|
|
312
681
|
const stateHash = hashCanonicalState(input.state);
|
|
313
682
|
const attempt = Math.max(1, input.attempt ?? 1);
|
|
683
|
+
// ── State file reference + conditional read instruction ────────────
|
|
684
|
+
// L2 rounds (full rehydration), recovery, rejection, drift, and pointer
|
|
685
|
+
// mode all benefit from the model reading the durable state file rather
|
|
686
|
+
// than relying on potentially-compacted conversation history.
|
|
687
|
+
const fullContextReasons = new Set([
|
|
688
|
+
"first_round", "plan_boundary", "checkpoint_boundary", "goal_changed",
|
|
689
|
+
"constraint_changed",
|
|
690
|
+
"missing_previous_state", "verification_contradicted",
|
|
691
|
+
"rejection_rehydrate", "recovery_boundary", "state_drift",
|
|
692
|
+
"periodic_refresh",
|
|
693
|
+
]);
|
|
694
|
+
const needsFullContext = input.level === "l2"
|
|
695
|
+
|| input.mode === "pointer"
|
|
696
|
+
|| input.reasons.some((r) => fullContextReasons.has(r));
|
|
697
|
+
const stateFilePath = input.state.stateFilePath;
|
|
698
|
+
const stateFileRef = stateFilePath
|
|
699
|
+
? `📄 Full state: \`${stateFilePath}\`\n`
|
|
700
|
+
: "";
|
|
701
|
+
const readInstruction = needsFullContext && stateFilePath
|
|
702
|
+
? [
|
|
703
|
+
"",
|
|
704
|
+
"> ⚠️ **Read the full state file before acting.** The summary below is a",
|
|
705
|
+
"> capsule. The durable state file contains complete phase history,",
|
|
706
|
+
"> sub-goal dashboard, constraint lifecycle, and agent trust data.",
|
|
707
|
+
"> Conversation history may be incomplete after compaction or session",
|
|
708
|
+
"> resume — the state file is the source of truth.",
|
|
709
|
+
"",
|
|
710
|
+
].join("\n")
|
|
711
|
+
: "";
|
|
314
712
|
const header = [
|
|
315
713
|
`## LoopForge Round ${input.state.round}`,
|
|
316
714
|
`State: ${stateHash.slice(0, 12)} | Level: ${input.level.toUpperCase()} | Attempt: ${attempt}`,
|
|
715
|
+
stateFileRef,
|
|
716
|
+
readInstruction,
|
|
317
717
|
"",
|
|
318
718
|
].join("\n");
|
|
319
719
|
const sections = selectSections(input);
|
|
320
|
-
const pointer =
|
|
321
|
-
? section("Full State",
|
|
720
|
+
const pointer = stateFilePath
|
|
721
|
+
? section("Full State", stateFilePath)
|
|
322
722
|
: "";
|
|
323
|
-
const
|
|
324
|
-
|
|
723
|
+
const reportBlock = input.roundReportBlock ?? "";
|
|
724
|
+
const evaluation = reportBlock.trim()
|
|
725
|
+
? `${reportBlock.trim()}\n`
|
|
325
726
|
: "";
|
|
326
727
|
const howToRespond = [
|
|
327
728
|
"## How to Complete This Round",
|
|
@@ -329,41 +730,79 @@ export function assemblePromptArtifact(input) {
|
|
|
329
730
|
"1. Execute the task described above. Do NOT generate another prompt or plan — act.",
|
|
330
731
|
"2. When finished, call the MCP tool **`loopforge_next`** with these parameters:",
|
|
331
732
|
" - `sessionId`: the session ID from `loopforge_start`.",
|
|
332
|
-
" - `
|
|
733
|
+
" - `report`: a canonical `RoundReport` object (see schema below).",
|
|
333
734
|
"",
|
|
334
735
|
"### Evaluation Rules",
|
|
335
736
|
"",
|
|
336
|
-
"- `success
|
|
337
|
-
"
|
|
338
|
-
"-
|
|
339
|
-
"
|
|
340
|
-
"- `constraint_violations`: Be honest. List every constraint you actually violated.",
|
|
341
|
-
"- `execution_evidence.files_changed`: List files you modified — used for verification.",
|
|
342
|
-
"- `execution_evidence.test_results`: Report actual test runner output.",
|
|
343
|
-
"- `progress_estimate`: A number 0.0–1.0 reflecting overall task completion.",
|
|
737
|
+
"- Use `outcome=success` only when all criteria have runtime-verified evidence; use `continuation=stop` only when you are requesting no further round.",
|
|
738
|
+
"- `violations`: list every constraint actually violated.",
|
|
739
|
+
"- Put claims in `evidence.claims` and reference configured providers or commands through `evidenceRefs`; provenance is runtime-owned.",
|
|
740
|
+
"- Do not estimate progress. LoopForge derives it from verified claims and remaining criteria.",
|
|
344
741
|
"",
|
|
345
742
|
"### If the Prompt Says \"REJECTED\"",
|
|
346
743
|
"",
|
|
347
744
|
"- Re-execute the **same round**. Do NOT advance the round counter.",
|
|
348
745
|
"- Read the Required Fix section, address every issue, then submit again.",
|
|
349
|
-
"- Use `loopforge_next` with a corrected
|
|
746
|
+
"- Use `loopforge_next` with a corrected report.",
|
|
350
747
|
"",
|
|
351
748
|
evaluation,
|
|
352
749
|
].join("\n");
|
|
353
750
|
const footer = [pointer, howToRespond].join("");
|
|
354
|
-
|
|
751
|
+
// ── v2.9: Prompt Requests — model-expressed information needs ──────────
|
|
752
|
+
const pr = input.promptRequests;
|
|
753
|
+
const levelIsL0 = input.level === "l0";
|
|
754
|
+
const levelIsL1 = input.level === "l1";
|
|
755
|
+
const levelIsL2 = input.level === "l2";
|
|
756
|
+
// Confusion alerts: L1/L2 only, rendered at the top (before mandatory sections)
|
|
757
|
+
const confusionText = (!levelIsL0 && pr?.confusion_points?.length)
|
|
758
|
+
? renderConfusionAlerts(pr.confusion_points, input.state, levelIsL2)
|
|
759
|
+
: "";
|
|
760
|
+
// Emphasize: L1/L2 only, matched against active state items
|
|
761
|
+
const policy = getPolicy().prompt;
|
|
762
|
+
const emphasizeCap = levelIsL2 ? policy.max_emphasize_l2 : policy.max_emphasize_l1;
|
|
763
|
+
const emphasized = (!levelIsL0 && pr?.emphasize?.length)
|
|
764
|
+
? matchEmphasize(pr.emphasize, [
|
|
765
|
+
...input.state.activeConstraints,
|
|
766
|
+
...input.state.discoveries,
|
|
767
|
+
...input.state.blockers,
|
|
768
|
+
...input.state.changesSinceLastRound,
|
|
769
|
+
], emphasizeCap)
|
|
770
|
+
: [];
|
|
771
|
+
const criticalContextText = renderCriticalContext(emphasized, input.state);
|
|
772
|
+
// Expand: L1 only, max 1 section (L2 already expanded, L0 ignored)
|
|
773
|
+
const expandCap = levelIsL1 ? policy.max_expand_l1 : 0;
|
|
774
|
+
const expandedSection = (pr?.expand?.length && expandCap > 0)
|
|
775
|
+
? renderExpandedSection(pr.expand[0], input.state)
|
|
776
|
+
: "";
|
|
777
|
+
// Inject confusion alerts into the header area (before mandatory sections)
|
|
778
|
+
const fixedText = header + confusionText;
|
|
779
|
+
// Inject critical context as a mandatory section
|
|
780
|
+
let augmentedSections = sections;
|
|
781
|
+
if (criticalContextText) {
|
|
782
|
+
augmentedSections = [
|
|
783
|
+
{
|
|
784
|
+
id: "critical_context",
|
|
785
|
+
text: criticalContextText,
|
|
786
|
+
mandatory: true,
|
|
787
|
+
},
|
|
788
|
+
...sections,
|
|
789
|
+
];
|
|
790
|
+
}
|
|
355
791
|
const selected = mode === "pointer"
|
|
356
|
-
? renderWithinBudget(
|
|
357
|
-
: renderWithinBudget(
|
|
358
|
-
const renderedPrompt = selected.rendered + footer;
|
|
792
|
+
? renderWithinBudget(augmentedSections.filter((item) => item.mandatory), fixedText, budget)
|
|
793
|
+
: renderWithinBudget(augmentedSections, fixedText, budget - footer.length - expandedSection.length);
|
|
794
|
+
const renderedPrompt = selected.rendered + expandedSection + footer;
|
|
359
795
|
const includedSections = [
|
|
360
796
|
...selected.included,
|
|
797
|
+
...(confusionText ? ["confusion_alerts"] : []),
|
|
798
|
+
...(criticalContextText ? ["critical_context"] : []),
|
|
799
|
+
...(expandedSection ? [`expand:${pr?.expand?.[0] ?? ""}`] : []),
|
|
361
800
|
...(pointer ? ["state_pointer"] : []),
|
|
362
801
|
...(evaluation ? ["self_evaluation"] : []),
|
|
363
802
|
];
|
|
364
803
|
const promptHash = createHash("sha256").update(renderedPrompt).digest("hex");
|
|
365
804
|
return {
|
|
366
|
-
|
|
805
|
+
format: PROMPT_ARTIFACT_FORMAT,
|
|
367
806
|
roundId: `loop:${input.state.loopId}:round:${input.state.round}`,
|
|
368
807
|
attempt,
|
|
369
808
|
level: input.level,
|
|
@@ -373,9 +812,17 @@ export function assemblePromptArtifact(input) {
|
|
|
373
812
|
stateHash,
|
|
374
813
|
basePromptVersion: BASE_PROMPT_VERSION,
|
|
375
814
|
includedSections,
|
|
815
|
+
omittedSections: selected.omitted,
|
|
376
816
|
budgetChars: budget,
|
|
377
817
|
charCount: renderedPrompt.length,
|
|
378
818
|
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,
|
|
379
826
|
generatedAt: Date.now(),
|
|
380
827
|
};
|
|
381
828
|
}
|