loopforge 3.7.0 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +171 -157
- package/dist/audit.d.ts +53 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +106 -22
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +75 -69
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +276 -168
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +111 -12
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +13 -0
- package/dist/cognitive-facts.d.ts.map +1 -1
- package/dist/cognitive-facts.js +30 -16
- package/dist/cognitive-facts.js.map +1 -1
- package/dist/cognitive-governance.d.ts +30 -6
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +74 -18
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +109 -24
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +235 -52
- package/dist/committed-round.js.map +1 -1
- package/dist/contract-items.d.ts +55 -0
- package/dist/contract-items.d.ts.map +1 -0
- package/dist/contract-items.js +180 -0
- package/dist/contract-items.js.map +1 -0
- package/dist/enforcement-gate.d.ts +43 -20
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +269 -378
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +11 -6
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +102 -67
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts +4 -4
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +16 -14
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +91 -90
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +349 -160
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +76 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +127 -0
- package/dist/explain.js.map +1 -0
- package/dist/generate-schema.js +250 -53
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +41 -40
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +275 -515
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +0 -1
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +0 -4
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts.map +1 -1
- package/dist/loop-projection.js +3 -1
- package/dist/loop-projection.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +40 -21
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +173 -120
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +63 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +53 -7
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +240 -139
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +89 -0
- package/dist/mcp/submission-boundary.d.ts.map +1 -0
- package/dist/mcp/submission-boundary.js +203 -0
- package/dist/mcp/submission-boundary.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +377 -125
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.js +2 -2
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +65 -95
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +151 -39
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +7 -33
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +313 -473
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -4
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +6 -7
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +411 -168
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +23 -69
- package/dist/protocol.js.map +1 -1
- package/dist/replay.js +2 -2
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +69 -57
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +104 -64
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +55 -16
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +179 -57
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +7 -5
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +2 -1
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +96 -0
- package/dist/round-facts.d.ts.map +1 -0
- package/dist/round-facts.js +216 -0
- package/dist/round-facts.js.map +1 -0
- package/dist/round-transaction.d.ts +23 -9
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +99 -22
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +127 -11
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +517 -36
- package/dist/self-eval.js.map +1 -1
- package/dist/subgoal-state.d.ts +77 -0
- package/dist/subgoal-state.d.ts.map +1 -0
- package/dist/subgoal-state.js +198 -0
- package/dist/subgoal-state.js.map +1 -0
- package/dist/token-utils.d.ts +52 -19
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +83 -72
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +27 -46
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +308 -557
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/workspace.d.ts +9 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +17 -0
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +17 -25
- package/package.json +7 -7
- package/skills/perception/SKILL.md +266 -64
package/dist/canonical-state.js
CHANGED
|
@@ -5,8 +5,32 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { deriveItemId, STABLE_ID_RE, unique } from "./token-utils.js";
|
|
8
|
+
import { claimedMetCriteria, claimedRemainingCriteria } from "./self-eval.js";
|
|
8
9
|
import { getPolicy } from "./policy.js";
|
|
10
|
+
import { deriveConfiguredCapability } from "./policy.js";
|
|
9
11
|
export const CANONICAL_STATE_SCHEMA_VERSION = 1;
|
|
12
|
+
/** v3.7.1: Presentation view of sub-goals shared by prompts and the state
|
|
13
|
+
* file (one derivation, no second copy). Only ACTIVE items render as rows
|
|
14
|
+
* — pending/in_progress/blocked — ordered blocked → in_progress → pending
|
|
15
|
+
* (priority ascending, then most recently changed first) and trimmed to
|
|
16
|
+
* `cap`. done/canceled never render as items: they stay in the vault, in
|
|
17
|
+
* replay, and in these counts. */
|
|
18
|
+
export function activeSubGoalView(subGoals, cap) {
|
|
19
|
+
const rank = (s) => s.status === "blocked" ? 0 : s.status === "in_progress" ? 1 : 2;
|
|
20
|
+
const active = subGoals
|
|
21
|
+
.filter((s) => s.status !== "done" && s.status !== "canceled")
|
|
22
|
+
.sort((a, b) => rank(a) - rank(b) ||
|
|
23
|
+
a.priority - b.priority ||
|
|
24
|
+
b.status_changed_at_round - a.status_changed_at_round ||
|
|
25
|
+
(a.id < b.id ? -1 : 1));
|
|
26
|
+
return {
|
|
27
|
+
active: active.slice(0, cap),
|
|
28
|
+
activeTotal: active.length,
|
|
29
|
+
done: subGoals.filter((s) => s.status === "done").length,
|
|
30
|
+
canceled: subGoals.filter((s) => s.status === "canceled").length,
|
|
31
|
+
total: subGoals.length,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
10
34
|
// unique() imported from token-utils.ts
|
|
11
35
|
/** Deterministic JSON serialization used by state and prompt hashes. */
|
|
12
36
|
export function stableStringify(value) {
|
|
@@ -23,6 +47,16 @@ export function stableStringify(value) {
|
|
|
23
47
|
export function hashCanonicalState(state) {
|
|
24
48
|
return createHash("sha256").update(stableStringify(state)).digest("hex");
|
|
25
49
|
}
|
|
50
|
+
/** v3.8.1: how many recurring flags the state file lists before folding the
|
|
51
|
+
* rest into a count. The prompt keeps its own, smaller cap (Active Warnings)
|
|
52
|
+
* over a different slice of the same list. */
|
|
53
|
+
const RECURRING_RENDER_CAP = 8;
|
|
54
|
+
/** v3.8.1: the state file's recurring slice — only facts that actually
|
|
55
|
+
* RECURRED (present in >= 2 rounds). The prompt shows the recent tail
|
|
56
|
+
* instead; both read the same `RecurringFlag` list. */
|
|
57
|
+
function recurringOnly(flags) {
|
|
58
|
+
return flags.filter((flag) => flag.rounds.length >= 2);
|
|
59
|
+
}
|
|
26
60
|
function addList(lines, title, values) {
|
|
27
61
|
if (values.length === 0)
|
|
28
62
|
return;
|
|
@@ -33,13 +67,16 @@ function addList(lines, title, values) {
|
|
|
33
67
|
}
|
|
34
68
|
/** v2.11: Derive a stable 8-char hex ID from text using SHA-256.
|
|
35
69
|
* Same hash strategy as computeGoalTextHash in loop-compiler.ts. */
|
|
36
|
-
/** v2.11: Render a list with ID prefixes
|
|
37
|
-
*
|
|
38
|
-
*
|
|
70
|
+
/** v2.11: Render a list with ID prefixes. Each item gets a
|
|
71
|
+
* [prefix-XXXXXXXX] tag derived from its text hash. Items that already look
|
|
72
|
+
* like IDs (e.g. "cr-a3f2b1c0") are rendered as-is.
|
|
73
|
+
* v3.8.1: IDs are always rendered — the `constraint_id_enabled` escape hatch
|
|
74
|
+
* was deleted (it never switched the MATCHING strategy, only the rendering,
|
|
75
|
+
* so it could only ever produce a prompt whose items the agent could not
|
|
76
|
+
* name). */
|
|
39
77
|
function addListWithIds(lines, title, values, prefix) {
|
|
40
78
|
if (values.length === 0)
|
|
41
79
|
return;
|
|
42
|
-
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
43
80
|
lines.push(`## ${title}`, "");
|
|
44
81
|
for (const value of values) {
|
|
45
82
|
const trimmed = value.trim();
|
|
@@ -49,12 +86,8 @@ function addListWithIds(lines, title, values, prefix) {
|
|
|
49
86
|
if (STABLE_ID_RE.test(trimmed)) {
|
|
50
87
|
lines.push(`- [\`${trimmed}\`] ${trimmed}`);
|
|
51
88
|
}
|
|
52
|
-
else if (idEnabled) {
|
|
53
|
-
const id = prefix + "-" + deriveItemId(trimmed);
|
|
54
|
-
lines.push(`- [\`${id}\`] ${trimmed}`);
|
|
55
|
-
}
|
|
56
89
|
else {
|
|
57
|
-
lines.push(`- ${trimmed}`);
|
|
90
|
+
lines.push(`- [\`${prefix}-${deriveItemId(trimmed)}\`] ${trimmed}`);
|
|
58
91
|
}
|
|
59
92
|
}
|
|
60
93
|
lines.push("");
|
|
@@ -69,7 +102,6 @@ function addListWithIds(lines, title, values, prefix) {
|
|
|
69
102
|
function addListWithIdentities(lines, title, values, metadata) {
|
|
70
103
|
if (values.length === 0)
|
|
71
104
|
return;
|
|
72
|
-
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
73
105
|
const idByText = new Map(metadata.map((meta) => [meta.text, meta.id]));
|
|
74
106
|
lines.push(`## ${title}`, "");
|
|
75
107
|
for (const value of values) {
|
|
@@ -79,110 +111,82 @@ function addListWithIdentities(lines, title, values, metadata) {
|
|
|
79
111
|
if (STABLE_ID_RE.test(trimmed)) {
|
|
80
112
|
lines.push(`- [\`${trimmed}\`] ${trimmed}`);
|
|
81
113
|
}
|
|
82
|
-
else
|
|
114
|
+
else {
|
|
83
115
|
const id = idByText.get(trimmed) ?? `c-${deriveItemId(trimmed)}`;
|
|
84
116
|
lines.push(`- [\`${id}\`] ${trimmed}`);
|
|
85
117
|
}
|
|
86
|
-
else {
|
|
87
|
-
lines.push(`- ${trimmed}`);
|
|
88
|
-
}
|
|
89
118
|
}
|
|
90
119
|
lines.push("");
|
|
91
120
|
}
|
|
92
|
-
/**
|
|
93
|
-
*
|
|
94
|
-
/** v3.3: Forward-looking "roadmap" view derived from existing state — loop
|
|
95
|
-
* position, met/remaining criteria (with IDs), and sub-goal activity.
|
|
121
|
+
/** v3.8.1: the one-line phase position — which phase the loop is in, and how
|
|
122
|
+
* far the next boundary is.
|
|
96
123
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
export function buildRoadmap(state) {
|
|
124
|
+
* This is all that survives of v3.3's "roadmap": of its four lines, three
|
|
125
|
+
* (met/remaining criteria with ids, sub-goal activity) were already rendered
|
|
126
|
+
* in full by the sections that own them. Returns "" when no milestone exists,
|
|
127
|
+
* so the caller renders no section at all — bare position is already in the
|
|
128
|
+
* prompt header.
|
|
129
|
+
*
|
|
130
|
+
* Shared by the state-file and prompt renderers so they cannot drift apart
|
|
131
|
+
* (module contract above). Presentation only; never feeds enforcement. */
|
|
132
|
+
export function buildPhaseLine(state) {
|
|
107
133
|
const lastMilestone = state.milestones.length > 0
|
|
108
134
|
? state.milestones[state.milestones.length - 1]
|
|
109
135
|
: null;
|
|
110
|
-
|
|
111
|
-
|
|
136
|
+
// No phase boundary yet → no phase to report. Position alone ("round 3/200")
|
|
137
|
+
// is already in the prompt header, so rendering it again would be noise.
|
|
138
|
+
if (!lastMilestone)
|
|
139
|
+
return "";
|
|
140
|
+
const roundsSince = lastMilestone.round_range.end <= state.round
|
|
112
141
|
? state.round - lastMilestone.round_range.end
|
|
113
142
|
: null;
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// criteria, remaining criteria, or sub-goal activity means nothing to
|
|
121
|
-
// look ahead to. Returns [] so empty states render no section at all.
|
|
122
|
-
if (roundsSinceMilestone === null && total === 0 &&
|
|
123
|
-
state.remainingCriteria.length === 0 && inProgress === 0 && pending === 0) {
|
|
124
|
-
return [];
|
|
125
|
-
}
|
|
126
|
-
const lines = [];
|
|
127
|
-
let position = `Position: round ${state.round}/${state.maxRounds}`;
|
|
128
|
-
if (roundsSinceMilestone !== null) {
|
|
129
|
-
position += ` · ${roundsSinceMilestone} round${roundsSinceMilestone === 1 ? "" : "s"} since the last milestone boundary`;
|
|
130
|
-
}
|
|
131
|
-
lines.push(`- ${position}`);
|
|
132
|
-
if (total > 0) {
|
|
133
|
-
lines.push(`- Criteria: ${met}/${total} met · ${remaining.length} remaining`);
|
|
134
|
-
}
|
|
135
|
-
else if (state.remainingCriteria.length > 0) {
|
|
136
|
-
lines.push(`- Criteria: ${state.remainingCriteria.length} remaining`);
|
|
137
|
-
}
|
|
138
|
-
if (remaining.length > 0) {
|
|
139
|
-
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
140
|
-
for (const cs of remaining.slice(0, 8)) {
|
|
141
|
-
const idTag = idEnabled ? ` [\`${cs.id}\`]` : "";
|
|
142
|
-
const icon = cs.status === "remaining" ? "⬜" : "❔";
|
|
143
|
-
lines.push(` - ${icon}${idTag} ${cs.text}`);
|
|
144
|
-
}
|
|
145
|
-
if (remaining.length > 8) {
|
|
146
|
-
lines.push(` - ... and ${remaining.length - 8} more`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (inProgress > 0 || pending > 0) {
|
|
150
|
-
lines.push(`- Sub-goals: ${inProgress} in progress · ${pending} pending`);
|
|
143
|
+
const parts = [
|
|
144
|
+
`phase "${lastMilestone.label}"`,
|
|
145
|
+
`round ${state.round}/${state.maxRounds}`,
|
|
146
|
+
];
|
|
147
|
+
if (roundsSince !== null) {
|
|
148
|
+
parts.push(`${roundsSince} round${roundsSince === 1 ? "" : "s"} since the last boundary`);
|
|
151
149
|
}
|
|
152
|
-
return
|
|
150
|
+
return parts.join(" · ");
|
|
153
151
|
}
|
|
154
152
|
/** v3.3/v3.4: Render the ACTIVE Round Contract as the Current Task section
|
|
155
153
|
* body — the single formatting source shared by prompts and the state
|
|
156
154
|
* file (module contract above). The original objective is NOT here: it
|
|
157
155
|
* lives in the Objective section. Empty arrays render no line. */
|
|
158
|
-
export function formatRoundContract(contract
|
|
156
|
+
export function formatRoundContract(contract,
|
|
157
|
+
/** v3.8: the derived item statuses. When given, each item renders with its
|
|
158
|
+
* machine status — the agent sees the verification debt in its own task. */
|
|
159
|
+
statuses) {
|
|
159
160
|
const lines = [];
|
|
160
161
|
const heading = contract.work_item?.trim();
|
|
161
|
-
lines.push(`**${heading || "Round Contract"}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
lines.push(`**${heading || "Round Contract"}** (${contract.id})`);
|
|
163
|
+
const statusById = new Map((statuses?.items ?? []).map((item) => [item.itemId, item]));
|
|
164
|
+
for (const item of contract.items) {
|
|
165
|
+
const machine = statusById.get(item.id);
|
|
166
|
+
const mark = machine?.status === "verified" ? "✅"
|
|
167
|
+
: machine?.status === "insufficient" ? "🟠"
|
|
168
|
+
: machine?.status === "contradicted" ? "⛔"
|
|
169
|
+
: machine?.status === "pending" ? "⬜"
|
|
170
|
+
: "";
|
|
171
|
+
lines.push(`- ${mark ? `${mark} ` : ""}[\`${item.id}\`] ${item.description}${machine ? ` — ${machine.status}` : ""}`);
|
|
172
|
+
if (item.verify_with.length > 0) {
|
|
173
|
+
lines.push(` - Verify via: ${item.verify_with.join(", ")}`);
|
|
174
|
+
}
|
|
175
|
+
if (item.criterion_refs.length > 0) {
|
|
176
|
+
lines.push(` - Criteria: ${item.criterion_refs.join(", ")}`);
|
|
177
|
+
}
|
|
178
|
+
if (machine?.reasons.length) {
|
|
179
|
+
lines.push(` - Unverified because: ${machine.reasons[0]}`);
|
|
180
|
+
}
|
|
167
181
|
}
|
|
168
|
-
for (const
|
|
169
|
-
lines.push(`- Scope: ${
|
|
182
|
+
for (const entry of contract.scope) {
|
|
183
|
+
lines.push(`- Scope: ${entry}`);
|
|
170
184
|
}
|
|
171
185
|
return lines.join("\n");
|
|
172
186
|
}
|
|
173
187
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
174
188
|
// v3.3.1: Shared presentation atoms
|
|
175
189
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
176
|
-
/** The trust bar line ("██████░░░░ 60%"). The L2 Agent Trust section and the
|
|
177
|
-
* detailed L1 renderer used to carry private copies of this formula — shared
|
|
178
|
-
* here so a formatting change is made once (module contract: renderers must
|
|
179
|
-
* not silently drift apart). */
|
|
180
|
-
export function trustBarLine(score) {
|
|
181
|
-
const barLen = 10;
|
|
182
|
-
const filled = Math.round(score * barLen);
|
|
183
|
-
const bar = "█".repeat(filled) + "░".repeat(barLen - filled);
|
|
184
|
-
return `${bar} ${(score * 100).toFixed(0)}%`;
|
|
185
|
-
}
|
|
186
190
|
/** Milestone heading line ("**🏁 Round 7** (Rounds 3–7, 60%)"). Shared by the
|
|
187
191
|
* L2 Phase History and the detailed L1 renderer; the L1 copy previously
|
|
188
192
|
* rendered the range as "R3–R7", a format only this heading used. */
|
|
@@ -192,22 +196,90 @@ export function milestoneHeading(milestone) {
|
|
|
192
196
|
: milestone.kind === "criteria_milestone" ? "✅" : "📍";
|
|
193
197
|
return `**${kindIcon} ${milestone.label}** (Rounds ${milestone.round_range.start}–${milestone.round_range.end}, ${(milestone.progress_at_boundary * 100).toFixed(0)}%)`;
|
|
194
198
|
}
|
|
195
|
-
|
|
196
|
-
|
|
199
|
+
const STATE_SECTION_TIER = {
|
|
200
|
+
// Current — what the next round must hold in working memory.
|
|
201
|
+
"Loop Objective": "Current",
|
|
202
|
+
"Current Task": "Current",
|
|
203
|
+
"Success Criteria": "Current",
|
|
204
|
+
"Hard Constraints": "Current",
|
|
205
|
+
"Active Constraints": "Current",
|
|
206
|
+
Remaining: "Current",
|
|
207
|
+
Blockers: "Current",
|
|
208
|
+
"Sub-Goal Dashboard": "Current",
|
|
209
|
+
"External Context": "Current",
|
|
210
|
+
// v3.8.1: the one-line phase position (the only fact the deleted Roadmap
|
|
211
|
+
// section carried that no other section renders). It is orientation for the
|
|
212
|
+
// NEXT round, so it belongs with the working-memory tier — and it must be
|
|
213
|
+
// listed here at all: an unlisted title is not a heading, so `## Phase` was
|
|
214
|
+
// absorbed into whatever section happened to precede it (its body even kept
|
|
215
|
+
// a raw `## ` line) and inherited that section's tier.
|
|
216
|
+
Phase: "Current",
|
|
217
|
+
// Recent — this round's boundary facts (and, in a recovery window, the
|
|
218
|
+
// derived Recovery Brief injected at the top of this tier).
|
|
219
|
+
"Progress Dashboard": "Recent",
|
|
220
|
+
Verification: "Recent",
|
|
221
|
+
"Changes Since Last Round": "Recent",
|
|
222
|
+
Discoveries: "Recent",
|
|
223
|
+
// Historical Summary — the loop's memory skeleton, kept under existing
|
|
224
|
+
// milestone/window caps.
|
|
225
|
+
"Goal → Criteria": "Historical Summary",
|
|
226
|
+
"Cross-Round Outcomes": "Historical Summary",
|
|
227
|
+
"Recurring Flags": "Historical Summary",
|
|
228
|
+
"Failed Patterns": "Historical Summary",
|
|
229
|
+
"Retired Constraints": "Historical Summary",
|
|
230
|
+
"Phase History": "Historical Summary",
|
|
231
|
+
};
|
|
232
|
+
const STATE_TIER_ORDER = ["Current", "Recent", "Historical Summary"];
|
|
233
|
+
/** Human/Agent-readable materialized view. It is always reproducible from the
|
|
234
|
+
* canonical state and is never consulted as transaction truth.
|
|
235
|
+
*
|
|
236
|
+
* v3.8.1: this is where the diagnostics live — the progress dashboard, the
|
|
237
|
+
* criterion list, round stats, the FULL recurring-flag history and the phase
|
|
238
|
+
* history. The prompt carries only what the next round needs. */
|
|
239
|
+
export function renderCanonicalStateMarkdown(state, options) {
|
|
240
|
+
const preamble = [
|
|
197
241
|
`# LoopForge State — ${state.loopId}`,
|
|
198
242
|
"",
|
|
199
243
|
`**Schema**: ${state.schemaVersion}`,
|
|
200
244
|
`**Round**: ${state.round}/${state.maxRounds}`,
|
|
201
245
|
`**Goal ID**: ${state.goalId}`,
|
|
246
|
+
"**Derived**: true",
|
|
247
|
+
`**Source**: round ${state.round} (attempt ${options?.attempt ?? 1})`,
|
|
248
|
+
`**State hash**: ${hashCanonicalState(state).slice(0, 12)}`,
|
|
202
249
|
"",
|
|
203
|
-
"## Loop Objective",
|
|
204
|
-
"",
|
|
205
|
-
state.objective,
|
|
206
|
-
"",
|
|
207
|
-
"## Current Task",
|
|
208
|
-
"",
|
|
209
|
-
state.currentTask,
|
|
210
250
|
];
|
|
251
|
+
const sections = flatStateSections(state);
|
|
252
|
+
if (options?.recoveryBrief && options.recoveryBrief.length > 0) {
|
|
253
|
+
sections.unshift({ title: "Recovery Brief", body: options.recoveryBrief });
|
|
254
|
+
}
|
|
255
|
+
const byTier = new Map();
|
|
256
|
+
for (const tier of STATE_TIER_ORDER)
|
|
257
|
+
byTier.set(tier, []);
|
|
258
|
+
for (const section of sections) {
|
|
259
|
+
const tier = STATE_SECTION_TIER[section.title] ?? "Recent";
|
|
260
|
+
byTier.get(tier).push(section);
|
|
261
|
+
}
|
|
262
|
+
const out = [...preamble];
|
|
263
|
+
for (const tier of STATE_TIER_ORDER) {
|
|
264
|
+
const tierSections = byTier.get(tier);
|
|
265
|
+
if (tierSections.length === 0)
|
|
266
|
+
continue;
|
|
267
|
+
out.push(`## ${tier}`, "");
|
|
268
|
+
for (const section of tierSections) {
|
|
269
|
+
out.push(`### ${section.title}`, "");
|
|
270
|
+
for (const line of section.body)
|
|
271
|
+
out.push(line);
|
|
272
|
+
out.push("");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return out.join("\n").trimEnd() + "\n";
|
|
276
|
+
}
|
|
277
|
+
/** Render the flat section list (title + body) that the tier wrapper
|
|
278
|
+
* groups. Each builder keeps its original heading text; only the heading
|
|
279
|
+
* level is demoted by the wrapper. */
|
|
280
|
+
function flatStateSections(state) {
|
|
281
|
+
const lines = [];
|
|
282
|
+
lines.push("## Loop Objective", "", state.objective, "", "## Current Task", "", state.currentTask);
|
|
211
283
|
// v2.11: Success Criteria and Constraints render with stable IDs for exact agent matching
|
|
212
284
|
addListWithIds(lines, "Success Criteria", state.successCriteria, "cr");
|
|
213
285
|
addListWithIds(lines, "Hard Constraints", state.hardConstraints, "c");
|
|
@@ -239,7 +311,7 @@ export function renderCanonicalStateMarkdown(state) {
|
|
|
239
311
|
lines.push(`**Machine (git)**: ${motion}`);
|
|
240
312
|
}
|
|
241
313
|
if (state.criterionStatuses.length > 0) {
|
|
242
|
-
const metCount = state.criterionStatuses.filter((cs) => cs.status === "
|
|
314
|
+
const metCount = state.criterionStatuses.filter((cs) => cs.status === "verified" || cs.status === "claimed").length;
|
|
243
315
|
lines.push(`**Machine (criteria)**: ${metCount}/${state.criterionStatuses.length} met across committed rounds`);
|
|
244
316
|
}
|
|
245
317
|
if (state.progress.tests) {
|
|
@@ -253,14 +325,16 @@ export function renderCanonicalStateMarkdown(state) {
|
|
|
253
325
|
}
|
|
254
326
|
lines.push("");
|
|
255
327
|
}
|
|
256
|
-
// v3.2: Goal → criteria → evidence vertical view. IDs
|
|
257
|
-
// constraint_id_enabled like every other ID-rendering path.
|
|
328
|
+
// v3.2: Goal → criteria → evidence vertical view. IDs are always rendered.
|
|
258
329
|
if (state.criterionStatuses.length > 0) {
|
|
259
|
-
const idEnabled = getPolicy().evolution.constraint_id_enabled;
|
|
260
330
|
lines.push("## Goal → Criteria", "");
|
|
261
331
|
for (const cs of state.criterionStatuses) {
|
|
262
|
-
const icon = cs.status === "
|
|
263
|
-
|
|
332
|
+
const icon = cs.status === "verified" ? "✅"
|
|
333
|
+
: cs.status === "claimed" ? "🟡"
|
|
334
|
+
: cs.status === "insufficient" ? "🟠"
|
|
335
|
+
: cs.status === "contradicted" ? "⛔"
|
|
336
|
+
: cs.status === "remaining" ? "⬜" : "❔";
|
|
337
|
+
const idTag = ` [\`${cs.id}\`]`;
|
|
264
338
|
const met = cs.met_at_round !== undefined ? ` (met R${cs.met_at_round})` : "";
|
|
265
339
|
const related = cs.related_subgoal_ids.length > 0
|
|
266
340
|
? ` (related: ${cs.related_subgoal_ids.join(", ")})`
|
|
@@ -271,34 +345,35 @@ export function renderCanonicalStateMarkdown(state) {
|
|
|
271
345
|
}
|
|
272
346
|
addListWithIds(lines, "Remaining", state.remainingCriteria, "cr");
|
|
273
347
|
addList(lines, "Blockers", state.blockers);
|
|
348
|
+
const recurring = recurringOnly(state.recurringFlags);
|
|
274
349
|
addList(lines, "Discoveries", state.discoveries);
|
|
275
350
|
addList(lines, "Cross-Round Outcomes", state.rollingOutcomes);
|
|
276
|
-
addList(lines, "Recurring Issues", state.recurringIssues);
|
|
277
351
|
addList(lines, "Failed Patterns", state.failedPatterns);
|
|
278
|
-
// v3.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
352
|
+
// v3.8.1: ONE recurring-facts section. "Recurring Issues" (the last 5
|
|
353
|
+
// rounds' raw violations, no threshold) and "Lessons Learned" (the whole
|
|
354
|
+
// history, count >= 2) were two windows over one fact set rendered as two
|
|
355
|
+
// headings. The state file keeps the genuinely recurring set; the prompt
|
|
356
|
+
// shows the recent tail of the same derivation.
|
|
357
|
+
if (recurring.length > 0) {
|
|
358
|
+
lines.push("## Recurring Flags", "");
|
|
359
|
+
for (const flag of recurring.slice(0, RECURRING_RENDER_CAP)) {
|
|
360
|
+
const icon = flag.kind === "constraint_violation"
|
|
283
361
|
? "🚫"
|
|
284
|
-
:
|
|
285
|
-
|
|
362
|
+
: flag.kind === "verification_error" ? "⚠️" : "ℹ️";
|
|
363
|
+
const subject = flag.ref ? `${flag.subject} [${flag.ref}]` : flag.subject;
|
|
364
|
+
lines.push(`- ${icon} ${subject} — ${flag.count}× (R${flag.rounds.join(", R")})`);
|
|
286
365
|
}
|
|
287
|
-
if (
|
|
288
|
-
lines.push(`- ... and ${
|
|
366
|
+
if (recurring.length > RECURRING_RENDER_CAP) {
|
|
367
|
+
lines.push(`- ... and ${recurring.length - RECURRING_RENDER_CAP} more`);
|
|
289
368
|
}
|
|
290
369
|
lines.push("");
|
|
291
370
|
}
|
|
292
371
|
addList(lines, "Retired Constraints", state.retiredConstraints);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (roadmap.length > 0) {
|
|
299
|
-
lines.push("## Roadmap", "");
|
|
300
|
-
for (const line of roadmap)
|
|
301
|
-
lines.push(line);
|
|
372
|
+
// v3.8.1: the Roadmap section is gone. Only its non-duplicated fact — the
|
|
373
|
+
// phase position — survives, as the one-line phase line the prompts render.
|
|
374
|
+
const phaseLine = buildPhaseLine(state);
|
|
375
|
+
if (phaseLine) {
|
|
376
|
+
lines.push("## Phase", "", `- ${phaseLine}`);
|
|
302
377
|
lines.push("");
|
|
303
378
|
}
|
|
304
379
|
if (state.verificationFlags.length > 0) {
|
|
@@ -331,50 +406,81 @@ export function renderCanonicalStateMarkdown(state) {
|
|
|
331
406
|
}
|
|
332
407
|
}
|
|
333
408
|
if (state.subGoals.length > 0) {
|
|
409
|
+
// v3.7.1: only ACTIVE items render as rows; done/canceled survive in
|
|
410
|
+
// the vault, replay, and the counts line below.
|
|
411
|
+
const cap = getPolicy().evolution.max_active_subgoals;
|
|
412
|
+
const view = activeSubGoalView(state.subGoals, cap);
|
|
413
|
+
// L8: the heading is emitted whenever sub-goals exist — with every
|
|
414
|
+
// sub-goal terminal (0 active rows) the counts line must still live
|
|
415
|
+
// under its own section instead of leaking into the previous one.
|
|
334
416
|
lines.push("## Sub-Goal Dashboard", "");
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
sg.status === "
|
|
338
|
-
sg.status === "blocked" ? "🚫" :
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const detail = sg.status === "done" && sg.completed_at_round
|
|
343
|
-
? ` (done, R${sg.completed_at_round})`
|
|
344
|
-
: sg.status === "in_progress"
|
|
417
|
+
if (view.active.length > 0) {
|
|
418
|
+
for (const sg of view.active) {
|
|
419
|
+
const statusIcon = sg.status === "in_progress" ? "🔄" :
|
|
420
|
+
sg.status === "blocked" ? "🚫" : "⏳";
|
|
421
|
+
const stale = sg.status === "pending" &&
|
|
422
|
+
state.round - sg.declared_at_round >= 10 ? " ⚠️ stale" : "";
|
|
423
|
+
const detail = sg.status === "in_progress"
|
|
345
424
|
? ` (since R${sg.status_changed_at_round})`
|
|
346
425
|
: sg.status === "pending"
|
|
347
426
|
? ` (since R${sg.declared_at_round})`
|
|
348
|
-
: sg.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
lines.push(
|
|
427
|
+
: ` (blocked R${sg.status_changed_at_round})`;
|
|
428
|
+
lines.push(`- ${statusIcon} [\`${sg.id}\`] ${sg.description}${detail}${stale}`);
|
|
429
|
+
}
|
|
430
|
+
lines.push("");
|
|
352
431
|
}
|
|
353
|
-
lines.push(
|
|
354
|
-
|
|
355
|
-
if (state.agentTrustScore !== undefined) {
|
|
356
|
-
const barLen = 10;
|
|
357
|
-
const filled = Math.round(state.agentTrustScore * barLen);
|
|
358
|
-
const bar = "█".repeat(filled) + "░".repeat(barLen - filled);
|
|
359
|
-
const avg = state.agentTrustTrend.length > 0
|
|
360
|
-
? (state.agentTrustTrend.reduce((a, b) => a + b, 0) / state.agentTrustTrend.length).toFixed(2)
|
|
361
|
-
: "—";
|
|
362
|
-
lines.push("## Agent Trust", "", `${bar} ${(state.agentTrustScore * 100).toFixed(0)}%`, `Trend (last ${state.agentTrustTrend.length}): ${state.agentTrustTrend.join(" → ")}`, `Average: ${avg}`, "");
|
|
432
|
+
lines.push(`> ${view.total} total · ${view.activeTotal} active · ` +
|
|
433
|
+
`${view.done} done · ${view.canceled} canceled`, "");
|
|
363
434
|
}
|
|
364
435
|
if (state.externalContext) {
|
|
365
436
|
lines.push("## External Context", "", state.externalContext, "");
|
|
366
437
|
}
|
|
367
|
-
|
|
438
|
+
// Split the flat markdown into titled sections for tier grouping. Every
|
|
439
|
+
// builder above emits `## <Title>` headings over their content — but only
|
|
440
|
+
// the KNOWN section titles are headings. Free-text bodies (objective,
|
|
441
|
+
// external context, milestone outcomes) may contain their own markdown
|
|
442
|
+
// lines starting with "## "; splitting on any such line would cut the
|
|
443
|
+
// body into a phantom section and re-tier everything that follows.
|
|
444
|
+
const knownSectionTitles = new Set([
|
|
445
|
+
...Object.keys(STATE_SECTION_TIER),
|
|
446
|
+
"Recovery Brief",
|
|
447
|
+
]);
|
|
448
|
+
const sections = [];
|
|
449
|
+
let current = null;
|
|
450
|
+
let inBody = false;
|
|
451
|
+
for (const line of lines) {
|
|
452
|
+
const heading = /^## (.+)$/.exec(line);
|
|
453
|
+
if (heading && knownSectionTitles.has(heading[1])) {
|
|
454
|
+
current = { title: heading[1], body: [] };
|
|
455
|
+
sections.push(current);
|
|
456
|
+
inBody = false;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (!current)
|
|
460
|
+
continue;
|
|
461
|
+
if (!inBody) {
|
|
462
|
+
if (line.trim() === "")
|
|
463
|
+
continue; // drop leading blanks after the heading
|
|
464
|
+
inBody = true;
|
|
465
|
+
}
|
|
466
|
+
current.body.push(line);
|
|
467
|
+
}
|
|
468
|
+
for (const section of sections) {
|
|
469
|
+
while (section.body.length > 0 && section.body[section.body.length - 1] === "") {
|
|
470
|
+
section.body.pop();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return sections;
|
|
368
474
|
}
|
|
369
|
-
export function createCanonicalLoopState(request, response, stateFilePath,
|
|
370
|
-
/** v3.3: Display-only derived data
|
|
371
|
-
*
|
|
475
|
+
export function createCanonicalLoopState(request, response, stateFilePath,
|
|
476
|
+
/** v3.3: Display-only derived data. Optional 4th param — callers that
|
|
477
|
+
* predate v3.3 stay on 3-arg calls. */
|
|
372
478
|
derived) {
|
|
373
479
|
const last = request.last_round_result;
|
|
374
480
|
const objective = response.loop_objective;
|
|
375
481
|
const verificationFlags = request.verification_flags ?? [];
|
|
376
482
|
const rolling = response.rolling_summary;
|
|
377
|
-
const
|
|
483
|
+
const executionReport = last?.execution_report;
|
|
378
484
|
const changes = unique([
|
|
379
485
|
request.new_since_last_round,
|
|
380
486
|
last?.output_summary,
|
|
@@ -395,54 +501,56 @@ derived) {
|
|
|
395
501
|
// to do this round"; the original objective stays in the Objective
|
|
396
502
|
// section above. No active contract → the original task text is used,
|
|
397
503
|
// byte-identical to pre-v3.3 behavior.
|
|
398
|
-
const roundContract = derived?.
|
|
504
|
+
const roundContract = derived?.roundFacts?.activeContract ?? null;
|
|
399
505
|
return {
|
|
400
506
|
schemaVersion: CANONICAL_STATE_SCHEMA_VERSION,
|
|
401
507
|
loopId: response.loop_id || request.loop_id,
|
|
402
508
|
round: response.round || request.round,
|
|
403
|
-
maxRounds: request.max_rounds ??
|
|
509
|
+
maxRounds: request.max_rounds ?? getPolicy().engine.max_rounds,
|
|
404
510
|
goalId: response.goal_id,
|
|
405
511
|
objective: objective?.objective || request.task,
|
|
406
512
|
objectiveVersion: objective?.version ?? 1,
|
|
407
|
-
currentTask: roundContract
|
|
513
|
+
currentTask: roundContract
|
|
514
|
+
? formatRoundContract(roundContract, derived?.roundFacts?.itemStatuses ?? null)
|
|
515
|
+
: request.task,
|
|
408
516
|
successCriteria: unique(objective?.success_criteria ?? []),
|
|
409
517
|
hardConstraints: unique(objective?.hard_constraints ?? []),
|
|
410
518
|
activeConstraints: unique(response.constraints_active),
|
|
411
519
|
retiredConstraints: unique(response.constraints_retired),
|
|
412
520
|
constraintMetadata: response.constraint_metadata ?? [],
|
|
413
521
|
changesSinceLastRound: changes,
|
|
414
|
-
remainingCriteria: unique(last?.
|
|
522
|
+
remainingCriteria: unique(claimedRemainingCriteria(last?.execution_report)),
|
|
415
523
|
blockers,
|
|
416
524
|
verificationFlags,
|
|
417
525
|
discoveries,
|
|
418
|
-
nextAction: last?.next_action?.trim() || response.suggested_next_task,
|
|
419
526
|
rollingOutcomes: unique(rolling?.key_outcomes ?? []),
|
|
420
|
-
recurringIssues: unique(rolling?.recurring_issues ?? []),
|
|
421
527
|
failedPatterns: unique(rolling?.failed_patterns ?? []),
|
|
422
528
|
milestones: rolling?.milestones ?? [],
|
|
423
529
|
subGoals: response.sub_goals ?? [],
|
|
424
530
|
criterionStatuses: response.criterion_statuses ?? [],
|
|
425
|
-
|
|
426
|
-
agentTrustScore: response.agent_trust_score,
|
|
427
|
-
agentTrustTrend: response.agent_trust_trend ?? [],
|
|
428
|
-
suggestedNextTask: response.suggested_next_task,
|
|
531
|
+
recurringFlags: response.recurring_flags ?? [],
|
|
429
532
|
externalContext: request.external_context?.trim() ?? "",
|
|
430
533
|
stateFilePath,
|
|
431
534
|
progress: {
|
|
432
|
-
estimate:
|
|
433
|
-
criteriaMet: unique(
|
|
434
|
-
criteriaRemaining: unique(
|
|
435
|
-
filesChanged: unique(
|
|
436
|
-
tests:
|
|
535
|
+
estimate: executionReport?.progress_estimate ?? null,
|
|
536
|
+
criteriaMet: unique(claimedMetCriteria(executionReport)),
|
|
537
|
+
criteriaRemaining: unique(claimedRemainingCriteria(executionReport)),
|
|
538
|
+
filesChanged: unique(executionReport?.files_changed ?? []),
|
|
539
|
+
tests: executionReport?.tests_reported ?? null,
|
|
437
540
|
},
|
|
438
541
|
// v3.3: Conditional presence — empty/absent derived data must not add
|
|
439
542
|
// keys, or every state hash would change for rounds without it.
|
|
440
|
-
...(derived?.roundStats && derived.roundStats.length > 0
|
|
441
|
-
? { roundStats: derived.roundStats }
|
|
442
|
-
: {}),
|
|
443
543
|
...(derived?.machineStatus ? { machineStatus: derived.machineStatus } : {}),
|
|
444
544
|
// v3.3: Conditional presence — no contract, no key (hash stability).
|
|
445
545
|
...(roundContract ? { roundContract } : {}),
|
|
546
|
+
...(derived?.roundFacts?.itemStatuses
|
|
547
|
+
? { contractItemStatuses: derived.roundFacts.itemStatuses }
|
|
548
|
+
: {}),
|
|
549
|
+
...(derived?.roundFacts?.verifiedSubGoals &&
|
|
550
|
+
derived.roundFacts.verifiedSubGoals.length > 0
|
|
551
|
+
? { verifiedSubGoals: [...derived.roundFacts.verifiedSubGoals] }
|
|
552
|
+
: {}),
|
|
553
|
+
capability: deriveConfiguredCapability(getPolicy()),
|
|
446
554
|
};
|
|
447
555
|
}
|
|
448
556
|
//# sourceMappingURL=canonical-state.js.map
|