loopforge 3.9.2 → 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 (285) 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 +152 -40
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +382 -206
  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 +105 -141
  18. package/dist/committed-round.d.ts.map +1 -1
  19. package/dist/committed-round.js +249 -372
  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 +32 -67
  42. package/dist/enforcement-gate.d.ts.map +1 -1
  43. package/dist/enforcement-gate.js +203 -439
  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 +28 -887
  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 +167 -27
  70. package/dist/explain.d.ts.map +1 -1
  71. package/dist/explain.js +373 -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 +51 -19
  103. package/dist/loop-compiler.d.ts.map +1 -1
  104. package/dist/loop-compiler.js +423 -507
  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 +285 -81
  111. package/dist/mcp/round-lifecycle.d.ts.map +1 -1
  112. package/dist/mcp/round-lifecycle.js +1629 -574
  113. package/dist/mcp/round-lifecycle.js.map +1 -1
  114. package/dist/mcp/server.d.ts +5 -0
  115. package/dist/mcp/server.d.ts.map +1 -1
  116. package/dist/mcp/server.js +61 -19
  117. package/dist/mcp/server.js.map +1 -1
  118. package/dist/mcp/session.d.ts +81 -39
  119. package/dist/mcp/session.d.ts.map +1 -1
  120. package/dist/mcp/session.js +963 -271
  121. package/dist/mcp/session.js.map +1 -1
  122. package/dist/mcp/submission-boundary.d.ts +23 -31
  123. package/dist/mcp/submission-boundary.d.ts.map +1 -1
  124. package/dist/mcp/submission-boundary.js +242 -82
  125. package/dist/mcp/submission-boundary.js.map +1 -1
  126. package/dist/mcp/submission-handle.d.ts +66 -0
  127. package/dist/mcp/submission-handle.d.ts.map +1 -0
  128. package/dist/mcp/submission-handle.js +173 -0
  129. package/dist/mcp/submission-handle.js.map +1 -0
  130. package/dist/mcp/tools.d.ts +17 -7
  131. package/dist/mcp/tools.d.ts.map +1 -1
  132. package/dist/mcp/tools.js +763 -244
  133. package/dist/mcp/tools.js.map +1 -1
  134. package/dist/open-question.d.ts +7 -0
  135. package/dist/open-question.d.ts.map +1 -0
  136. package/dist/open-question.js +14 -0
  137. package/dist/open-question.js.map +1 -0
  138. package/dist/policy-metrics.d.ts +31 -21
  139. package/dist/policy-metrics.d.ts.map +1 -1
  140. package/dist/policy-metrics.js +117 -64
  141. package/dist/policy-metrics.js.map +1 -1
  142. package/dist/policy.d.ts +15 -40
  143. package/dist/policy.d.ts.map +1 -1
  144. package/dist/policy.js +13 -13
  145. package/dist/policy.js.map +1 -1
  146. package/dist/progress.d.ts +45 -0
  147. package/dist/progress.d.ts.map +1 -0
  148. package/dist/progress.js +136 -0
  149. package/dist/progress.js.map +1 -0
  150. package/dist/prompt-assembler.d.ts +28 -9
  151. package/dist/prompt-assembler.d.ts.map +1 -1
  152. package/dist/prompt-assembler.js +592 -603
  153. package/dist/prompt-assembler.js.map +1 -1
  154. package/dist/prompt-coverage.d.ts +39 -0
  155. package/dist/prompt-coverage.d.ts.map +1 -0
  156. package/dist/prompt-coverage.js +80 -0
  157. package/dist/prompt-coverage.js.map +1 -0
  158. package/dist/prompt-freshness.d.ts +25 -0
  159. package/dist/prompt-freshness.d.ts.map +1 -0
  160. package/dist/prompt-freshness.js +88 -0
  161. package/dist/prompt-freshness.js.map +1 -0
  162. package/dist/prompt-policy.d.ts +14 -4
  163. package/dist/prompt-policy.d.ts.map +1 -1
  164. package/dist/prompt-policy.js +29 -10
  165. package/dist/prompt-policy.js.map +1 -1
  166. package/dist/protocol.d.ts +875 -419
  167. package/dist/protocol.d.ts.map +1 -1
  168. package/dist/protocol.js +73 -99
  169. package/dist/protocol.js.map +1 -1
  170. package/dist/recovery-guidance.d.ts +13 -0
  171. package/dist/recovery-guidance.d.ts.map +1 -0
  172. package/dist/recovery-guidance.js +15 -0
  173. package/dist/recovery-guidance.js.map +1 -0
  174. package/dist/recovery.d.ts +68 -0
  175. package/dist/recovery.d.ts.map +1 -0
  176. package/dist/recovery.js +234 -0
  177. package/dist/recovery.js.map +1 -0
  178. package/dist/replay.d.ts +42 -8
  179. package/dist/replay.d.ts.map +1 -1
  180. package/dist/replay.js +311 -84
  181. package/dist/replay.js.map +1 -1
  182. package/dist/result-facts.d.ts +45 -0
  183. package/dist/result-facts.d.ts.map +1 -0
  184. package/dist/result-facts.js +86 -0
  185. package/dist/result-facts.js.map +1 -0
  186. package/dist/round-contract.d.ts +38 -8
  187. package/dist/round-contract.d.ts.map +1 -1
  188. package/dist/round-contract.js +71 -31
  189. package/dist/round-contract.js.map +1 -1
  190. package/dist/round-coordinator.d.ts +45 -91
  191. package/dist/round-coordinator.d.ts.map +1 -1
  192. package/dist/round-coordinator.js +254 -271
  193. package/dist/round-coordinator.js.map +1 -1
  194. package/dist/round-driver.d.ts +56 -16
  195. package/dist/round-driver.d.ts.map +1 -1
  196. package/dist/round-driver.js +225 -39
  197. package/dist/round-driver.js.map +1 -1
  198. package/dist/round-facts.d.ts +33 -52
  199. package/dist/round-facts.d.ts.map +1 -1
  200. package/dist/round-facts.js +54 -157
  201. package/dist/round-facts.js.map +1 -1
  202. package/dist/round-guidance.d.ts +56 -0
  203. package/dist/round-guidance.d.ts.map +1 -0
  204. package/dist/round-guidance.js +161 -0
  205. package/dist/round-guidance.js.map +1 -0
  206. package/dist/round-outcome.d.ts +21 -0
  207. package/dist/round-outcome.d.ts.map +1 -0
  208. package/dist/round-outcome.js +31 -0
  209. package/dist/round-outcome.js.map +1 -0
  210. package/dist/round-transaction.d.ts +168 -21
  211. package/dist/round-transaction.d.ts.map +1 -1
  212. package/dist/round-transaction.js +856 -39
  213. package/dist/round-transaction.js.map +1 -1
  214. package/dist/self-eval.d.ts +40 -60
  215. package/dist/self-eval.d.ts.map +1 -1
  216. package/dist/self-eval.js +455 -384
  217. package/dist/self-eval.js.map +1 -1
  218. package/dist/storage.d.ts +16 -0
  219. package/dist/storage.d.ts.map +1 -1
  220. package/dist/storage.js +54 -4
  221. package/dist/storage.js.map +1 -1
  222. package/dist/subgoal-state.d.ts +2 -3
  223. package/dist/subgoal-state.d.ts.map +1 -1
  224. package/dist/subgoal-state.js +2 -3
  225. package/dist/subgoal-state.js.map +1 -1
  226. package/dist/submission-guidance.d.ts +43 -21
  227. package/dist/submission-guidance.d.ts.map +1 -1
  228. package/dist/submission-guidance.js +368 -262
  229. package/dist/submission-guidance.js.map +1 -1
  230. package/dist/submission-receipt.d.ts +16 -8
  231. package/dist/submission-receipt.d.ts.map +1 -1
  232. package/dist/submission-receipt.js +146 -51
  233. package/dist/submission-receipt.js.map +1 -1
  234. package/dist/token-utils.d.ts +47 -7
  235. package/dist/token-utils.d.ts.map +1 -1
  236. package/dist/token-utils.js +66 -15
  237. package/dist/token-utils.js.map +1 -1
  238. package/dist/transaction-identity.d.ts +9 -0
  239. package/dist/transaction-identity.d.ts.map +1 -0
  240. package/dist/transaction-identity.js +69 -0
  241. package/dist/transaction-identity.js.map +1 -0
  242. package/dist/verification-actions.d.ts +13 -0
  243. package/dist/verification-actions.d.ts.map +1 -0
  244. package/dist/verification-actions.js +44 -0
  245. package/dist/verification-actions.js.map +1 -0
  246. package/dist/verification-gate.d.ts +26 -46
  247. package/dist/verification-gate.d.ts.map +1 -1
  248. package/dist/verification-gate.js +218 -427
  249. package/dist/verification-gate.js.map +1 -1
  250. package/dist/version.d.ts +1 -1
  251. package/dist/version.js +1 -1
  252. package/dist/working-memory.d.ts +187 -0
  253. package/dist/working-memory.d.ts.map +1 -0
  254. package/dist/working-memory.js +767 -0
  255. package/dist/working-memory.js.map +1 -0
  256. package/dist/workspace-checkpoint.d.ts +88 -0
  257. package/dist/workspace-checkpoint.d.ts.map +1 -0
  258. package/dist/workspace-checkpoint.js +326 -0
  259. package/dist/workspace-checkpoint.js.map +1 -0
  260. package/dist/workspace-restore.d.ts +68 -0
  261. package/dist/workspace-restore.d.ts.map +1 -0
  262. package/dist/workspace-restore.js +604 -0
  263. package/dist/workspace-restore.js.map +1 -0
  264. package/dist/workspace.d.ts +0 -9
  265. package/dist/workspace.d.ts.map +1 -1
  266. package/dist/workspace.js +0 -17
  267. package/dist/workspace.js.map +1 -1
  268. package/loop_policy.json +5 -19
  269. package/package.json +2 -2
  270. package/skills/forge/SKILL.md +212 -0
  271. package/skills/forge/references/evidence-and-contracts.md +204 -0
  272. package/skills/forge/references/recovery-and-views.md +188 -0
  273. package/dist/cognitive-facts.d.ts +0 -30
  274. package/dist/cognitive-facts.d.ts.map +0 -1
  275. package/dist/cognitive-facts.js +0 -113
  276. package/dist/cognitive-facts.js.map +0 -1
  277. package/dist/loop-extras-parser.d.ts +0 -81
  278. package/dist/loop-extras-parser.d.ts.map +0 -1
  279. package/dist/loop-extras-parser.js +0 -213
  280. package/dist/loop-extras-parser.js.map +0 -1
  281. package/dist/loop-projection.d.ts +0 -6
  282. package/dist/loop-projection.d.ts.map +0 -1
  283. package/dist/loop-projection.js +0 -28
  284. package/dist/loop-projection.js.map +0 -1
  285. package/skills/perception/SKILL.md +0 -370
@@ -4,11 +4,33 @@
4
4
  * consume the same value so they cannot silently drift apart.
5
5
  */
6
6
  import { createHash } from "node:crypto";
7
- import { deriveItemId, STABLE_ID_RE, unique } from "./token-utils.js";
7
+ import { deriveCriterionId, deriveItemId, normalizeText, STABLE_ID_RE, unique, uniqueNormalized, } from "./token-utils.js";
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
@@ -31,6 +53,97 @@ export function activeSubGoalView(subGoals, cap) {
31
53
  total: subGoals.length,
32
54
  };
33
55
  }
56
+ export function partitionActiveConstraints(state) {
57
+ const criteriaKeys = new Set(state.successCriteria.map((text) => normalizeText(text)));
58
+ const hardKeys = new Set(state.hardConstraints.map((text) => normalizeText(text)));
59
+ const hard = [];
60
+ const other = [];
61
+ for (const text of state.activeConstraints) {
62
+ const key = normalizeText(text);
63
+ if (criteriaKeys.has(key))
64
+ continue;
65
+ if (hardKeys.has(key))
66
+ hard.push(text);
67
+ else
68
+ other.push(text);
69
+ }
70
+ return { criteria: [...state.successCriteria], hard, other };
71
+ }
72
+ function criterionIcon(status) {
73
+ switch (status) {
74
+ case "verified": return "✅";
75
+ case "claimed": return "🟡";
76
+ case "insufficient": return "🟠";
77
+ case "contradicted": return "⛔";
78
+ case "remaining": return "⬜";
79
+ default: return "❔";
80
+ }
81
+ }
82
+ /** v3.9.4: the ONE criterion row renderer for the normative prompt section.
83
+ * It consumes the same `successCriteria`/`criterionStatuses` pair the state
84
+ * file and submission boundary use, and reports the exact ids it emitted so
85
+ * prompt coverage can prove them. */
86
+ export function formatCriterionRows(statuses, successCriteria) {
87
+ const byText = new Map(statuses.map((status) => [normalizeText(status.text), status]));
88
+ const seen = new Set();
89
+ const lines = [];
90
+ const ids = [];
91
+ for (const text of successCriteria) {
92
+ const status = byText.get(normalizeText(text));
93
+ const id = status?.id ?? deriveCriterionId(text);
94
+ if (seen.has(id))
95
+ continue;
96
+ seen.add(id);
97
+ ids.push(id);
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");
104
+ }
105
+ return { lines, ids };
106
+ }
107
+ /** v3.9.4: the ONE active-sub-goal formatter shared by the prompt and the
108
+ * state file. Rows past `cap` lose their descriptions but NEVER their ids:
109
+ * the overflow line lists every remaining `sg-*`. */
110
+ export function formatActiveSubGoals(subGoals, round, cap = getPolicy().evolution.max_active_subgoals) {
111
+ const view = activeSubGoalView([...subGoals], Number.POSITIVE_INFINITY);
112
+ const visible = view.active;
113
+ const shown = visible.slice(0, Math.max(0, cap));
114
+ const hidden = visible.slice(shown.length);
115
+ const lines = [];
116
+ for (const sg of shown) {
117
+ const statusIcon = sg.status === "in_progress" ? "🔄" :
118
+ sg.status === "blocked" ? "🚫" : "⏳";
119
+ const stale = sg.status === "pending" &&
120
+ round - sg.declared_at_round >= 10 ? " ⚠️ stale" : "";
121
+ const detail = sg.status === "in_progress"
122
+ ? ` (since R${sg.status_changed_at_round})`
123
+ : sg.status === "pending"
124
+ ? ` (since R${sg.declared_at_round})`
125
+ : ` (blocked R${sg.status_changed_at_round})`;
126
+ lines.push(`- ${statusIcon} [\`${sg.id}\`] ${sg.description}${detail}${stale}`);
127
+ }
128
+ if (hidden.length > 0) {
129
+ lines.push(`- also active (ids only): ${hidden.map((sg) => `\`${sg.id}\``).join(", ")}`);
130
+ }
131
+ lines.push(`> ${view.total} total · ${view.activeTotal} active · ` +
132
+ `${view.done} done · ${view.canceled} canceled`);
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;
146
+ }
34
147
  // unique() imported from token-utils.ts
35
148
  /** Deterministic JSON serialization used by state and prompt hashes. */
36
149
  export function stableStringify(value) {
@@ -102,132 +215,132 @@ function addListWithIds(lines, title, values, prefix) {
102
215
  function addListWithIdentities(lines, title, values, metadata) {
103
216
  if (values.length === 0)
104
217
  return;
105
- const idByText = new Map(metadata.map((meta) => [meta.text, meta.id]));
218
+ const metaByText = new Map(metadata.map((meta) => [meta.text, meta]));
106
219
  lines.push(`## ${title}`, "");
107
220
  for (const value of values) {
108
221
  const trimmed = value.trim();
109
222
  if (!trimmed)
110
223
  continue;
224
+ const meta = metaByText.get(trimmed);
111
225
  if (STABLE_ID_RE.test(trimmed)) {
112
226
  lines.push(`- [\`${trimmed}\`] ${trimmed}`);
113
227
  }
114
228
  else {
115
- const id = idByText.get(trimmed) ?? `c-${deriveItemId(trimmed)}`;
116
- lines.push(`- [\`${id}\`] ${trimmed}`);
229
+ const id = meta?.id ?? `c-${deriveItemId(trimmed)}`;
230
+ const provenance = constraintProvenanceLabel(meta);
231
+ lines.push(`- [\`${id}\`] ${trimmed}${provenance}`);
117
232
  }
118
233
  }
119
234
  lines.push("");
120
235
  }
121
- /** v3.8.1: the one-line phase position — which phase the loop is in, and how
122
- * far the next boundary is.
123
- *
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) {
133
- const lastMilestone = state.milestones.length > 0
134
- ? state.milestones[state.milestones.length - 1]
135
- : null;
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)
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)
139
241
  return "";
140
- const roundsSince = lastMilestone.round_range.end <= state.round
141
- ? state.round - lastMilestone.round_range.end
142
- : null;
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`);
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 "";
149
256
  }
150
- return parts.join(" · ");
151
257
  }
152
- /** v3.3/v3.4: Render the ACTIVE Round Contract as the Current Task section
153
- * body — the single formatting source shared by prompts and the state
154
- * file (module contract above). The original objective is NOT here: it
155
- * lives in the Objective section. Empty arrays render no line. */
156
- export function formatRoundContract(contract,
258
+ export function formatRoundContractWithIds(contract,
157
259
  /** v3.8: the derived item statuses. When given, each item renders with its
158
260
  * machine status — the agent sees the verification debt in its own task. */
159
261
  statuses) {
160
262
  const lines = [];
263
+ const emittedIds = [contract.id];
161
264
  const heading = contract.work_item?.trim();
162
265
  lines.push(`**${heading || "Round Contract"}** (${contract.id})`);
163
266
  const statusById = new Map((statuses?.items ?? []).map((item) => [item.itemId, item]));
164
- 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);
165
273
  const machine = statusById.get(item.id);
166
274
  const mark = machine?.status === "verified" ? "✅"
167
275
  : machine?.status === "insufficient" ? "🟠"
168
276
  : machine?.status === "contradicted" ? "⛔"
169
277
  : machine?.status === "pending" ? "⬜"
170
278
  : "";
171
- 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}`);
172
281
  if (item.verify_with.length > 0) {
173
- lines.push(` - Verify via: ${item.verify_with.join(", ")}`);
282
+ lines.push(` - Verify via: ${item.verify_with.join(", ")}`);
174
283
  }
175
- if (item.criterion_refs.length > 0) {
176
- lines.push(` - Criteria: ${item.criterion_refs.join(", ")}`);
284
+ if ((item.criterion_refs ?? []).length > 0) {
285
+ lines.push(` - Criteria: ${(item.criterion_refs ?? []).join(", ")}`);
177
286
  }
178
287
  if (machine?.reasons.length) {
179
- lines.push(` - Unverified because: ${machine.reasons[0]}`);
288
+ lines.push(` - Unverified because: ${machine.reasons[0]}`);
180
289
  }
181
290
  }
182
- for (const entry of contract.scope) {
183
- 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
+ }
184
297
  }
185
- return lines.join("\n");
298
+ return { text: lines.join("\n"), emittedIds };
186
299
  }
187
- // ═══════════════════════════════════════════════════════════════════════════
188
- // v3.3.1: Shared presentation atoms
189
- // ═══════════════════════════════════════════════════════════════════════════
190
- /** Milestone heading line ("**🏁 Round 7** (Rounds 3–7)"). Shared by the
191
- * L2 Phase History and the detailed L1 renderer; the L1 copy previously
192
- * rendered the range as "R3–R7", a format only this heading used. */
193
- export function milestoneHeading(milestone) {
194
- const kindIcon = milestone.kind === "agent_declared"
195
- ? "🏁"
196
- : milestone.kind === "criteria_milestone" ? "✅" : "📍";
197
- 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;
198
303
  }
199
304
  const STATE_SECTION_TIER = {
200
305
  // Current — what the next round must hold in working memory.
201
306
  "Loop Objective": "Current",
202
307
  "Current Task": "Current",
308
+ "Active Round Contract": "Current",
309
+ "Current User Instruction": "Current",
203
310
  "Success Criteria": "Current",
204
311
  "Hard Constraints": "Current",
205
312
  "Active Constraints": "Current",
206
- Remaining: "Current",
207
- 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",
208
318
  "Sub-Goal Dashboard": "Current",
209
- "External Context": "Current",
210
319
  // v3.8.1: the one-line phase position (the only fact the deleted Roadmap
211
320
  // section carried that no other section renders). It is orientation for the
212
321
  // NEXT round, so it belongs with the working-memory tier — and it must be
213
322
  // listed here at all: an unlisted title is not a heading, so `## Phase` was
214
323
  // absorbed into whatever section happened to precede it (its body even kept
215
324
  // a raw `## ` line) and inherited that section's tier.
216
- Phase: "Current",
217
325
  // Recent — this round's boundary facts (and, in a recovery window, the
218
326
  // derived Recovery Brief injected at the top of this tier).
219
327
  "Progress Dashboard": "Recent",
220
328
  Verification: "Recent",
221
- "Changes Since Last Round": "Recent",
222
- 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",
223
339
  // Historical Summary — the loop's memory skeleton, kept under existing
224
340
  // milestone/window caps.
225
341
  "Goal → Criteria": "Historical Summary",
226
- "Cross-Round Outcomes": "Historical Summary",
227
342
  "Recurring Flags": "Historical Summary",
228
- "Failed Patterns": "Historical Summary",
229
343
  "Retired Constraints": "Historical Summary",
230
- "Phase History": "Historical Summary",
231
344
  };
232
345
  const STATE_TIER_ORDER = ["Current", "Recent", "Historical Summary"];
233
346
  /** Human/Agent-readable materialized view. It is always reproducible from the
@@ -237,20 +350,34 @@ const STATE_TIER_ORDER = ["Current", "Recent", "Historical Summary"];
237
350
  * criterion list, round stats, the FULL recurring-flag history and the phase
238
351
  * history. The prompt carries only what the next round needs. */
239
352
  export function renderCanonicalStateMarkdown(state, options) {
353
+ const context = state.context ?? options?.context;
240
354
  const preamble = [
241
355
  `# LoopForge State — ${state.loopId}`,
242
356
  "",
243
357
  `**Schema**: ${state.schemaVersion}`,
244
358
  `**Round**: ${state.round}/${state.maxRounds}`,
245
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
+ : []),
246
368
  "**Derived**: true",
247
- `**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.",
248
374
  `**State hash**: ${hashCanonicalState(state).slice(0, 12)}`,
249
375
  "",
250
376
  ];
251
377
  const sections = flatStateSections(state);
252
- if (options?.recoveryBrief && options.recoveryBrief.length > 0) {
253
- 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 });
254
381
  }
255
382
  const byTier = new Map();
256
383
  for (const tier of STATE_TIER_ORDER)
@@ -280,42 +407,78 @@ export function renderCanonicalStateMarkdown(state, options) {
280
407
  function flatStateSections(state) {
281
408
  const lines = [];
282
409
  lines.push("## Loop Objective", "", state.objective, "", "## Current Task", "", state.currentTask);
283
- // v2.11: Success Criteria and Constraints render with stable IDs for exact agent matching
284
- addListWithIds(lines, "Success Criteria", state.successCriteria, "cr");
285
- addListWithIds(lines, "Hard Constraints", state.hardConstraints, "c");
286
- addListWithIdentities(lines, "Active Constraints", state.activeConstraints, state.constraintMetadata);
287
- addList(lines, "Changes Since Last Round", state.changesSinceLastRound);
288
- if (state.progress.criteriaMet.length > 0 ||
289
- state.progress.criteriaRemaining.length > 0 ||
290
- state.progress.filesChanged.length > 0 ||
291
- state.progress.tests !== null ||
292
- state.machineStatus !== undefined) {
293
- const total = state.progress.criteriaMet.length + state.progress.criteriaRemaining.length;
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
+ }
420
+ // v2.11: Success Criteria and Constraints render with stable IDs for exact
421
+ // agent matching. v3.9.4: the three groups are the SAME mutually exclusive
422
+ // partition the prompt uses, so one text never appears with two prefixes.
423
+ const constraintPartition = partitionActiveConstraints(state);
424
+ addListWithIds(lines, "Success Criteria", constraintPartition.criteria, "cr");
425
+ addListWithIdentities(lines, "Hard Constraints", constraintPartition.hard, state.constraintMetadata);
426
+ addListWithIdentities(lines, "Active Constraints", constraintPartition.other, state.constraintMetadata);
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;
294
449
  lines.push("## Progress Dashboard", "");
295
450
  if (total > 0) {
296
- 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"}`);
297
468
  }
298
469
  // v3.3: machine side of the comparison — git motion over committed rounds
299
470
  // (v3.6: the object always carries definite values — no "unavailable" arm).
300
471
  if (state.machineStatus) {
301
472
  const ms = state.machineStatus;
302
- const motion = ms.gitMotion
303
- ? `changes observed in ${ms.motionRounds}/${ms.windowRounds} recent committed rounds`
304
- : `no git changes in the last ${ms.windowRounds} committed rounds`;
305
- lines.push(`**Machine (git)**: ${motion}`);
306
- }
307
- if (state.criterionStatuses.length > 0) {
308
- const metCount = state.criterionStatuses.filter((cs) => cs.status === "verified" || cs.status === "claimed").length;
309
- lines.push(`**Machine (criteria)**: ${metCount}/${state.criterionStatuses.length} met across committed rounds`);
310
- }
311
- if (state.progress.tests) {
312
- lines.push(`**Tests**: ${state.progress.tests.passed} passed, ` +
313
- `${state.progress.tests.failed} failed, ${state.progress.tests.skipped} skipped`);
314
- }
315
- if (state.progress.filesChanged.length > 0) {
316
- lines.push("", "**Files Changed**:");
317
- for (const file of state.progress.filesChanged)
318
- 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}`);
319
482
  }
320
483
  lines.push("");
321
484
  }
@@ -329,20 +492,35 @@ function flatStateSections(state) {
329
492
  : cs.status === "contradicted" ? "⛔"
330
493
  : cs.status === "remaining" ? "⬜" : "❔";
331
494
  const idTag = ` [\`${cs.id}\`]`;
332
- const met = cs.met_at_round !== undefined ? ` (met R${cs.met_at_round})` : "";
333
- const related = cs.related_subgoal_ids.length > 0
334
- ? ` (related: ${cs.related_subgoal_ids.join(", ")})`
335
- : "";
336
- 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)");
337
520
  }
338
521
  lines.push("");
339
522
  }
340
- addListWithIds(lines, "Remaining", state.remainingCriteria, "cr");
341
- addList(lines, "Blockers", state.blockers);
342
523
  const recurring = recurringOnly(state.recurringFlags);
343
- addList(lines, "Discoveries", state.discoveries);
344
- addList(lines, "Cross-Round Outcomes", state.rollingOutcomes);
345
- addList(lines, "Failed Patterns", state.failedPatterns);
346
524
  // v3.8.1: ONE recurring-facts section. "Recurring Issues" (the last 5
347
525
  // rounds' raw violations, no threshold) and "Lessons Learned" (the whole
348
526
  // history, count >= 2) were two windows over one fact set rendered as two
@@ -355,78 +533,40 @@ function flatStateSections(state) {
355
533
  ? "🚫"
356
534
  : flag.kind === "verification_error" ? "⚠️" : "ℹ️";
357
535
  const subject = flag.ref ? `${flag.subject} [${flag.ref}]` : flag.subject;
358
- 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}`);
359
540
  }
360
541
  if (recurring.length > RECURRING_RENDER_CAP) {
361
542
  lines.push(`- ... and ${recurring.length - RECURRING_RENDER_CAP} more`);
362
543
  }
363
544
  lines.push("");
364
545
  }
365
- addList(lines, "Retired Constraints", state.retiredConstraints);
366
- // v3.8.1: the Roadmap section is gone. Only its non-duplicated fact — the
367
- // phase position — survives, as the one-line phase line the prompts render.
368
- const phaseLine = buildPhaseLine(state);
369
- if (phaseLine) {
370
- 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
+ }
371
551
  lines.push("");
372
552
  }
373
553
  if (state.verificationFlags.length > 0) {
374
554
  lines.push("## Verification", "");
375
555
  for (const flag of state.verificationFlags) {
376
- lines.push(`- [${flag.severity}] [${flag.check}] ${flag.detail}`);
556
+ lines.push(`- [${flag.severity}] [${flag.check}] \`${flag.field}\` — ${flag.detail}` +
557
+ (flag.ref ? ` [ref: \`${flag.ref}\`]` : ""));
377
558
  }
378
559
  lines.push("");
379
560
  }
380
- if (state.milestones.length > 0) {
381
- lines.push("## Phase History", "");
382
- for (const m of state.milestones) {
383
- const kindIcon = m.kind === "agent_declared"
384
- ? "🏁"
385
- : m.kind === "criteria_milestone" ? "✅" : "📍";
386
- lines.push(`### ${kindIcon} ${m.label}`, `Rounds ${m.round_range.start}–${m.round_range.end}`, "", `**Outcome**: ${m.outcome}`, "");
387
- if (m.carried_constraints.length > 0) {
388
- lines.push("**Carried Constraints**:");
389
- for (const c of m.carried_constraints)
390
- lines.push(`- ${c}`);
391
- lines.push("");
392
- }
393
- if (m.resolved_constraints.length > 0) {
394
- lines.push("**Resolved During Phase**:");
395
- for (const c of m.resolved_constraints)
396
- lines.push(`- ${c}`);
397
- lines.push("");
398
- }
399
- }
400
- }
401
561
  if (state.subGoals.length > 0) {
402
- // v3.7.1: only ACTIVE items render as rows; done/canceled survive in
403
- // the vault, replay, and the counts line below.
404
- const cap = getPolicy().evolution.max_active_subgoals;
405
- const view = activeSubGoalView(state.subGoals, cap);
562
+ // v3.9.4: the SAME formatter the prompt uses. Rows past the display cap
563
+ // keep their ids in an overflow line, so the state file's active id set
564
+ // can never be narrower than the prompt's.
406
565
  // L8: the heading is emitted whenever sub-goals exist — with every
407
566
  // sub-goal terminal (0 active rows) the counts line must still live
408
567
  // under its own section instead of leaking into the previous one.
409
568
  lines.push("## Sub-Goal Dashboard", "");
410
- if (view.active.length > 0) {
411
- for (const sg of view.active) {
412
- const statusIcon = sg.status === "in_progress" ? "🔄" :
413
- sg.status === "blocked" ? "🚫" : "⏳";
414
- const stale = sg.status === "pending" &&
415
- state.round - sg.declared_at_round >= 10 ? " ⚠️ stale" : "";
416
- const detail = sg.status === "in_progress"
417
- ? ` (since R${sg.status_changed_at_round})`
418
- : sg.status === "pending"
419
- ? ` (since R${sg.declared_at_round})`
420
- : ` (blocked R${sg.status_changed_at_round})`;
421
- lines.push(`- ${statusIcon} [\`${sg.id}\`] ${sg.description}${detail}${stale}`);
422
- }
423
- lines.push("");
424
- }
425
- lines.push(`> ${view.total} total · ${view.activeTotal} active · ` +
426
- `${view.done} done · ${view.canceled} canceled`, "");
427
- }
428
- if (state.externalContext) {
429
- lines.push("## External Context", "", state.externalContext, "");
569
+ lines.push(...formatActiveSubGoals(state.subGoals, state.round).lines, "");
430
570
  }
431
571
  // Split the flat markdown into titled sections for tier grouping. Every
432
572
  // builder above emits `## <Title>` headings over their content — but only
@@ -469,32 +609,63 @@ export function createCanonicalLoopState(request, response, stateFilePath,
469
609
  /** v3.3: Display-only derived data. Optional 4th param — callers that
470
610
  * predate v3.3 stay on 3-arg calls. */
471
611
  derived) {
472
- const last = request.last_round_result;
612
+ const last = derived?.previousRound?.result ?? null;
473
613
  const objective = response.loop_objective;
474
- const verificationFlags = request.verification_flags ?? [];
475
- const rolling = response.rolling_summary;
476
- const executionReport = last?.execution_report;
477
- const changes = unique([
478
- request.new_since_last_round,
479
- last?.output_summary,
480
- ...(last?.wrong_assumptions ?? []).map((value) => `Corrected assumption: ${value}`),
614
+ const verificationFlags = derived?.previousRound?.verificationFlags ??
615
+ request.verification_flags ?? [];
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] : []),
481
629
  ]);
482
- const discoveries = unique([
483
- ...(last?.discovered_constraints ?? []),
484
- ...(last?.emerged_subtasks ?? []),
485
- ]);
486
- const blockers = unique([
487
- ...(last?.constraint_violations ?? []),
488
- last?.manual_fixes_needed,
489
- request.rejection_notice,
490
- ...response.warnings,
491
- ]);
492
- // v3.4: The ACTIVE Round Contract (derived — loop-compiler computes it
493
- // from committed rounds) drives the Current Task — the contract IS "what
494
- // to do this round"; the original objective stays in the Objective
495
- // section above. No active contract → the original task text is used,
496
- // 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.
497
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;
651
+ // v3.9.4: the rendered criterion/hard sets are ACTIVE sets. The compile
652
+ // response's `criterion_statuses` is the active criterion reference space
653
+ // (loop-compiler filters it); `constraints_active` is the active constraint
654
+ // set. A direct caller that omits `criterion_statuses` gets the objective
655
+ // fallback so library-level state construction still works.
656
+ const activeConstraints = uniqueNormalized(response.constraints_active);
657
+ const activeConstraintKeys = new Set(activeConstraints.map((text) => normalizeText(text)));
658
+ const observedCompile = response.criterion_statuses !== undefined;
659
+ const successCriteria = observedCompile
660
+ ? uniqueNormalized(response.criterion_statuses.map((criterion) => criterion.text))
661
+ : uniqueNormalized(objective?.success_criteria ?? []);
662
+ const criteriaKeys = new Set(successCriteria.map((text) => normalizeText(text)));
663
+ const hardConstraints = uniqueNormalized(objective?.hard_constraints ?? [])
664
+ .filter((text) => {
665
+ const key = normalizeText(text);
666
+ return !criteriaKeys.has(key) &&
667
+ (!observedCompile || activeConstraintKeys.has(key));
668
+ });
498
669
  return {
499
670
  schemaVersion: CANONICAL_STATE_SCHEMA_VERSION,
500
671
  loopId: response.loop_id || request.loop_id,
@@ -502,47 +673,52 @@ derived) {
502
673
  maxRounds: request.max_rounds ?? getPolicy().engine.max_rounds,
503
674
  goalId: response.goal_id,
504
675
  objective: objective?.objective || request.task,
505
- objectiveVersion: objective?.version ?? 1,
506
- currentTask: roundContract
676
+ currentTask: currentUserInstruction ?? (roundContract && capability.contractVerificationAvailable
507
677
  ? formatRoundContract(roundContract, derived?.roundFacts?.itemStatuses ?? null)
508
- : request.task,
509
- successCriteria: unique(objective?.success_criteria ?? []),
510
- hardConstraints: unique(objective?.hard_constraints ?? []),
511
- activeConstraints: unique(response.constraints_active),
678
+ : objective?.objective ?? request.task),
679
+ ...(currentUserInstruction ? { currentUserInstruction } : {}),
680
+ successCriteria,
681
+ hardConstraints,
682
+ activeConstraints,
512
683
  retiredConstraints: unique(response.constraints_retired),
513
684
  constraintMetadata: response.constraint_metadata ?? [],
514
- changesSinceLastRound: changes,
515
- remainingCriteria: unique(claimedRemainingCriteria(last?.execution_report)),
516
- 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,
517
691
  verificationFlags,
518
- discoveries,
519
- rollingOutcomes: unique(rolling?.key_outcomes ?? []),
520
- failedPatterns: unique(rolling?.failed_patterns ?? []),
521
- milestones: rolling?.milestones ?? [],
692
+ // v4.0.2: conditional presence — no committed working memory, no key.
693
+ ...(hasWorkingMemoryContent(derived?.workingMemory)
694
+ ? { workingMemory: derived.workingMemory }
695
+ : {}),
522
696
  subGoals: response.sub_goals ?? [],
523
697
  criterionStatuses: response.criterion_statuses ?? [],
524
698
  recurringFlags: response.recurring_flags ?? [],
525
- externalContext: request.external_context?.trim() ?? "",
526
699
  stateFilePath,
527
- progress: {
700
+ reportedProgress: {
528
701
  criteriaMet: unique(claimedMetCriteria(executionReport)),
529
702
  criteriaRemaining: unique(claimedRemainingCriteria(executionReport)),
703
+ agentReportedRemaining: unique(claimedRemainingCriteria(executionReport)),
530
704
  filesChanged: unique(executionReport?.files_changed ?? []),
531
- tests: executionReport?.tests_reported ?? null,
532
705
  },
706
+ ...(resolvedOpenQuestionIds.length > 0
707
+ ? { resolvedOpenQuestionIds }
708
+ : {}),
709
+ ...(derived?.context ? { context: derived.context } : {}),
533
710
  // v3.3: Conditional presence — empty/absent derived data must not add
534
711
  // keys, or every state hash would change for rounds without it.
712
+ ...(derived?.recoveryBrief && derived.recoveryBrief.length > 0
713
+ ? { recoveryBrief: [...derived.recoveryBrief] }
714
+ : {}),
535
715
  ...(derived?.machineStatus ? { machineStatus: derived.machineStatus } : {}),
536
716
  // v3.3: Conditional presence — no contract, no key (hash stability).
537
717
  ...(roundContract ? { roundContract } : {}),
538
718
  ...(derived?.roundFacts?.itemStatuses
539
719
  ? { contractItemStatuses: derived.roundFacts.itemStatuses }
540
720
  : {}),
541
- ...(derived?.roundFacts?.verifiedSubGoals &&
542
- derived.roundFacts.verifiedSubGoals.length > 0
543
- ? { verifiedSubGoals: [...derived.roundFacts.verifiedSubGoals] }
544
- : {}),
545
- capability: deriveConfiguredCapability(getPolicy()),
721
+ capability,
546
722
  };
547
723
  }
548
724
  //# sourceMappingURL=canonical-state.js.map