loopforge 3.9.4 → 4.0.7

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.
Files changed (283) hide show
  1. package/README.md +236 -112
  2. package/dist/audit.d.ts +73 -15
  3. package/dist/audit.d.ts.map +1 -1
  4. package/dist/audit.js +131 -61
  5. package/dist/audit.js.map +1 -1
  6. package/dist/canonical-state.d.ts +114 -40
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +276 -182
  9. package/dist/canonical-state.js.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +41 -12
  12. package/dist/cli.js.map +1 -1
  13. package/dist/cognitive-governance.d.ts +0 -2
  14. package/dist/cognitive-governance.d.ts.map +1 -1
  15. package/dist/cognitive-governance.js +0 -4
  16. package/dist/cognitive-governance.js.map +1 -1
  17. package/dist/committed-round.d.ts +97 -156
  18. package/dist/committed-round.d.ts.map +1 -1
  19. package/dist/committed-round.js +223 -419
  20. package/dist/committed-round.js.map +1 -1
  21. package/dist/constraint-lifecycle.d.ts +73 -0
  22. package/dist/constraint-lifecycle.d.ts.map +1 -0
  23. package/dist/constraint-lifecycle.js +174 -0
  24. package/dist/constraint-lifecycle.js.map +1 -0
  25. package/dist/context-boundary.d.ts +83 -0
  26. package/dist/context-boundary.d.ts.map +1 -0
  27. package/dist/context-boundary.js +101 -0
  28. package/dist/context-boundary.js.map +1 -0
  29. package/dist/contract-items.d.ts +12 -8
  30. package/dist/contract-items.d.ts.map +1 -1
  31. package/dist/contract-items.js +31 -26
  32. package/dist/contract-items.js.map +1 -1
  33. package/dist/contract-requirement.d.ts +47 -0
  34. package/dist/contract-requirement.d.ts.map +1 -0
  35. package/dist/contract-requirement.js +51 -0
  36. package/dist/contract-requirement.js.map +1 -0
  37. package/dist/criterion-status.d.ts +35 -0
  38. package/dist/criterion-status.d.ts.map +1 -0
  39. package/dist/criterion-status.js +89 -0
  40. package/dist/criterion-status.js.map +1 -0
  41. package/dist/enforcement-gate.d.ts +26 -38
  42. package/dist/enforcement-gate.d.ts.map +1 -1
  43. package/dist/enforcement-gate.js +196 -319
  44. package/dist/enforcement-gate.js.map +1 -1
  45. package/dist/engine.d.ts +9 -103
  46. package/dist/engine.d.ts.map +1 -1
  47. package/dist/engine.js +27 -897
  48. package/dist/engine.js.map +1 -1
  49. package/dist/errors.d.ts +41 -0
  50. package/dist/errors.d.ts.map +1 -0
  51. package/dist/errors.js +69 -0
  52. package/dist/errors.js.map +1 -0
  53. package/dist/evidence-claims.d.ts +33 -31
  54. package/dist/evidence-claims.d.ts.map +1 -1
  55. package/dist/evidence-claims.js +58 -72
  56. package/dist/evidence-claims.js.map +1 -1
  57. package/dist/evidence-posture-view.d.ts +19 -0
  58. package/dist/evidence-posture-view.d.ts.map +1 -0
  59. package/dist/evidence-posture-view.js +116 -0
  60. package/dist/evidence-posture-view.js.map +1 -0
  61. package/dist/evidence-posture.d.ts +73 -0
  62. package/dist/evidence-posture.d.ts.map +1 -0
  63. package/dist/evidence-posture.js +281 -0
  64. package/dist/evidence-posture.js.map +1 -0
  65. package/dist/evidence-provider.d.ts +55 -37
  66. package/dist/evidence-provider.d.ts.map +1 -1
  67. package/dist/evidence-provider.js +182 -332
  68. package/dist/evidence-provider.js.map +1 -1
  69. package/dist/explain.d.ts +166 -27
  70. package/dist/explain.d.ts.map +1 -1
  71. package/dist/explain.js +372 -58
  72. package/dist/explain.js.map +1 -1
  73. package/dist/field-accounting.d.ts.map +1 -1
  74. package/dist/field-accounting.js +3 -22
  75. package/dist/field-accounting.js.map +1 -1
  76. package/dist/generate-schema.js +13 -8
  77. package/dist/generate-schema.js.map +1 -1
  78. package/dist/git-boundary.d.ts +128 -0
  79. package/dist/git-boundary.d.ts.map +1 -0
  80. package/dist/git-boundary.js +625 -0
  81. package/dist/git-boundary.js.map +1 -0
  82. package/dist/git-facts.d.ts +22 -0
  83. package/dist/git-facts.d.ts.map +1 -0
  84. package/dist/git-facts.js +29 -0
  85. package/dist/git-facts.js.map +1 -0
  86. package/dist/git-motion.d.ts +37 -0
  87. package/dist/git-motion.d.ts.map +1 -0
  88. package/dist/git-motion.js +59 -0
  89. package/dist/git-motion.js.map +1 -0
  90. package/dist/history.d.ts +44 -0
  91. package/dist/history.d.ts.map +1 -0
  92. package/dist/history.js +77 -0
  93. package/dist/history.js.map +1 -0
  94. package/dist/index.d.ts +19 -9
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +12 -4
  97. package/dist/index.js.map +1 -1
  98. package/dist/init.d.ts +16 -0
  99. package/dist/init.d.ts.map +1 -1
  100. package/dist/init.js +80 -14
  101. package/dist/init.js.map +1 -1
  102. package/dist/loop-compiler.d.ts +39 -23
  103. package/dist/loop-compiler.d.ts.map +1 -1
  104. package/dist/loop-compiler.js +368 -565
  105. package/dist/loop-compiler.js.map +1 -1
  106. package/dist/loop-store.d.ts +6 -5
  107. package/dist/loop-store.d.ts.map +1 -1
  108. package/dist/loop-store.js +153 -35
  109. package/dist/loop-store.js.map +1 -1
  110. package/dist/mcp/round-lifecycle.d.ts +255 -81
  111. package/dist/mcp/round-lifecycle.d.ts.map +1 -1
  112. package/dist/mcp/round-lifecycle.js +1552 -539
  113. package/dist/mcp/round-lifecycle.js.map +1 -1
  114. package/dist/mcp/server.d.ts.map +1 -1
  115. package/dist/mcp/server.js +42 -18
  116. package/dist/mcp/server.js.map +1 -1
  117. package/dist/mcp/session.d.ts +81 -39
  118. package/dist/mcp/session.d.ts.map +1 -1
  119. package/dist/mcp/session.js +956 -282
  120. package/dist/mcp/session.js.map +1 -1
  121. package/dist/mcp/submission-boundary.d.ts +23 -31
  122. package/dist/mcp/submission-boundary.d.ts.map +1 -1
  123. package/dist/mcp/submission-boundary.js +242 -92
  124. package/dist/mcp/submission-boundary.js.map +1 -1
  125. package/dist/mcp/submission-handle.d.ts +66 -0
  126. package/dist/mcp/submission-handle.d.ts.map +1 -0
  127. package/dist/mcp/submission-handle.js +173 -0
  128. package/dist/mcp/submission-handle.js.map +1 -0
  129. package/dist/mcp/tools.d.ts +17 -7
  130. package/dist/mcp/tools.d.ts.map +1 -1
  131. package/dist/mcp/tools.js +707 -244
  132. package/dist/mcp/tools.js.map +1 -1
  133. package/dist/open-question.d.ts +7 -0
  134. package/dist/open-question.d.ts.map +1 -0
  135. package/dist/open-question.js +14 -0
  136. package/dist/open-question.js.map +1 -0
  137. package/dist/policy-metrics.d.ts +31 -21
  138. package/dist/policy-metrics.d.ts.map +1 -1
  139. package/dist/policy-metrics.js +117 -64
  140. package/dist/policy-metrics.js.map +1 -1
  141. package/dist/policy.d.ts +11 -39
  142. package/dist/policy.d.ts.map +1 -1
  143. package/dist/policy.js +12 -12
  144. package/dist/policy.js.map +1 -1
  145. package/dist/progress.d.ts +45 -0
  146. package/dist/progress.d.ts.map +1 -0
  147. package/dist/progress.js +136 -0
  148. package/dist/progress.js.map +1 -0
  149. package/dist/prompt-assembler.d.ts +20 -6
  150. package/dist/prompt-assembler.d.ts.map +1 -1
  151. package/dist/prompt-assembler.js +475 -479
  152. package/dist/prompt-assembler.js.map +1 -1
  153. package/dist/prompt-coverage.d.ts +0 -6
  154. package/dist/prompt-coverage.d.ts.map +1 -1
  155. package/dist/prompt-coverage.js +4 -8
  156. package/dist/prompt-coverage.js.map +1 -1
  157. package/dist/prompt-freshness.d.ts +6 -17
  158. package/dist/prompt-freshness.d.ts.map +1 -1
  159. package/dist/prompt-freshness.js +27 -22
  160. package/dist/prompt-freshness.js.map +1 -1
  161. package/dist/prompt-policy.d.ts +14 -7
  162. package/dist/prompt-policy.d.ts.map +1 -1
  163. package/dist/prompt-policy.js +21 -5
  164. package/dist/prompt-policy.js.map +1 -1
  165. package/dist/protocol.d.ts +843 -424
  166. package/dist/protocol.d.ts.map +1 -1
  167. package/dist/protocol.js +73 -102
  168. package/dist/protocol.js.map +1 -1
  169. package/dist/recovery-guidance.d.ts +13 -0
  170. package/dist/recovery-guidance.d.ts.map +1 -0
  171. package/dist/recovery-guidance.js +15 -0
  172. package/dist/recovery-guidance.js.map +1 -0
  173. package/dist/recovery.d.ts +68 -0
  174. package/dist/recovery.d.ts.map +1 -0
  175. package/dist/recovery.js +234 -0
  176. package/dist/recovery.js.map +1 -0
  177. package/dist/replay.d.ts +42 -8
  178. package/dist/replay.d.ts.map +1 -1
  179. package/dist/replay.js +311 -84
  180. package/dist/replay.js.map +1 -1
  181. package/dist/result-facts.d.ts +45 -0
  182. package/dist/result-facts.d.ts.map +1 -0
  183. package/dist/result-facts.js +86 -0
  184. package/dist/result-facts.js.map +1 -0
  185. package/dist/round-contract.d.ts +38 -8
  186. package/dist/round-contract.d.ts.map +1 -1
  187. package/dist/round-contract.js +71 -31
  188. package/dist/round-contract.js.map +1 -1
  189. package/dist/round-coordinator.d.ts +41 -89
  190. package/dist/round-coordinator.d.ts.map +1 -1
  191. package/dist/round-coordinator.js +250 -267
  192. package/dist/round-coordinator.js.map +1 -1
  193. package/dist/round-driver.d.ts +56 -16
  194. package/dist/round-driver.d.ts.map +1 -1
  195. package/dist/round-driver.js +225 -39
  196. package/dist/round-driver.js.map +1 -1
  197. package/dist/round-facts.d.ts +33 -52
  198. package/dist/round-facts.d.ts.map +1 -1
  199. package/dist/round-facts.js +51 -160
  200. package/dist/round-facts.js.map +1 -1
  201. package/dist/round-guidance.d.ts +56 -0
  202. package/dist/round-guidance.d.ts.map +1 -0
  203. package/dist/round-guidance.js +161 -0
  204. package/dist/round-guidance.js.map +1 -0
  205. package/dist/round-outcome.d.ts +21 -0
  206. package/dist/round-outcome.d.ts.map +1 -0
  207. package/dist/round-outcome.js +31 -0
  208. package/dist/round-outcome.js.map +1 -0
  209. package/dist/round-transaction.d.ts +168 -21
  210. package/dist/round-transaction.d.ts.map +1 -1
  211. package/dist/round-transaction.js +837 -38
  212. package/dist/round-transaction.js.map +1 -1
  213. package/dist/self-eval.d.ts +37 -61
  214. package/dist/self-eval.d.ts.map +1 -1
  215. package/dist/self-eval.js +413 -392
  216. package/dist/self-eval.js.map +1 -1
  217. package/dist/storage.d.ts +16 -0
  218. package/dist/storage.d.ts.map +1 -1
  219. package/dist/storage.js +54 -4
  220. package/dist/storage.js.map +1 -1
  221. package/dist/subgoal-state.d.ts +2 -3
  222. package/dist/subgoal-state.d.ts.map +1 -1
  223. package/dist/subgoal-state.js +2 -3
  224. package/dist/subgoal-state.js.map +1 -1
  225. package/dist/submission-guidance.d.ts +29 -21
  226. package/dist/submission-guidance.d.ts.map +1 -1
  227. package/dist/submission-guidance.js +308 -260
  228. package/dist/submission-guidance.js.map +1 -1
  229. package/dist/submission-receipt.d.ts +16 -8
  230. package/dist/submission-receipt.d.ts.map +1 -1
  231. package/dist/submission-receipt.js +145 -51
  232. package/dist/submission-receipt.js.map +1 -1
  233. package/dist/token-utils.d.ts +28 -7
  234. package/dist/token-utils.d.ts.map +1 -1
  235. package/dist/token-utils.js +27 -11
  236. package/dist/token-utils.js.map +1 -1
  237. package/dist/transaction-identity.d.ts +9 -0
  238. package/dist/transaction-identity.d.ts.map +1 -0
  239. package/dist/transaction-identity.js +69 -0
  240. package/dist/transaction-identity.js.map +1 -0
  241. package/dist/verification-actions.d.ts.map +1 -1
  242. package/dist/verification-actions.js +10 -11
  243. package/dist/verification-actions.js.map +1 -1
  244. package/dist/verification-gate.d.ts +23 -47
  245. package/dist/verification-gate.d.ts.map +1 -1
  246. package/dist/verification-gate.js +202 -423
  247. package/dist/verification-gate.js.map +1 -1
  248. package/dist/version.d.ts +1 -1
  249. package/dist/version.js +1 -1
  250. package/dist/working-memory.d.ts +187 -0
  251. package/dist/working-memory.d.ts.map +1 -0
  252. package/dist/working-memory.js +767 -0
  253. package/dist/working-memory.js.map +1 -0
  254. package/dist/workspace-checkpoint.d.ts +88 -0
  255. package/dist/workspace-checkpoint.d.ts.map +1 -0
  256. package/dist/workspace-checkpoint.js +326 -0
  257. package/dist/workspace-checkpoint.js.map +1 -0
  258. package/dist/workspace-restore.d.ts +68 -0
  259. package/dist/workspace-restore.d.ts.map +1 -0
  260. package/dist/workspace-restore.js +604 -0
  261. package/dist/workspace-restore.js.map +1 -0
  262. package/dist/workspace.d.ts +0 -9
  263. package/dist/workspace.d.ts.map +1 -1
  264. package/dist/workspace.js +0 -17
  265. package/dist/workspace.js.map +1 -1
  266. package/loop_policy.json +4 -18
  267. package/package.json +68 -68
  268. package/skills/forge/SKILL.md +212 -0
  269. package/skills/forge/references/evidence-and-contracts.md +204 -0
  270. package/skills/forge/references/recovery-and-views.md +188 -0
  271. package/dist/cognitive-facts.d.ts +0 -30
  272. package/dist/cognitive-facts.d.ts.map +0 -1
  273. package/dist/cognitive-facts.js +0 -113
  274. package/dist/cognitive-facts.js.map +0 -1
  275. package/dist/loop-extras-parser.d.ts +0 -81
  276. package/dist/loop-extras-parser.d.ts.map +0 -1
  277. package/dist/loop-extras-parser.js +0 -213
  278. package/dist/loop-extras-parser.js.map +0 -1
  279. package/dist/loop-projection.d.ts +0 -6
  280. package/dist/loop-projection.d.ts.map +0 -1
  281. package/dist/loop-projection.js +0 -28
  282. package/dist/loop-projection.js.map +0 -1
  283. package/skills/perception/SKILL.md +0 -389
@@ -8,7 +8,29 @@ import { deriveCriterionId, deriveItemId, normalizeText, STABLE_ID_RE, unique, u
8
8
  import { claimedMetCriteria, claimedRemainingCriteria } from "./self-eval.js";
9
9
  import { getPolicy } from "./policy.js";
10
10
  import { deriveConfiguredCapability } from "./policy.js";
11
- export const CANONICAL_STATE_SCHEMA_VERSION = 1;
11
+ import { formatWorkingMemoryState, hasWorkingMemoryContent, } from "./working-memory.js";
12
+ import { isOpenQuestionId } from "./open-question.js";
13
+ import { MACHINE_EVIDENCE_AUTHORITY, formatEvidencePostureLines, } from "./evidence-posture-view.js";
14
+ import { formatContextBoundaryLabel } from "./round-guidance.js";
15
+ /** v4.0.3: schema 7 adds the previous/retry evidence posture, explicitly split
16
+ * reported progress/issues/warnings, compile context and Working Memory
17
+ * source counts. v4.0.4: schema 8 records the renamed context vocabulary
18
+ * (`fresh_invocation`) in the derived declaration and stateHash. v4.0.5:
19
+ * schema 9 adds the optional `currentUserInstruction` projection; schema 10
20
+ * (v4.0.6) makes the objective the WORKING objective, falls back to it for
21
+ * the Current Task, and removes the Agent-reported test counts. The state
22
+ * file is regenerated every round; an older file is a stale view, not a
23
+ * legacy document to decode. */
24
+ export const CANONICAL_STATE_SCHEMA_VERSION = 10;
25
+ /** v4.0.5: the ONE wording for "the host supplied no instruction". Prompt and
26
+ * state file both render it; neither infers an instruction from the old task
27
+ * or transcript. */
28
+ export const CURRENT_USER_INSTRUCTION_NOT_PROVIDED = "not provided for this invocation (the Objective and Current Task remain operative).";
29
+ /** v4.0.5: the protected section's fixed authority note. The instruction is
30
+ * invocation input; it never edits the loop's objective or acceptance. */
31
+ export const CURRENT_USER_INSTRUCTION_AUTHORITY = "Host-provided current user instruction for this invocation. It does not " +
32
+ "rewrite the Objective, Success Criteria, hard constraints, or the Round " +
33
+ "Contract; a changed objective or acceptance requires a new loop.";
12
34
  /** v3.7.1: Presentation view of sub-goals shared by prompts and the state
13
35
  * file (one derivation, no second copy). Only ACTIVE items render as rows
14
36
  * — pending/in_progress/blocked — ordered blocked → in_progress → pending
@@ -73,10 +95,12 @@ export function formatCriterionRows(statuses, successCriteria) {
73
95
  continue;
74
96
  seen.add(id);
75
97
  ids.push(id);
76
- const related = status && status.related_subgoal_ids.length > 0
77
- ? ` (related: ${status.related_subgoal_ids.join(", ")})`
78
- : "";
79
- lines.push(`- ${criterionIcon(status?.status ?? "unknown")} [\`${id}\`] ${text}${related}`);
98
+ lines.push(`- ${criterionIcon(status?.status ?? "unknown")} [\`${id}\`] ${text}`);
99
+ }
100
+ if (lines.length > 0) {
101
+ // v4.0.3: the icons are meaningless without their provenance.
102
+ lines.push("> ✅ machine verified · 🟡 Agent claim · 🟠 insufficient · " +
103
+ "⛔ contradicted · ⬜ Agent-reported remaining · ❔ unknown");
80
104
  }
81
105
  return { lines, ids };
82
106
  }
@@ -85,8 +109,9 @@ export function formatCriterionRows(statuses, successCriteria) {
85
109
  * the overflow line lists every remaining `sg-*`. */
86
110
  export function formatActiveSubGoals(subGoals, round, cap = getPolicy().evolution.max_active_subgoals) {
87
111
  const view = activeSubGoalView([...subGoals], Number.POSITIVE_INFINITY);
88
- const shown = view.active.slice(0, Math.max(0, cap));
89
- const hidden = view.active.slice(shown.length);
112
+ const visible = view.active;
113
+ const shown = visible.slice(0, Math.max(0, cap));
114
+ const hidden = visible.slice(shown.length);
90
115
  const lines = [];
91
116
  for (const sg of shown) {
92
117
  const statusIcon = sg.status === "in_progress" ? "🔄" :
@@ -105,7 +130,19 @@ export function formatActiveSubGoals(subGoals, round, cap = getPolicy().evolutio
105
130
  }
106
131
  lines.push(`> ${view.total} total · ${view.activeTotal} active · ` +
107
132
  `${view.done} done · ${view.canceled} canceled`);
108
- return { lines, ids: view.active.map((sg) => sg.id) };
133
+ return { lines, ids: visible.map((sg) => sg.id) };
134
+ }
135
+ /**
136
+ * The active contract is the Current Task only when the runtime has a
137
+ * machine-verifiable contract capability and this invocation did not provide
138
+ * an explicit instruction. The contract remains a separately rendered
139
+ * committed fact in every other case; this helper keeps prompt and state-file
140
+ * projections on the same rule as state construction.
141
+ */
142
+ export function contractDrivesCurrentTask(state) {
143
+ return state.roundContract !== undefined &&
144
+ state.currentUserInstruction === undefined &&
145
+ state.capability.contractVerificationAvailable;
109
146
  }
110
147
  // unique() imported from token-utils.ts
111
148
  /** Deterministic JSON serialization used by state and prompt hashes. */
@@ -178,132 +215,132 @@ function addListWithIds(lines, title, values, prefix) {
178
215
  function addListWithIdentities(lines, title, values, metadata) {
179
216
  if (values.length === 0)
180
217
  return;
181
- const idByText = new Map(metadata.map((meta) => [meta.text, meta.id]));
218
+ const metaByText = new Map(metadata.map((meta) => [meta.text, meta]));
182
219
  lines.push(`## ${title}`, "");
183
220
  for (const value of values) {
184
221
  const trimmed = value.trim();
185
222
  if (!trimmed)
186
223
  continue;
224
+ const meta = metaByText.get(trimmed);
187
225
  if (STABLE_ID_RE.test(trimmed)) {
188
226
  lines.push(`- [\`${trimmed}\`] ${trimmed}`);
189
227
  }
190
228
  else {
191
- const id = idByText.get(trimmed) ?? `c-${deriveItemId(trimmed)}`;
192
- lines.push(`- [\`${id}\`] ${trimmed}`);
229
+ const id = meta?.id ?? `c-${deriveItemId(trimmed)}`;
230
+ const provenance = constraintProvenanceLabel(meta);
231
+ lines.push(`- [\`${id}\`] ${trimmed}${provenance}`);
193
232
  }
194
233
  }
195
234
  lines.push("");
196
235
  }
197
- /** v3.8.1: the one-line phase position — which phase the loop is in, and how
198
- * far the next boundary is.
199
- *
200
- * This is all that survives of v3.3's "roadmap": of its four lines, three
201
- * (met/remaining criteria with ids, sub-goal activity) were already rendered
202
- * in full by the sections that own them. Returns "" when no milestone exists,
203
- * so the caller renders no section at all — bare position is already in the
204
- * prompt header.
205
- *
206
- * Shared by the state-file and prompt renderers so they cannot drift apart
207
- * (module contract above). Presentation only; never feeds enforcement. */
208
- export function buildPhaseLine(state) {
209
- const lastMilestone = state.milestones.length > 0
210
- ? state.milestones[state.milestones.length - 1]
211
- : null;
212
- // No phase boundary yet → no phase to report. Position alone ("round 3/200")
213
- // is already in the prompt header, so rendering it again would be noise.
214
- if (!lastMilestone)
236
+ /** v3.9.7: the ONE constraint provenance label shared by the state file and
237
+ * the prompt. A discovered/preserved constraint is an Agent report, never a
238
+ * machine-verified fact. */
239
+ export function constraintProvenanceLabel(meta) {
240
+ if (!meta)
215
241
  return "";
216
- const roundsSince = lastMilestone.round_range.end <= state.round
217
- ? state.round - lastMilestone.round_range.end
218
- : null;
219
- const parts = [
220
- `phase "${lastMilestone.label}"`,
221
- `round ${state.round}/${state.maxRounds}`,
222
- ];
223
- if (roundsSince !== null) {
224
- parts.push(`${roundsSince} round${roundsSince === 1 ? "" : "s"} since the last boundary`);
242
+ const round = meta.source_round !== undefined && meta.source_round >= 1
243
+ ? ` R${meta.source_round}`
244
+ : "";
245
+ switch (meta.source) {
246
+ case "discovered":
247
+ return ` (Agent-declared${round}; report, not machine-verified)`;
248
+ case "preserved":
249
+ return ` (Agent discovery preserved by rollback${round}; report)`;
250
+ case "plan":
251
+ return ` (plan/carried${round})`;
252
+ case "unrecorded":
253
+ return " (origin not recorded in committed rounds)";
254
+ default:
255
+ return "";
225
256
  }
226
- return parts.join(" · ");
227
257
  }
228
- /** v3.3/v3.4: Render the ACTIVE Round Contract as the Current Task section
229
- * body — the single formatting source shared by prompts and the state
230
- * file (module contract above). The original objective is NOT here: it
231
- * lives in the Objective section. Empty arrays render no line. */
232
- export function formatRoundContract(contract,
258
+ export function formatRoundContractWithIds(contract,
233
259
  /** v3.8: the derived item statuses. When given, each item renders with its
234
260
  * machine status — the agent sees the verification debt in its own task. */
235
261
  statuses) {
236
262
  const lines = [];
263
+ const emittedIds = [contract.id];
237
264
  const heading = contract.work_item?.trim();
238
265
  lines.push(`**${heading || "Round Contract"}** (${contract.id})`);
239
266
  const statusById = new Map((statuses?.items ?? []).map((item) => [item.itemId, item]));
240
- for (const item of contract.items) {
267
+ const items = contract.items;
268
+ if (items.length > 0) {
269
+ lines.push("- Acceptance conditions:");
270
+ }
271
+ for (const item of items) {
272
+ emittedIds.push(item.id);
241
273
  const machine = statusById.get(item.id);
242
274
  const mark = machine?.status === "verified" ? "✅"
243
275
  : machine?.status === "insufficient" ? "🟠"
244
276
  : machine?.status === "contradicted" ? "⛔"
245
277
  : machine?.status === "pending" ? "⬜"
246
278
  : "";
247
- lines.push(`- ${mark ? `${mark} ` : ""}[\`${item.id}\`] ${item.description}${machine ? ` — ${machine.status}` : ""}`);
279
+ const status = machine ? ` — ${machine.status}` : "";
280
+ lines.push(` - ${mark ? `${mark} ` : ""}[\`${item.id}\`] ${item.description}${status}`);
248
281
  if (item.verify_with.length > 0) {
249
- lines.push(` - Verify via: ${item.verify_with.join(", ")}`);
282
+ lines.push(` - Verify via: ${item.verify_with.join(", ")}`);
250
283
  }
251
- if (item.criterion_refs.length > 0) {
252
- lines.push(` - Criteria: ${item.criterion_refs.join(", ")}`);
284
+ if ((item.criterion_refs ?? []).length > 0) {
285
+ lines.push(` - Criteria: ${(item.criterion_refs ?? []).join(", ")}`);
253
286
  }
254
287
  if (machine?.reasons.length) {
255
- lines.push(` - Unverified because: ${machine.reasons[0]}`);
288
+ lines.push(` - Unverified because: ${machine.reasons[0]}`);
256
289
  }
257
290
  }
258
- for (const entry of contract.scope) {
259
- lines.push(`- Scope: ${entry}`);
291
+ if (contract.scope.length > 0) {
292
+ // v3.9.9: scope is the Agent's declared intent, not a permission.
293
+ lines.push("- Declared scope (Agent report; not enforced):");
294
+ for (const entry of contract.scope) {
295
+ lines.push(` - ${entry}`);
296
+ }
260
297
  }
261
- return lines.join("\n");
298
+ return { text: lines.join("\n"), emittedIds };
262
299
  }
263
- // ═══════════════════════════════════════════════════════════════════════════
264
- // v3.3.1: Shared presentation atoms
265
- // ═══════════════════════════════════════════════════════════════════════════
266
- /** Milestone heading line ("**🏁 Round 7** (Rounds 3–7)"). Shared by the
267
- * L2 Phase History and the detailed L1 renderer; the L1 copy previously
268
- * rendered the range as "R3–R7", a format only this heading used. */
269
- export function milestoneHeading(milestone) {
270
- const kindIcon = milestone.kind === "agent_declared"
271
- ? "🏁"
272
- : milestone.kind === "criteria_milestone" ? "✅" : "📍";
273
- return `**${kindIcon} ${milestone.label}** (Rounds ${milestone.round_range.start}–${milestone.round_range.end})`;
300
+ /** Backwards-compatible text-only view used by the state file. */
301
+ export function formatRoundContract(contract, statuses) {
302
+ return formatRoundContractWithIds(contract, statuses).text;
274
303
  }
275
304
  const STATE_SECTION_TIER = {
276
305
  // Current — what the next round must hold in working memory.
277
306
  "Loop Objective": "Current",
278
307
  "Current Task": "Current",
308
+ "Active Round Contract": "Current",
309
+ "Current User Instruction": "Current",
279
310
  "Success Criteria": "Current",
280
311
  "Hard Constraints": "Current",
281
312
  "Active Constraints": "Current",
282
- Remaining: "Current",
283
- Blockers: "Current",
313
+ // v4.0.3: Agent-reported blockers and remaining claims are one report
314
+ // surface; compile warnings are a separate machine/compile surface.
315
+ "Agent-Reported Blockers": "Current",
316
+ "Agent-Reported Remaining Criteria": "Current",
317
+ "Agent-Reported Resolutions": "Recent",
284
318
  "Sub-Goal Dashboard": "Current",
285
- "External Context": "Current",
286
319
  // v3.8.1: the one-line phase position (the only fact the deleted Roadmap
287
320
  // section carried that no other section renders). It is orientation for the
288
321
  // NEXT round, so it belongs with the working-memory tier — and it must be
289
322
  // listed here at all: an unlisted title is not a heading, so `## Phase` was
290
323
  // absorbed into whatever section happened to precede it (its body even kept
291
324
  // a raw `## ` line) and inherited that section's tier.
292
- Phase: "Current",
293
325
  // Recent — this round's boundary facts (and, in a recovery window, the
294
326
  // derived Recovery Brief injected at the top of this tier).
295
327
  "Progress Dashboard": "Recent",
296
328
  Verification: "Recent",
297
- "Changes Since Last Round": "Recent",
298
- Discoveries: "Recent",
329
+ // v4.0.3: the retry/governance facts and machine evidence live next to the
330
+ // round boundary they describe.
331
+ "Retry Requirements": "Recent",
332
+ "Compile Warnings": "Recent",
333
+ "Machine Evidence": "Recent",
334
+ "Rejected Attempt Evidence": "Recent",
335
+ // v4.0.2: bounded committed working memory. It replaces the former
336
+ // Corrections/Discoveries sections; discovered constraints remain visible
337
+ // through active-constraint provenance.
338
+ "Working Memory": "Recent",
299
339
  // Historical Summary — the loop's memory skeleton, kept under existing
300
340
  // milestone/window caps.
301
341
  "Goal → Criteria": "Historical Summary",
302
- "Cross-Round Outcomes": "Historical Summary",
303
342
  "Recurring Flags": "Historical Summary",
304
- "Failed Patterns": "Historical Summary",
305
343
  "Retired Constraints": "Historical Summary",
306
- "Phase History": "Historical Summary",
307
344
  };
308
345
  const STATE_TIER_ORDER = ["Current", "Recent", "Historical Summary"];
309
346
  /** Human/Agent-readable materialized view. It is always reproducible from the
@@ -313,20 +350,34 @@ const STATE_TIER_ORDER = ["Current", "Recent", "Historical Summary"];
313
350
  * criterion list, round stats, the FULL recurring-flag history and the phase
314
351
  * history. The prompt carries only what the next round needs. */
315
352
  export function renderCanonicalStateMarkdown(state, options) {
353
+ const context = state.context ?? options?.context;
316
354
  const preamble = [
317
355
  `# LoopForge State — ${state.loopId}`,
318
356
  "",
319
357
  `**Schema**: ${state.schemaVersion}`,
320
358
  `**Round**: ${state.round}/${state.maxRounds}`,
321
359
  `**Goal ID**: ${state.goalId}`,
360
+ ...(state.currentUserInstruction
361
+ ? []
362
+ : [`**Current user instruction**: ${CURRENT_USER_INSTRUCTION_NOT_PROVIDED}`]),
363
+ ...(context
364
+ ? [
365
+ `**Context**: ${formatContextBoundaryLabel(context.mode, context.reason, { recoveryBoundary: (state.recoveryBrief?.length ?? 0) > 0 })}, attempt ${context.attempt}`,
366
+ ]
367
+ : []),
322
368
  "**Derived**: true",
323
- `**Source**: round ${state.round} (attempt ${options?.attempt ?? 1})`,
369
+ `**Source**: round ${state.round} (attempt ${context?.attempt ?? options?.attempt ?? 1})`,
370
+ "**Authority**: committed typed rounds are the factual source; this file is a derived view.",
371
+ "**Round commit**: a LoopForge round commit is not a Git commit; it does not verify every workspace change.",
372
+ "**Evidence**: machine evidence = providers, command observations and verified contract items. Agent reports = outcome/files/claims.",
373
+ "**Working memory**: bounded; it may be abbreviated; full history via replay/explain or this file when enabled.",
324
374
  `**State hash**: ${hashCanonicalState(state).slice(0, 12)}`,
325
375
  "",
326
376
  ];
327
377
  const sections = flatStateSections(state);
328
- if (options?.recoveryBrief && options.recoveryBrief.length > 0) {
329
- sections.unshift({ title: "Recovery Brief", body: options.recoveryBrief });
378
+ const recoveryBrief = state.recoveryBrief ?? options?.recoveryBrief;
379
+ if (recoveryBrief && recoveryBrief.length > 0) {
380
+ sections.unshift({ title: "Recovery Brief", body: recoveryBrief });
330
381
  }
331
382
  const byTier = new Map();
332
383
  for (const tier of STATE_TIER_ORDER)
@@ -356,6 +407,16 @@ export function renderCanonicalStateMarkdown(state, options) {
356
407
  function flatStateSections(state) {
357
408
  const lines = [];
358
409
  lines.push("## Loop Objective", "", state.objective, "", "## Current Task", "", state.currentTask);
410
+ if (state.roundContract && !contractDrivesCurrentTask(state)) {
411
+ lines.push("## Active Round Contract", "", formatRoundContract(state.roundContract, state.contractItemStatuses ?? null));
412
+ }
413
+ // The host's invocation instruction renders as its own section only when
414
+ // it is not already visible in Current Task or Objective.
415
+ if (state.currentUserInstruction &&
416
+ state.currentUserInstruction !== state.currentTask.trim() &&
417
+ state.currentUserInstruction !== state.objective.trim()) {
418
+ lines.push("## Current User Instruction", "", state.currentUserInstruction, "", `> ${CURRENT_USER_INSTRUCTION_AUTHORITY}`);
419
+ }
359
420
  // v2.11: Success Criteria and Constraints render with stable IDs for exact
360
421
  // agent matching. v3.9.4: the three groups are the SAME mutually exclusive
361
422
  // partition the prompt uses, so one text never appears with two prefixes.
@@ -363,38 +424,61 @@ function flatStateSections(state) {
363
424
  addListWithIds(lines, "Success Criteria", constraintPartition.criteria, "cr");
364
425
  addListWithIdentities(lines, "Hard Constraints", constraintPartition.hard, state.constraintMetadata);
365
426
  addListWithIdentities(lines, "Active Constraints", constraintPartition.other, state.constraintMetadata);
366
- addList(lines, "Changes Since Last Round", state.changesSinceLastRound);
367
- if (state.progress.criteriaMet.length > 0 ||
368
- state.progress.criteriaRemaining.length > 0 ||
369
- state.progress.filesChanged.length > 0 ||
370
- state.progress.tests !== null ||
371
- state.machineStatus !== undefined) {
372
- const total = state.progress.criteriaMet.length + state.progress.criteriaRemaining.length;
427
+ if (state.rejectionNotice) {
428
+ lines.push("## Retry Requirements", "", state.rejectionNotice.trim(), "");
429
+ }
430
+ addList(lines, "Compile Warnings", state.warnings);
431
+ if (state.previousEvidencePosture) {
432
+ lines.push("## Machine Evidence", "", ...formatEvidencePostureLines(state.previousEvidencePosture), "", `> ${MACHINE_EVIDENCE_AUTHORITY}`, "");
433
+ }
434
+ if (state.retryEvidencePosture) {
435
+ lines.push("## Rejected Attempt Evidence", "", "> Not committed; it exists only for the current retry.", "", ...formatEvidencePostureLines(state.retryEvidencePosture), "");
436
+ }
437
+ if (hasWorkingMemoryContent(state.workingMemory)) {
438
+ const working = formatWorkingMemoryState(state.workingMemory);
439
+ lines.push("## Working Memory", "", ...working.lines, "");
440
+ }
441
+ const reported = state.reportedProgress;
442
+ const posture = state.previousEvidencePosture;
443
+ if (reported.criteriaMet.length > 0 ||
444
+ reported.criteriaRemaining.length > 0 ||
445
+ reported.filesChanged.length > 0 ||
446
+ state.machineStatus !== undefined ||
447
+ posture !== undefined) {
448
+ const total = reported.criteriaMet.length + reported.criteriaRemaining.length;
373
449
  lines.push("## Progress Dashboard", "");
374
450
  if (total > 0) {
375
- lines.push(`**Criteria**: ${state.progress.criteriaMet.length}/${total}`);
451
+ lines.push(`**Agent-reported criteria**: ${reported.criteriaMet.length}/${total}`);
452
+ }
453
+ if (state.criterionStatuses.length > 0) {
454
+ const verifiedCount = state.criterionStatuses.filter((criterion) => criterion.status === "verified").length;
455
+ lines.push(`**Machine-verified criteria**: ${verifiedCount}/${state.criterionStatuses.length}`);
456
+ }
457
+ if (reported.filesChanged.length > 0) {
458
+ lines.push("", "**Agent-reported files**:");
459
+ for (const file of reported.filesChanged)
460
+ lines.push(`- ${file}`);
461
+ }
462
+ if (posture) {
463
+ const observed = posture.workspace.status === "observed"
464
+ ? `${posture.workspace.changedPathCount}`
465
+ : posture.workspace.status === "unknown" ? "unknown" : "not applicable";
466
+ lines.push(`**Observed changes (last committed round)**: ${observed}`);
467
+ lines.push(`**Files reconciliation**: ${posture.files?.reconciliation ?? "not_applicable"}`);
376
468
  }
377
469
  // v3.3: machine side of the comparison — git motion over committed rounds
378
470
  // (v3.6: the object always carries definite values — no "unavailable" arm).
379
471
  if (state.machineStatus) {
380
472
  const ms = state.machineStatus;
381
- const motion = ms.gitMotion
382
- ? `changes observed in ${ms.motionRounds}/${ms.windowRounds} recent committed rounds`
383
- : `no git changes in the last ${ms.windowRounds} committed rounds`;
384
- lines.push(`**Machine (git)**: ${motion}`);
385
- }
386
- if (state.criterionStatuses.length > 0) {
387
- const metCount = state.criterionStatuses.filter((cs) => cs.status === "verified" || cs.status === "claimed").length;
388
- lines.push(`**Machine (criteria)**: ${metCount}/${state.criterionStatuses.length} met across committed rounds`);
389
- }
390
- if (state.progress.tests) {
391
- lines.push(`**Tests**: ${state.progress.tests.passed} passed, ` +
392
- `${state.progress.tests.failed} failed, ${state.progress.tests.skipped} skipped`);
393
- }
394
- if (state.progress.filesChanged.length > 0) {
395
- lines.push("", "**Files Changed**:");
396
- for (const file of state.progress.filesChanged)
397
- lines.push(`- ${file}`);
473
+ const providers = ms.providers.length > 0
474
+ ? ` (providers: ${ms.providers.join(", ")})`
475
+ : "";
476
+ const progress = ms.machineProgress === "changed"
477
+ ? `changes observed in ${ms.changedRounds}/${ms.windowRounds} recent committed rounds${providers}`
478
+ : ms.machineProgress === "unknown"
479
+ ? `not observable in the last ${ms.windowRounds} committed rounds${providers}`
480
+ : `no machine-observed progress in the last ${ms.windowRounds} committed rounds${providers}`;
481
+ lines.push(`**Machine progress**: ${progress}`);
398
482
  }
399
483
  lines.push("");
400
484
  }
@@ -408,20 +492,35 @@ function flatStateSections(state) {
408
492
  : cs.status === "contradicted" ? "⛔"
409
493
  : cs.status === "remaining" ? "⬜" : "❔";
410
494
  const idTag = ` [\`${cs.id}\`]`;
411
- const met = cs.met_at_round !== undefined ? ` (met R${cs.met_at_round})` : "";
412
- const related = cs.related_subgoal_ids.length > 0
413
- ? ` (related: ${cs.related_subgoal_ids.join(", ")})`
414
- : "";
415
- lines.push(`- ${icon}${idTag} ${cs.text}${met}${related}`);
495
+ // v4.0.3: the round and the provenance are separate facts. A claimed
496
+ // row names the latest Agent claim; a verified/contradicted/insufficient
497
+ // row names the machine round that reached it.
498
+ const at = cs.status_at_round !== undefined ? ` R${cs.status_at_round}` : "";
499
+ const label = cs.status === "verified" ? `machine verified${at}`
500
+ : cs.status === "claimed" ? `agent claim${at}`
501
+ : cs.status === "insufficient" ? `machine insufficient${at}`
502
+ : cs.status === "contradicted" ? `machine contradicted${at}`
503
+ : cs.status === "remaining" ? `agent reported remaining${at}`
504
+ : "unknown";
505
+ lines.push(`- ${icon}${idTag} ${cs.text} (${label})`);
506
+ }
507
+ lines.push("");
508
+ }
509
+ if (reported.agentReportedRemaining.length > 0) {
510
+ addListWithIds(lines, "Agent-Reported Remaining Criteria", reported.agentReportedRemaining, "cr");
511
+ }
512
+ addList(lines, "Agent-Reported Blockers", state.reportedIssues);
513
+ if (state.resolvedOpenQuestionIds && state.resolvedOpenQuestionIds.length > 0) {
514
+ const shown = state.resolvedOpenQuestionIds.slice(0, 6);
515
+ lines.push("## Agent-Reported Resolutions", "", `- ${state.resolvedOpenQuestionIds.length} open question(s) resolved in the ` +
516
+ "last committed round (Agent report):", ...shown.map((id) => ` - \`${id}\``));
517
+ if (state.resolvedOpenQuestionIds.length > shown.length) {
518
+ lines.push(` - ... and ${state.resolvedOpenQuestionIds.length - shown.length} more ` +
519
+ "(replay/explain shows every id)");
416
520
  }
417
521
  lines.push("");
418
522
  }
419
- addListWithIds(lines, "Remaining", state.remainingCriteria, "cr");
420
- addList(lines, "Blockers", state.blockers);
421
523
  const recurring = recurringOnly(state.recurringFlags);
422
- addList(lines, "Discoveries", state.discoveries);
423
- addList(lines, "Cross-Round Outcomes", state.rollingOutcomes);
424
- addList(lines, "Failed Patterns", state.failedPatterns);
425
524
  // v3.8.1: ONE recurring-facts section. "Recurring Issues" (the last 5
426
525
  // rounds' raw violations, no threshold) and "Lessons Learned" (the whole
427
526
  // history, count >= 2) were two windows over one fact set rendered as two
@@ -434,19 +533,21 @@ function flatStateSections(state) {
434
533
  ? "🚫"
435
534
  : flag.kind === "verification_error" ? "⚠️" : "ℹ️";
436
535
  const subject = flag.ref ? `${flag.subject} [${flag.ref}]` : flag.subject;
437
- lines.push(`- ${icon} ${subject} — ${flag.count}× (R${flag.rounds.join(", R")})`);
536
+ const source = flag.kind === "constraint_violation"
537
+ ? "Agent-reported violation"
538
+ : "machine finding";
539
+ lines.push(`- ${icon} ${subject} — ${flag.count}× (R${flag.rounds.join(", R")}) — ${source}`);
438
540
  }
439
541
  if (recurring.length > RECURRING_RENDER_CAP) {
440
542
  lines.push(`- ... and ${recurring.length - RECURRING_RENDER_CAP} more`);
441
543
  }
442
544
  lines.push("");
443
545
  }
444
- addList(lines, "Retired Constraints", state.retiredConstraints);
445
- // v3.8.1: the Roadmap section is gone. Only its non-duplicated fact — the
446
- // phase position — survives, as the one-line phase line the prompts render.
447
- const phaseLine = buildPhaseLine(state);
448
- if (phaseLine) {
449
- lines.push("## Phase", "", `- ${phaseLine}`);
546
+ if (state.retiredConstraints.length > 0) {
547
+ lines.push("## Retired Constraints", "");
548
+ for (const constraint of state.retiredConstraints) {
549
+ lines.push(`- ${constraint} (Agent report)`);
550
+ }
450
551
  lines.push("");
451
552
  }
452
553
  if (state.verificationFlags.length > 0) {
@@ -457,27 +558,6 @@ function flatStateSections(state) {
457
558
  }
458
559
  lines.push("");
459
560
  }
460
- if (state.milestones.length > 0) {
461
- lines.push("## Phase History", "");
462
- for (const m of state.milestones) {
463
- const kindIcon = m.kind === "agent_declared"
464
- ? "🏁"
465
- : m.kind === "criteria_milestone" ? "✅" : "📍";
466
- lines.push(`### ${kindIcon} ${m.label}`, `Rounds ${m.round_range.start}–${m.round_range.end}`, "", `**Outcome**: ${m.outcome}`, "");
467
- if (m.carried_constraints.length > 0) {
468
- lines.push("**Carried Constraints**:");
469
- for (const c of m.carried_constraints)
470
- lines.push(`- ${c}`);
471
- lines.push("");
472
- }
473
- if (m.resolved_constraints.length > 0) {
474
- lines.push("**Resolved During Phase**:");
475
- for (const c of m.resolved_constraints)
476
- lines.push(`- ${c}`);
477
- lines.push("");
478
- }
479
- }
480
- }
481
561
  if (state.subGoals.length > 0) {
482
562
  // v3.9.4: the SAME formatter the prompt uses. Rows past the display cap
483
563
  // keep their ids in an overflow line, so the state file's active id set
@@ -488,9 +568,6 @@ function flatStateSections(state) {
488
568
  lines.push("## Sub-Goal Dashboard", "");
489
569
  lines.push(...formatActiveSubGoals(state.subGoals, state.round).lines, "");
490
570
  }
491
- if (state.externalContext) {
492
- lines.push("## External Context", "", state.externalContext, "");
493
- }
494
571
  // Split the flat markdown into titled sections for tier grouping. Every
495
572
  // builder above emits `## <Title>` headings over their content — but only
496
573
  // the KNOWN section titles are headings. Free-text bodies (objective,
@@ -532,33 +609,45 @@ export function createCanonicalLoopState(request, response, stateFilePath,
532
609
  /** v3.3: Display-only derived data. Optional 4th param — callers that
533
610
  * predate v3.3 stay on 3-arg calls. */
534
611
  derived) {
535
- const last = derived?.previousRound?.result ?? request.last_round_result;
612
+ const last = derived?.previousRound?.result ?? null;
536
613
  const objective = response.loop_objective;
537
614
  const verificationFlags = derived?.previousRound?.verificationFlags ??
538
615
  request.verification_flags ?? [];
539
- const rolling = response.rolling_summary;
540
- const executionReport = last?.execution_report;
541
- const changes = unique([
542
- request.new_since_last_round,
543
- last?.output_summary,
544
- ...(last?.wrong_assumptions ?? []).map((value) => `Corrected assumption: ${value}`),
545
- ]);
546
- const discoveries = unique([
547
- ...(last?.discovered_constraints ?? []),
548
- ...(last?.emerged_subtasks ?? []),
616
+ // v3.9.5: only committed previous-round reports enter the projection. The
617
+ // compiler no longer has an uncommitted fallback at all.
618
+ const committed = derived?.previousRound?.committed === true;
619
+ // Keep the report projection behind the same boundary as corrections and
620
+ // blockers. The fallback result may still be consumed by compiler
621
+ // evolution, but an uncommitted/rejected attempt cannot become durable
622
+ // progress, discovery, or criterion state.
623
+ const executionReport = committed ? last?.execution_report : undefined;
624
+ // v4.0.3: blockers are Agent reports; warnings are compiler facts. They
625
+ // were previously merged into one `blockers` list.
626
+ const reportedIssues = unique([
627
+ ...(committed ? (last?.constraint_violations ?? []) : []),
628
+ ...(committed && last?.blocker ? [last.blocker] : []),
549
629
  ]);
550
- const blockers = unique([
551
- ...(last?.constraint_violations ?? []),
552
- last?.manual_fixes_needed,
553
- request.rejection_notice,
554
- ...response.warnings,
555
- ]);
556
- // v3.4: The ACTIVE Round Contract (derived — loop-compiler computes it
557
- // from committed rounds) drives the Current Task — the contract IS "what
558
- // to do this round"; the original objective stays in the Objective
559
- // section above. No active contract → the original task text is used,
560
- // byte-identical to pre-v3.3 behavior.
630
+ const warnings = unique(response.warnings);
631
+ const committedPosture = committed ? derived?.previousRound?.view?.evidencePosture ?? null : null;
632
+ const retryPosture = request.retry_evidence_posture ?? null;
633
+ // v4.0.3: the Agent's resolved-question report is bounded and rendered as a
634
+ // separate provenance line; it never silently disappears into Working
635
+ // Memory's active-set filtering.
636
+ const resolvedOpenQuestionIds = committed
637
+ ? unique(last?.resolved_open_question_ids ?? [])
638
+ .filter(isOpenQuestionId)
639
+ : [];
640
+ // v4.0.6: The Agent still chooses when a round ends. The ACTIVE Round
641
+ // Contract (derived from committed rounds) supplies the verifiable work
642
+ // slice for this invocation when verification capability exists. An
643
+ // explicit invocation instruction has priority; without contract capability
644
+ // the Current Task falls back to the working objective.
561
645
  const roundContract = derived?.roundFacts?.activeContract ?? null;
646
+ const capability = deriveConfiguredCapability(getPolicy());
647
+ // v4.0.5: the host's explicit invocation input. Empty/whitespace is "not
648
+ // provided", never a second task. Conditional presence keeps the state hash
649
+ // byte-identical when no instruction was given.
650
+ const currentUserInstruction = request.current_user_instruction?.trim() || undefined;
562
651
  // v3.9.4: the rendered criterion/hard sets are ACTIVE sets. The compile
563
652
  // response's `criterion_statuses` is the active criterion reference space
564
653
  // (loop-compiler filters it); `constraints_active` is the active constraint
@@ -584,47 +673,52 @@ derived) {
584
673
  maxRounds: request.max_rounds ?? getPolicy().engine.max_rounds,
585
674
  goalId: response.goal_id,
586
675
  objective: objective?.objective || request.task,
587
- objectiveVersion: objective?.version ?? 1,
588
- currentTask: roundContract
676
+ currentTask: currentUserInstruction ?? (roundContract && capability.contractVerificationAvailable
589
677
  ? formatRoundContract(roundContract, derived?.roundFacts?.itemStatuses ?? null)
590
- : request.task,
678
+ : objective?.objective ?? request.task),
679
+ ...(currentUserInstruction ? { currentUserInstruction } : {}),
591
680
  successCriteria,
592
681
  hardConstraints,
593
682
  activeConstraints,
594
683
  retiredConstraints: unique(response.constraints_retired),
595
684
  constraintMetadata: response.constraint_metadata ?? [],
596
- changesSinceLastRound: changes,
597
- remainingCriteria: unique(claimedRemainingCriteria(last?.execution_report)),
598
- blockers,
685
+ rejectionNotice: request.rejection_notice.trim() ? request.rejection_notice : null,
686
+ // v4.0.3: conditional presence keeps hashes stable for a first round.
687
+ ...(committedPosture ? { previousEvidencePosture: committedPosture } : {}),
688
+ ...(retryPosture ? { retryEvidencePosture: retryPosture } : {}),
689
+ reportedIssues,
690
+ warnings,
599
691
  verificationFlags,
600
- discoveries,
601
- rollingOutcomes: unique(rolling?.key_outcomes ?? []),
602
- failedPatterns: unique(rolling?.failed_patterns ?? []),
603
- milestones: rolling?.milestones ?? [],
692
+ // v4.0.2: conditional presence — no committed working memory, no key.
693
+ ...(hasWorkingMemoryContent(derived?.workingMemory)
694
+ ? { workingMemory: derived.workingMemory }
695
+ : {}),
604
696
  subGoals: response.sub_goals ?? [],
605
697
  criterionStatuses: response.criterion_statuses ?? [],
606
698
  recurringFlags: response.recurring_flags ?? [],
607
- externalContext: request.external_context?.trim() ?? "",
608
699
  stateFilePath,
609
- progress: {
700
+ reportedProgress: {
610
701
  criteriaMet: unique(claimedMetCriteria(executionReport)),
611
702
  criteriaRemaining: unique(claimedRemainingCriteria(executionReport)),
703
+ agentReportedRemaining: unique(claimedRemainingCriteria(executionReport)),
612
704
  filesChanged: unique(executionReport?.files_changed ?? []),
613
- tests: executionReport?.tests_reported ?? null,
614
705
  },
706
+ ...(resolvedOpenQuestionIds.length > 0
707
+ ? { resolvedOpenQuestionIds }
708
+ : {}),
709
+ ...(derived?.context ? { context: derived.context } : {}),
615
710
  // v3.3: Conditional presence — empty/absent derived data must not add
616
711
  // keys, or every state hash would change for rounds without it.
712
+ ...(derived?.recoveryBrief && derived.recoveryBrief.length > 0
713
+ ? { recoveryBrief: [...derived.recoveryBrief] }
714
+ : {}),
617
715
  ...(derived?.machineStatus ? { machineStatus: derived.machineStatus } : {}),
618
716
  // v3.3: Conditional presence — no contract, no key (hash stability).
619
717
  ...(roundContract ? { roundContract } : {}),
620
718
  ...(derived?.roundFacts?.itemStatuses
621
719
  ? { contractItemStatuses: derived.roundFacts.itemStatuses }
622
720
  : {}),
623
- ...(derived?.roundFacts?.verifiedSubGoals &&
624
- derived.roundFacts.verifiedSubGoals.length > 0
625
- ? { verifiedSubGoals: [...derived.roundFacts.verifiedSubGoals] }
626
- : {}),
627
- capability: deriveConfiguredCapability(getPolicy()),
721
+ capability,
628
722
  };
629
723
  }
630
724
  //# sourceMappingURL=canonical-state.js.map