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
@@ -11,48 +11,58 @@
11
11
  */
12
12
  import { deriveConfiguredCapability, getPolicy, } from "./policy.js";
13
13
  import { deriveRoundFacts, NO_IN_FLIGHT_ROUND } from "./round-facts.js";
14
+ import { deriveCriterionStatuses } from "./criterion-status.js";
14
15
  import { deriveSubGoals } from "./subgoal-state.js";
15
16
  import { verificationActionFor } from "./verification-actions.js";
17
+ import { deriveContractRequirement } from "./contract-requirement.js";
16
18
  /** Field order used by the prompt, guidance and receipts. Core fields first,
17
19
  * then the execution report, then contract/sub-goal state, then the
18
20
  * cognitive/report fields, then terminal fields. Deterministic by design. */
21
+ /** v4.0.3: THE four structural evaluation fields. The MCP input schema, the
22
+ * prompt template and field accounting read this one set. */
23
+ export const CORE_SELF_EVALUATION_FIELDS = new Set([
24
+ "outcome",
25
+ "output_summary",
26
+ "constraint_violations",
27
+ "should_continue",
28
+ ]);
19
29
  export const FIELD_ORDER = [
20
- "success",
30
+ "outcome",
21
31
  "output_summary",
22
32
  "constraint_violations",
23
33
  "should_continue",
24
34
  "execution_report.files_changed",
25
- "execution_report.tests_reported",
26
- "execution_report.contract_item_claims",
27
- "execution_report.criterion_claims",
35
+ "execution_report.claims",
36
+ "objective_refinement",
28
37
  "subgoal_updates",
29
38
  "emerged_subtasks",
30
39
  "discovered_constraints",
31
40
  "wrong_assumptions",
32
- "objective_refinement",
41
+ "open_questions",
42
+ "resolved_open_question_ids",
33
43
  "retracted_constraints",
34
- "revised_success_criteria",
35
44
  "round_contract",
36
45
  "worker_results",
37
- "prompt_requests",
38
- "compression_checkpoint",
39
- "checkpoint_label",
46
+ "subtask_boundary",
47
+ "subtask_boundary_label",
40
48
  "retroactiveClaims",
41
49
  "gate_ids",
42
- "outcome",
43
50
  "blocker",
44
- "stop_reason",
45
51
  "no_change_reason",
46
52
  ];
47
53
  const GUIDES = [
48
54
  {
49
- path: "success",
50
- purpose: "Whether the whole objective and every hard constraint are satisfied.",
51
- when: "Every round. Use true only when the full goal is achieved and verified.",
55
+ path: "outcome",
56
+ purpose: "The ONE round conclusion.",
57
+ when: "Every round. success requires should_continue=false; blocked requires " +
58
+ "should_continue=false and a blocker; partial/failed may continue or stop.",
52
59
  nature: "claim",
53
- downstream: "Stop decision, success trajectory, verification posture.",
54
- toolDescription: "true ONLY if all hard constraints were met AND the task goal was achieved. " +
55
- "This is a claim; a machine observation is what backs it.",
60
+ downstream: "Stop decision, success trajectory, verification posture, and every read " +
61
+ "surface. It is a claim; a machine observation is what backs it.",
62
+ toolDescription: "Required. success | partial | failed | blocked. success requires " +
63
+ "should_continue=false; blocked requires should_continue=false and a " +
64
+ "non-empty blocker. partial/failed may continue or stop. This is the ONE " +
65
+ "round conclusion.",
56
66
  core: true,
57
67
  },
58
68
  {
@@ -60,13 +70,15 @@ const GUIDES = [
60
70
  purpose: "What was actually done this round, not what was attempted.",
61
71
  when: "Every round.",
62
72
  nature: "report",
63
- downstream: "Rolling summary, focus/handoff projection, backtrack failed-approach list.",
73
+ downstream: "Committed record, state file, audit/replay/explain, and backtrack " +
74
+ "failed-approach facts — never machine evidence or a current-prompt narrative.",
64
75
  toolDescription: "Specific, actionable summary of what was DONE this round — not what was " +
65
76
  "attempted. Do not restate the plan.",
66
77
  core: true,
67
78
  },
68
79
  {
69
80
  path: "constraint_violations",
81
+ array: true,
70
82
  purpose: "Hard constraints actually violated this round.",
71
83
  when: "Every round; empty when none were violated.",
72
84
  nature: "report",
@@ -87,6 +99,7 @@ const GUIDES = [
87
99
  },
88
100
  {
89
101
  path: "execution_report.files_changed",
102
+ array: true,
90
103
  purpose: "The boundary net change: paths whose content or file type differs between " +
91
104
  "round preparation and this submission, including deleted paths.",
92
105
  when: "Every round that changed the workspace. A commit alone adds no entry when content is unchanged.",
@@ -99,42 +112,43 @@ const GUIDES = [
99
112
  reportOnly: false,
100
113
  },
101
114
  {
102
- path: "execution_report.tests_reported",
103
- purpose: "The agent's own test counts.",
104
- when: "When tests were run (or a test run is relevant to the claim).",
105
- nature: "report",
106
- downstream: "Compared with parsed command output when available; never converted into a verification verdict.",
107
- toolDescription: "Self-reported test counts. Informational: the runtime never translates " +
108
- "an exit code into a test count, and the receipt shows self-report and " +
109
- "command observation separately.",
110
- },
111
- {
112
- path: "execution_report.contract_item_claims",
113
- purpose: "Claims about items of the ACTIVE Round Contract.",
114
- when: "Only when an active contract is open; cite the rci-XXXXXXXX ids rendered in the prompt.",
115
+ path: "execution_report.claims",
116
+ array: true,
117
+ purpose: "Claims about objective criteria and ACTIVE contract items, in one channel.",
118
+ when: "When you want to declare which criteria or active contract items are met " +
119
+ "or still remaining; cite real cr-/rci- ids from this prompt.",
115
120
  nature: "claim",
116
- decision: "Claim completed items of the active contract with their real rci- ids",
117
- downstream: "Starts machine evaluation of the item; the item becomes verified only when its bound commands pass.",
118
- toolDescription: "Claims about items of the ACTIVE contract, cited by the rci-XXXXXXXX id " +
119
- "the prompt rendered. A claim never verifies anything by itself — the " +
120
- "runtime marks an item verified only when its bound commands are observed " +
121
- "passing. Unknown ids or illegal outcomes are contract_invalid.",
121
+ decision: "Claim criteria or active contract items with their real cr-/rci- ids",
122
+ compact: true,
123
+ promptRank: 10,
124
+ downstream: "`cr-` advisory; `rci-` strict; only machine commands verify.",
125
+ toolDescription: "One claims channel. Each entry is { id, outcome } where id is a " +
126
+ "cr-XXXXXXXX objective criterion id or an rci-XXXXXXXX item id of the " +
127
+ "ACTIVE contract, and outcome is met | remaining. cr- claims are " +
128
+ "advisory (unknown/malformed entries are dropped); rci- claims are strict " +
129
+ "(unknown/duplicate ids are contract_invalid). A claim never verifies " +
130
+ "anything by itself — the runtime marks an item verified only when its " +
131
+ "bound commands are observed passing.",
122
132
  },
123
133
  {
124
- path: "execution_report.criterion_claims",
125
- purpose: "Advisory claims about objective criteria.",
126
- when: "When you want to state which criteria are met or still remaining.",
127
- nature: "claim",
128
- decision: "You can state which criteria are met or still outstanding",
134
+ path: "objective_refinement",
135
+ purpose: "The Agent's working version of the OVERALL starting objective — not the " +
136
+ "current round, not the contract.",
137
+ when: "Only when the overall objective genuinely evolved. Omit when unchanged; " +
138
+ "this is event-driven, not a per-round field.",
139
+ nature: "report",
140
+ decision: "Overall objective genuinely evolved (omit when unchanged)",
129
141
  compact: true,
130
- promptRank: 10,
131
- downstream: "Advisory only: unbacked claims warn; they never upgrade a criterion to verified.",
132
- toolDescription: "Advisory claims about objective criteria. LENIENT: unknown or malformed " +
133
- "ids are dropped with a diagnostic, never a rejection. A criterion claim " +
134
- "never becomes machine-verified by itself.",
142
+ promptRank: 15,
143
+ downstream: "Updates the working objective text; never acceptance, criteria or constraints.",
144
+ toolDescription: "Optional, at most 1000 chars. A refined statement of the OVERALL " +
145
+ "starting objective when understanding genuinely changed. It never " +
146
+ "rewrites the operator acceptance, criteria or hard constraints, and it " +
147
+ "is not the round task or contract. Omit when the objective is unchanged.",
135
148
  },
136
149
  {
137
150
  path: "subgoal_updates",
151
+ array: true,
138
152
  purpose: "Explicit status transitions for existing sub-goals.",
139
153
  when: "When an active sg-XXXXXXXX sub-goal starts, is blocked, completes, or is canceled.",
140
154
  nature: "transition",
@@ -148,143 +162,171 @@ const GUIDES = [
148
162
  },
149
163
  {
150
164
  path: "emerged_subtasks",
151
- purpose: "New sub-problems that surfaced during execution.",
152
- when: "When a new work item appears or an existing one must be split.",
165
+ array: true,
166
+ purpose: "New execution-plan work that the active criteria/contract do not already cover.",
167
+ when: "Only when new work appears or an existing work item must be split. Zero " +
168
+ "is correct when the current criteria/contract already cover the round.",
153
169
  nature: "transition",
154
170
  decision: "A new sub-problem surfaced (plain text, never sg- ids)",
155
171
  compact: true,
156
172
  promptRank: 40,
157
173
  downstream: "The only sub-goal creation channel; the runtime derives stable sg- ids.",
158
- toolDescription: "Sub-problems that surfaced during execution. The ONLY sub-goal creation " +
159
- "channel. Plain descriptions, never sg-XXXXXXXX ids.",
174
+ toolDescription: "New work that surfaced during execution and was not already covered by " +
175
+ "the active criteria or current contract. The ONLY sub-goal creation " +
176
+ "channel. Plain descriptions, never sg-XXXXXXXX ids. Do not restate the " +
177
+ "criteria as sub-goals; zero entries is a valid result.",
160
178
  },
161
179
  {
162
180
  path: "discovered_constraints",
181
+ array: true,
163
182
  purpose: "New constraints that later rounds must respect.",
164
183
  when: "When this round discovered a limit that was not known before.",
165
184
  nature: "report",
166
185
  decision: "You discovered a limit later rounds must respect",
167
186
  compact: true,
168
187
  promptRank: 20,
169
- downstream: "Merged into the active constraint set by the compiler (bounded per round).",
188
+ downstream: "Merged into the active constraint set by the compiler; entries over " +
189
+ "max_discovered_constraints_per_round are diagnosed and dropped at the " +
190
+ "submission boundary.",
170
191
  toolDescription: "New constraints that must apply to later rounds. This is the ONLY " +
171
192
  "promotion channel for a worker-discovered constraint: a worker result " +
172
193
  "stays a report until the main agent states the constraint here.",
173
194
  },
174
195
  {
175
196
  path: "wrong_assumptions",
197
+ array: true,
176
198
  purpose: "Assumptions from earlier rounds that this round falsified.",
177
199
  when: "When evidence proved an earlier assumption wrong; omit when none.",
178
200
  nature: "report",
179
201
  decision: "Evidence falsified an earlier assumption",
180
202
  compact: true,
181
203
  promptRank: 30,
182
- downstream: "State-file 'Corrected assumption', recovery brief, and backtrack wrong-assumption facts.",
204
+ downstream: "Bounded Working Memory (fresh prompts and the committed projection), " +
205
+ "recovery brief, replay and explain. Only committed reports reach the " +
206
+ "next prompt.",
183
207
  toolDescription: "Assumptions from earlier rounds that turned out to be wrong. Record one " +
184
208
  "only when this round actually falsified it; otherwise omit. This is not " +
185
209
  "a reflection quota.",
186
210
  },
187
211
  {
188
- path: "objective_refinement",
189
- purpose: "A deeper or more accurate understanding of the objective.",
190
- when: "When this round changed your understanding of what the task needs.",
212
+ path: "open_questions",
213
+ array: true,
214
+ purpose: "Unresolved questions that still affect later slices (a question list, " +
215
+ "not a todo list).",
216
+ when: "Only when a real open question affects how later work should proceed; " +
217
+ "omit when none.",
191
218
  nature: "report",
192
- downstream: "Appended to the objective with a version bump; never replaces the original objective.",
193
- toolDescription: "Optional. A refinement or deepening of the task objective based on this " +
194
- "round's discoveries. Appended, never substituted for the original goal.",
219
+ reportOnly: true,
220
+ decision: "A question remains open that later slices must resolve",
221
+ compact: true,
222
+ promptRank: 35,
223
+ downstream: "Bounded Working Memory (fresh prompts and the committed projection), " +
224
+ "replay and explain. Strictly report-only: it never enters the " +
225
+ "verification gate, success, contract closure or machine evidence.",
226
+ toolDescription: "Optional. At most 3 unresolved questions, 300 chars each. Record only " +
227
+ "questions that still affect later slices — not a todo list, not a " +
228
+ "completion checklist, and not a substitute for a blocker or decision. " +
229
+ "Report-only: LoopForge never acts on these entries.",
195
230
  },
196
231
  {
197
- path: "retracted_constraints",
198
- purpose: "Existing constraints that are no longer correct or relevant.",
199
- when: "When a prior constraint is proven wrong or obsolete.",
232
+ path: "resolved_open_question_ids",
233
+ array: true,
234
+ purpose: "Open-question ids that this round resolved.",
235
+ when: "When a previously rendered open question is no longer unresolved.",
200
236
  nature: "report",
201
- downstream: "Retired from the active set (with the configured retirement window for plan/hard constraints).",
202
- toolDescription: "Optional. Constraints the agent now believes are wrong or irrelevant, by " +
203
- "c-XXXXXXXX id or exact text. Omit when none.",
237
+ reportOnly: true,
238
+ decision: "A rendered open question was resolved",
239
+ compact: true,
240
+ promptRank: 36,
241
+ downstream: "Working Memory removes the cited question; replay and explain retain the report.",
242
+ toolDescription: "Optional. Use only stable oq-... ids rendered beside open questions in " +
243
+ "Working Memory or the state file. Report-only; it never changes the " +
244
+ "verification gate or acceptance criteria.",
204
245
  },
205
246
  {
206
- path: "revised_success_criteria",
207
- purpose: "Success criteria that need reformulation.",
208
- when: "When the original criterion was incorrect or unmeasurable.",
247
+ path: "retracted_constraints",
248
+ array: true,
249
+ purpose: "Existing constraints that are no longer correct or relevant.",
250
+ when: "When a prior constraint is proven wrong or obsolete.",
209
251
  nature: "report",
210
- downstream: "Applied to the Loop Objective with a version bump.",
211
- toolDescription: "Optional. Success criteria that need reformulation: each entry has the " +
212
- "old and new wording.",
252
+ downstream: "Only active Agent-discovered constraints (including recovery-preserved " +
253
+ "discoveries) are retractable, by c-XXXXXXXX id or normalized-exact text. " +
254
+ "Operator criteria/hard constraints and plan constraints are rejected. " +
255
+ "Unknown/inactive targets are no-ops with a diagnostic.",
256
+ toolDescription: "Optional. Active Agent-discovered constraints that are no longer valid, " +
257
+ "by c-XXXXXXXX id or exact text. Operator criteria/hard constraints and " +
258
+ "plan constraints cannot be retracted. Omit when none.",
213
259
  },
214
260
  {
215
261
  path: "round_contract",
216
- purpose: "A proposal for the NEXT round's evidence-bound contract.",
217
- when: "When no contract is active and a contract command is available; while a " +
218
- "contract is open, omit it (or restate it unchanged) unless this round " +
219
- "closes it and declares a successor.",
262
+ purpose: "THE contract channel: the NEXT round's bounded deliverable and its " +
263
+ "acceptance conditions.",
264
+ when: "When contract coverage requires a next-round contract, or when you want " +
265
+ "to start or replace a slice. With after-capable commands, a continuing " +
266
+ "round with a successor available must leave an active contract; omit to " +
267
+ "continue the current one. The final configured round has no successor.",
220
268
  nature: "proposal",
221
- decision: "No active contract: if the next slice is machine-checkable, declare it",
269
+ decision: "The Agent decides the next slice; LoopForge only validates the declaration",
222
270
  downstream: "Becomes active only after this round commits; item ids rci-XXXXXXXX are " +
223
- "derived by the runtime.",
224
- toolDescription: "Proposal for the NEXT round's contract (items + verify_with + scope). " +
225
- "Declared here, it becomes the ACTIVE contract only after this round " +
226
- "commits. While it is open, omitting it continues the contract; a " +
227
- "different proposal is ignored (contract_premature). DECLARATION IS " +
228
- "STRICT: an item with no bound command, an unknown/disabled command, a " +
229
- "scope entry outside the workspace, a malformed reference, or a count " +
230
- "over the limits returns contract_invalid.",
271
+ "derived by the runtime. `scope` is a report-only intent statement and " +
272
+ "never rejects or blocks a round. `supersedes` closes the named ACTIVE " +
273
+ "contract as `superseded` and activates this proposal next round.",
274
+ toolDescription: "THE contract channel for the NEXT round. work_item is the bounded " +
275
+ "deliverable this slice produces (an outcome, not an activity) and is " +
276
+ "required for a new proposal. items are acceptance conditions; each item " +
277
+ "binds its own verify_with commands and may cite cr- ids in " +
278
+ "criterion_refs. scope is an OPTIONAL, report-only declaration of " +
279
+ "intended files — it is not enforced and is not part of the rc- id. " +
280
+ "While a contract is open, omit this field to continue it unchanged. To " +
281
+ "replace the active slice, add supersedes { contract_id, reason? } with " +
282
+ "the active rc- id and different acceptance items. If the active " +
283
+ "contract closes this round and a successor is available, a successor " +
284
+ "is REQUIRED. The final configured round has no successor consumer. " +
285
+ "DECLARATION IS STRICT: a missing work_item, an item with no bound " +
286
+ "command, an unknown/disabled command, a malformed reference, an " +
287
+ "unknown field, or a count over the limits returns contract_invalid.",
231
288
  },
232
289
  {
233
290
  path: "worker_results",
291
+ array: true,
234
292
  purpose: "Results of sub-agent / worker delegations.",
235
293
  when: "Only when work was actually delegated.",
236
294
  nature: "report",
237
295
  decision: "You delegated work",
238
296
  compact: true,
239
297
  promptRank: 50,
240
- downstream: "Saved with its source and written to the delegation journal; worker " +
241
- "discoveredConstraints is NOT auto-promoted.",
298
+ downstream: "Saved inside the committed transaction as an audit report. A worker " +
299
+ "finding is not auto-promoted.",
242
300
  toolDescription: "Optional. Results of sub-agent / Worker delegations this round. Each " +
243
301
  "entry must declare an outcome. Entries without a valid outcome are " +
244
- "dropped with a diagnostic. A worker's discoveredConstraints stays in " +
245
- "this report; to make it a global constraint, submit it in top-level " +
246
- "discovered_constraints.",
302
+ "dropped with a diagnostic. To make a worker finding a global " +
303
+ "constraint, submit it in top-level discovered_constraints.",
247
304
  reportOnly: true,
248
305
  },
249
306
  {
250
- path: "prompt_requests",
251
- purpose: "Information needs for the next round's prompt.",
252
- when: "When you know what context the next round will need.",
253
- nature: "control",
254
- decision: "You know what the next prompt needs",
255
- compact: true,
256
- promptRank: 70,
257
- downstream: "Matched exactly against state items; applied to the NEXT round's prompt. " +
258
- "If that prompt is L0 it is ignored. Unmatched entries have no effect.",
259
- toolDescription: "Optional. Information needs for the NEXT prompt. emphasize entries are " +
260
- "matched by stable id or normalized-exact text; a paraphrase matches " +
261
- "nothing. confusion_points are echoed at the top. They are applied to the " +
262
- "next round's prompt; if that prompt is L0 they are ignored. This never " +
263
- "becomes long-term knowledge.",
264
- },
265
- {
266
- path: "compression_checkpoint",
307
+ path: "subtask_boundary",
267
308
  purpose: "Mark a meaningful work boundary.",
268
- when: "At a phase/subtask boundary worth a full-state milestone.",
309
+ when: "At a subtask boundary that warrants an L2 rehydration.",
269
310
  nature: "report",
270
- decision: "This round reached a real phase boundary; pair with checkpoint_label",
311
+ decision: "This round reached a real phase boundary; pair with subtask_boundary_label",
271
312
  compact: true,
272
313
  promptRank: 60,
273
- downstream: "Creates an agent-declared milestone and forces an L2 rehydration boundary.",
274
- toolDescription: "Optional. true at a subtask boundary to create a recoverable milestone " +
275
- "and force a full-state checkpoint. Pair with checkpoint_label.",
314
+ downstream: "Forces an L2 rehydration boundary. It creates no milestone or phase narrative.",
315
+ toolDescription: "Optional. true at a subtask boundary to force an L2 rehydration boundary. " +
316
+ "Pair with subtask_boundary_label.",
276
317
  },
277
318
  {
278
- path: "checkpoint_label",
279
- purpose: "A human-readable label for the checkpoint.",
280
- when: "Only together with compression_checkpoint=true.",
319
+ path: "subtask_boundary_label",
320
+ purpose: "A human-readable label for the subtask boundary.",
321
+ when: "Only together with subtask_boundary=true.",
281
322
  nature: "report",
282
- downstream: "Milestone heading in later prompts and the state file.",
283
- toolDescription: "Optional. Short label for the checkpoint (for example " +
284
- "'core-model-complete'). Meaningful only when compression_checkpoint=true.",
323
+ downstream: "Stored on the committed report; it does not create a later prompt heading.",
324
+ toolDescription: "Optional. Short label for the subtask boundary (for example " +
325
+ "'core-model-complete'). Meaningful only when subtask_boundary=true.",
285
326
  },
286
327
  {
287
328
  path: "retroactiveClaims",
329
+ array: true,
288
330
  purpose: "A claim that an earlier round satisfied a criterion.",
289
331
  when: "When you can point at an earlier round's evidence.",
290
332
  nature: "claim",
@@ -295,6 +337,7 @@ const GUIDES = [
295
337
  },
296
338
  {
297
339
  path: "gate_ids",
340
+ array: true,
298
341
  purpose: "Approved gate ids this round's work depended on.",
299
342
  when: "Only when policy.gate.enabled is true and an approval was actually obtained.",
300
343
  nature: "control",
@@ -303,34 +346,16 @@ const GUIDES = [
303
346
  "Enforced only when policy.gate.enabled: a cited, unapproved gate rejects " +
304
347
  "the round. Without the gate layer this is saved but has no effect.",
305
348
  },
306
- {
307
- path: "outcome",
308
- purpose: "The declared round outcome.",
309
- when: "When the round is partial, failed, or blocked (success is derivable).",
310
- nature: "claim",
311
- downstream: "Wins over `success` for the stop decision and success-class checks.",
312
- toolDescription: "Optional. success | partial | failed | blocked. When absent, derived " +
313
- "from success. A declared non-success outcome suppresses the " +
314
- "success-class checks even when success=true.",
315
- },
316
349
  {
317
350
  path: "blocker",
318
351
  purpose: "A concrete description of what blocks the work.",
319
- when: "Required when outcome=blocked; useful with stop_reason=blocked.",
352
+ when: "Required when outcome=blocked; ignored (with a diagnostic) otherwise.",
320
353
  nature: "report",
321
- downstream: "Stop decision, handoff open risks, and gate classification when enabled.",
322
- toolDescription: "Optional. A flat blocker description, required when outcome=blocked. " +
323
- "State what cannot proceed and why; do not use it as a substitute for " +
324
- "`success=false`.",
325
- },
326
- {
327
- path: "stop_reason",
328
- purpose: "Why the agent is stopping.",
329
- when: "Only when should_continue=false and success=false.",
330
- nature: "control",
331
- downstream: "blocked/needs_human_input stop without claiming success; gave_up is the default stop behavior.",
332
- toolDescription: "Optional. Why the agent is stopping: gave_up | blocked | needs_human_input. " +
333
- "Only meaningful when should_continue=false and success=false.",
354
+ downstream: "Stop reason, blocked-contract closure, and gate classification when enabled. " +
355
+ "When work needs human input, say so here with outcome=blocked.",
356
+ toolDescription: "Required when outcome=blocked (trim non-empty, at most 500 chars). State " +
357
+ "what cannot proceed and why. When another outcome is declared it is " +
358
+ "dropped with a diagnostic and never rejects the round.",
334
359
  },
335
360
  {
336
361
  path: "no_change_reason",
@@ -345,13 +370,31 @@ const GUIDES = [
345
370
  },
346
371
  ];
347
372
  const GUIDE_BY_PATH = new Map(GUIDES.map((guide) => [guide.path, guide]));
373
+ /** v4.0.3: array-valued evaluation paths, derived from the SAME field
374
+ * metadata the prompt, MCP schema and receipts read. Field accounting uses
375
+ * this set instead of a second hardcoded list. */
376
+ export const ARRAY_SELF_EVALUATION_FIELDS = new Set(GUIDES.filter((guide) => guide.array === true).map((guide) => guide.path));
348
377
  /** Fields that existed in earlier releases and are now unknown. A diagnostic
349
378
  * for one of these names explains the removal instead of silently dropping. */
350
379
  const RETIRED_FIELDS = {
351
380
  progress_estimate: "removed in 3.9.0 — progress is machine-derived; remove this field",
352
381
  progress_at_boundary: "removed in 3.9.0 — milestones render a round range, not a percentage",
353
- success_criteria_remaining: "removed in 3.8 — report criterion claims under execution_report.criterion_claims",
382
+ success_criteria_remaining: "removed in 3.8 — report criterion claims under execution_report.claims",
354
383
  next_action: "removed in 3.8 — the agent plans; LoopForge does not accept a next-action field",
384
+ revised_success_criteria: "removed in 3.9.6 — operator acceptance is immutable; report a conflict in " +
385
+ "wrong_assumptions or stop with outcome=blocked/needs_human_input",
386
+ prompt_requests: "removed in 4.0.1 — the next prompt is compiled from committed facts; " +
387
+ "there is no per-round prompt-steering channel",
388
+ success: "removed in 4.0.6 — use `outcome` (success | partial | failed | blocked)",
389
+ agent_stop_reason: "removed in 4.0.6 — use outcome=blocked with `blocker`; stop with " +
390
+ "outcome=failed and should_continue=false",
391
+ contract_transition: "removed in 4.0.6 — use round_contract.supersedes { contract_id, reason? }",
392
+ tests_reported: "removed in 4.0.6 — command observations are the test fact; do not report counts",
393
+ criterion_claims: "removed in 4.0.6 — use execution_report.claims with cr- ids",
394
+ contract_item_claims: "removed in 4.0.6 — use execution_report.claims with rci- ids",
395
+ "execution_report.tests_reported": "removed in 4.0.6 — command observations are the test fact; remove tests_reported",
396
+ "execution_report.criterion_claims": "removed in 4.0.6 — move these claims into execution_report.claims with cr- ids",
397
+ "execution_report.contract_item_claims": "removed in 4.0.6 — move these claims into execution_report.claims with rci- ids",
355
398
  };
356
399
  export function fieldGuide(path) {
357
400
  return GUIDE_BY_PATH.get(path);
@@ -372,39 +415,26 @@ function firstActiveItem(contract) {
372
415
  function firstActiveSubGoal(subGoals) {
373
416
  return subGoals.find((item) => item.status !== "done" && item.status !== "canceled") ?? null;
374
417
  }
375
- /** The first criterion the next slice can still work on: `remaining` or
376
- * `unknown`. `claimed` is the Agent's own completion claim (machine
377
- * unverified), so it is not a natural NEXT work item. */
378
- export function outstandingCriterion(criteria) {
379
- return (criteria ?? []).find((criterion) => criterion.status === "remaining" || criterion.status === "unknown") ?? null;
380
- }
381
- function clipContractText(text) {
382
- const collapsed = (text ?? "").trim().replace(/\s+/g, " ");
383
- return collapsed.length > 200 ? collapsed.slice(0, 200) : collapsed;
384
- }
385
418
  /** A legal minimal example for a field, or null when the current facts make the
386
419
  * field unusable (no real id/command to cite). The renderer omits nulls; it
387
420
  * never invents an sg-/rci- reference. */
388
421
  export function fieldExample(path, ctx) {
389
422
  switch (path) {
390
- case "success": return false;
423
+ case "outcome": return "partial";
391
424
  case "output_summary": return "<what was done and how it was verified>";
392
425
  case "constraint_violations": return [];
393
426
  case "should_continue": return true;
394
427
  case "execution_report.files_changed":
395
428
  return ["<path whose content or type differs from round start>"];
396
- case "execution_report.tests_reported":
397
- return { passed: 0, failed: 0, skipped: 0 };
398
- case "execution_report.contract_item_claims": {
429
+ case "execution_report.claims": {
399
430
  const item = firstActiveItem(ctx.activeContract);
400
- return item ? [{ item_id: item.id, outcome: "met" }] : null;
401
- }
402
- case "execution_report.criterion_claims": {
431
+ if (item)
432
+ return [{ id: item.id, outcome: "met" }];
403
433
  const criterion = ctx.criteria?.[0];
404
- return criterion
405
- ? [{ criterion_id: criterion.id, outcome: "met" }]
406
- : null;
434
+ return criterion ? [{ id: criterion.id, outcome: "met" }] : null;
407
435
  }
436
+ case "objective_refinement":
437
+ return "<the overall objective as it now stands>";
408
438
  case "subgoal_updates": {
409
439
  const subGoal = firstActiveSubGoal(ctx.activeSubGoals);
410
440
  return subGoal ? [{ id: subGoal.id, status: "done" }] : null;
@@ -415,54 +445,38 @@ export function fieldExample(path, ctx) {
415
445
  return ["<a constraint later rounds must respect>"];
416
446
  case "wrong_assumptions":
417
447
  return ["<an assumption from an earlier round that was falsified>"];
418
- case "objective_refinement":
419
- return "<what you now understand better about the objective>";
448
+ case "open_questions":
449
+ return ["<an unresolved question that still affects later slices>"];
450
+ case "resolved_open_question_ids":
451
+ return ["oq-<stable id rendered for a question>"];
420
452
  case "retracted_constraints":
421
453
  return ["<constraint text or c-XXXXXXXX id that is no longer valid>"];
422
- case "revised_success_criteria":
423
- return [{ old: "<old criterion>", new: "<new criterion>" }];
424
454
  case "round_contract": {
425
455
  if (!ctx.contractVerificationAvailable || ctx.commands.length === 0)
426
456
  return null;
427
- const criterion = outstandingCriterion(ctx.criteria);
428
- const workItem = clipContractText(ctx.currentRequirement) ||
429
- "<the current requirement, one line>";
430
457
  return {
431
- work_item: workItem,
458
+ work_item: "<bounded deliverable this slice produces>",
432
459
  items: [{
433
- description: criterion
434
- ? clipContractText(criterion.text)
435
- : "<machine-checkable outcome for this requirement>",
436
- criterion_refs: criterion ? [criterion.id] : [],
437
- subgoal_refs: [],
460
+ description: "<machine-checkable acceptance condition>",
461
+ criterion_refs: [],
438
462
  verify_with: [ctx.commands[0]],
439
463
  }],
440
- scope: ["<workspace-relative file or directory>"],
441
464
  };
442
465
  }
443
466
  case "worker_results":
444
467
  return [{
445
468
  agentId: "<worker id>",
446
- subAgentType: "general-purpose",
447
469
  subTask: "<self-contained subtask you delegated>",
448
470
  resultSummary: "<what the worker reported>",
449
471
  outcome: "success",
450
- discoveredConstraints: [],
451
472
  }];
452
- case "prompt_requests":
453
- return {
454
- emphasize: ["<exact constraint text or stable id>"],
455
- confusion_points: ["<what is unclear about the current state>"],
456
- };
457
- case "compression_checkpoint": return false;
458
- case "checkpoint_label": return "<short phase label>";
473
+ case "subtask_boundary": return false;
474
+ case "subtask_boundary_label": return "<short phase label>";
459
475
  case "retroactiveClaims":
460
476
  return [{ round: 1, claim: "<what that earlier round satisfied>" }];
461
477
  case "gate_ids":
462
478
  return ["<approved gateId from loopforge_gate_check>"];
463
- case "outcome": return "partial";
464
479
  case "blocker": return "<what cannot proceed and why>";
465
- case "stop_reason": return "blocked";
466
480
  case "no_change_reason": return "<why no workspace change was needed>";
467
481
  default: return null;
468
482
  }
@@ -470,7 +484,7 @@ export function fieldExample(path, ctx) {
470
484
  /** Semantic applicability — the ONE rule, deliberately independent of prompt
471
485
  * level. Core fields and the two baseline execution-report fields are always
472
486
  * applicable; the rest depend on facts the runtime can actually observe.
473
- * Future events (delegation, a checkpoint boundary, a falsified assumption)
487
+ * Future events (delegation, a subtask boundary, a falsified assumption)
474
488
  * remain conditional opportunities, not render-time facts. */
475
489
  export function isFieldApplicable(path, facts) {
476
490
  const guide = GUIDE_BY_PATH.get(path);
@@ -480,21 +494,18 @@ export function isFieldApplicable(path, facts) {
480
494
  return true;
481
495
  switch (path) {
482
496
  case "execution_report.files_changed":
483
- case "execution_report.tests_reported":
484
497
  return true;
485
- case "execution_report.contract_item_claims":
486
- return facts.activeContract !== null;
487
- case "execution_report.criterion_claims":
488
- return (facts.criteria ?? []).length > 0;
498
+ case "execution_report.claims":
499
+ return facts.activeContract !== null || (facts.criteria ?? []).length > 0;
500
+ case "objective_refinement":
501
+ return true;
489
502
  case "subgoal_updates":
490
503
  return firstActiveSubGoal(facts.subGoals) !== null;
491
504
  case "round_contract":
492
- // A first declaration needs no active contract and a usable command.
493
- // While a contract is open, the dedicated active-contract block carries
494
- // the successor instruction instead of a competing proposal example.
495
- return facts.activeContract === null &&
496
- facts.contractVerificationAvailable &&
497
- facts.commands.length > 0;
505
+ // v4.0.6: the contract channel is usable whenever a contract can be
506
+ // verified. The dedicated contract block decides REQUIRED vs continue vs
507
+ // redirect for the current facts.
508
+ return facts.contractVerificationAvailable && facts.commands.length > 0;
498
509
  case "gate_ids":
499
510
  return facts.gateEnabled;
500
511
  case "no_change_reason":
@@ -506,8 +517,8 @@ export function isFieldApplicable(path, facts) {
506
517
  /** Compact-checklist membership: the explicit `compact` marker plus semantic
507
518
  * applicability. The level is deliberately absent — L0/L1/L2 offer the same
508
519
  * evaluation fields; only state density changes. Dedicated renderer fields
509
- * (`round_contract`, `subgoal_updates`, `contract_item_claims`) are never
510
- * compact members, so they cannot duplicate their own blocks. */
520
+ * (`round_contract`, `subgoal_updates`) are never compact members, so they
521
+ * cannot duplicate their own blocks. */
511
522
  export function isFieldPrompted(path, facts) {
512
523
  const guide = GUIDE_BY_PATH.get(path);
513
524
  if (!guide?.compact)
@@ -526,10 +537,23 @@ function statusOfItem(itemId, statuses) {
526
537
  }
527
538
  function renderContractGuidance(ctx) {
528
539
  const contract = ctx.activeContract;
540
+ const hasCommand = ctx.contractVerificationAvailable && ctx.commands.length > 0;
541
+ // v4.0.6: coverage guidance uses the SAME shared rule as the submission
542
+ // boundary and the post-evidence gate. `proposalPresent:false` describes
543
+ // this prompt (no next-round proposal has been committed yet).
544
+ const requirement = deriveContractRequirement({
545
+ shouldContinue: true,
546
+ blocked: false,
547
+ proposalPresent: false,
548
+ activeContract: contract,
549
+ closure: ctx.itemStatuses?.closure ?? "open",
550
+ contractCapability: hasCommand,
551
+ successorRoundAvailable: ctx.maxRounds === undefined || ctx.round < ctx.maxRounds,
552
+ });
529
553
  if (contract) {
530
554
  const lines = [
531
- "Active Round Contract — continue it; you do NOT need to restate it:",
532
- `- ${contract.work_item ? `${contract.work_item} ` : ""}(${contract.id})`,
555
+ "Active Round Contract — it governs this round:",
556
+ `- Deliverable: ${contract.work_item ?? "(not named; legacy contract)"} (${contract.id})`,
533
557
  ];
534
558
  const shown = contract.items.slice(0, 5);
535
559
  for (const item of shown) {
@@ -540,38 +564,48 @@ function renderContractGuidance(ctx) {
540
564
  if (contract.items.length > shown.length) {
541
565
  lines.push(` - … and ${contract.items.length - shown.length} more in the Current Task.`);
542
566
  }
543
- const claimDecision = GUIDE_BY_PATH.get("execution_report.contract_item_claims")?.decision
544
- ?? "Claim completed items of the active contract with their real rci- ids";
545
- lines.push(`${claimDecision} via \`execution_report.contract_item_claims\`.`, "A claim alone leaves an item insufficient; its bound command must be observed", "passing. Omit `round_contract` to continue this contract; a different proposal", "is ignored (contract_premature). If this round closes the contract and another", "slice follows, declare the successor proposal here.");
567
+ const claimDecision = GUIDE_BY_PATH.get("execution_report.claims")?.decision
568
+ ?? "Claim criteria or active contract items with their real cr-/rci- ids";
569
+ lines.push(`${claimDecision} via \`execution_report.claims\`.`, "A claim alone leaves an item insufficient; its bound command must be observed", "passing. Omit `round_contract` to CONTINUE this contract unchanged — a", "different proposal without `supersedes` is rejected as contract_premature.", "To replace this slice, add `round_contract.supersedes` with", `{ contract_id: "${contract.id}" } and DIFFERENT acceptance items in the`, "same proposal. The old contract closes as superseded, its unverified items", "remain in the audit record, the workspace is not restored, and this round", "cannot be completed.", "`scope` is your declared intent — a report, not permission. It is not", "enforced and it is not part of the rc- id.");
570
+ if (requirement.reason === "no_contract_capability") {
571
+ lines.push("No enabled after-capable command is configured now, so this contract's", "items cannot be machine-verified this round and no successor is required.");
572
+ }
573
+ else if (requirement.reason === "no_successor_round") {
574
+ lines.push("This is the final configured round. Do not submit a successor " +
575
+ "round_contract; there is no next round to consume it.");
576
+ }
577
+ else if (requirement.reason === "active_contract_closed") {
578
+ lines.push("This contract is already closed. If you continue, a successor", "`round_contract` is REQUIRED for the next round.");
579
+ }
580
+ else {
581
+ lines.push("If this round closes the contract (every item verified) and you continue,", "a successor `round_contract` is REQUIRED for the next round.");
582
+ }
546
583
  return lines;
547
584
  }
548
- if (!ctx.contractVerificationAvailable || ctx.commands.length === 0) {
585
+ if (requirement.reason === "no_contract_capability") {
586
+ return [
587
+ "No enabled after-capable evidence command is configured — no contract is possible.",
588
+ "If the OVERALL starting objective genuinely evolved, submit `objective_refinement`.",
589
+ ];
590
+ }
591
+ if (requirement.reason === "no_successor_round") {
549
592
  return [
550
- "No enabled after-capable evidence command is configured. Contract items cannot",
551
- "be machine-verified, so run without a contract and do not declare items.",
593
+ "This is the final configured round. Do not submit `round_contract`;",
594
+ "there is no successor round to consume a proposal.",
552
595
  ];
553
596
  }
554
597
  const commands = ctx.commands.slice(0, 3).join(", ");
555
- const proposalDecision = GUIDE_BY_PATH.get("round_contract")?.decision
556
- ?? "No active contract: if the next slice is machine-checkable, declare it";
557
- // The current requirement — not an invented slice — is the natural work
558
- // item. Real criterion ids/text and the real command are rendered; the
559
- // runtime still derives rci- ids and the Agent still decides whether this
560
- // is the right machine-checkable outcome.
561
598
  const example = fieldExample("round_contract", {
562
599
  activeContract: null,
563
600
  activeSubGoals: ctx.subGoals,
564
601
  commands: [...ctx.commands],
565
602
  contractVerificationAvailable: true,
566
603
  criteria: ctx.criteria,
567
- currentRequirement: ctx.currentRequirement,
568
604
  });
569
605
  return [
570
- "The current requirement is the natural work item — not an invented slice.",
571
- "Declare it as-is only if the command below is its acceptance check; otherwise",
572
- "describe the machine-checkable outcome and bind the command that actually",
573
- "verifies it. The proposal activates only after this round commits.",
574
- `${proposalDecision} → proposal below:`,
606
+ "No active Round Contract. If you continue, one is REQUIRED for the next",
607
+ "round. You decide the slice and acceptance conditions; LoopForge validates",
608
+ "structure, references and command bindings. Legal shape:",
575
609
  "```json",
576
610
  JSON.stringify({ round_contract: example }, null, 1),
577
611
  "```",
@@ -637,7 +671,7 @@ function renderCompactTriggers(ctx) {
637
671
  if (!guide?.decision)
638
672
  continue;
639
673
  lines.push(`- ${guide.decision} → \`${path}\` → ${guide.downstream}`);
640
- if (path === "execution_report.criterion_claims") {
674
+ if (path === "execution_report.claims") {
641
675
  lines.push(...criteriaDetailLines(ctx.criteria ?? []));
642
676
  }
643
677
  }
@@ -648,9 +682,9 @@ function renderCompactTriggers(ctx) {
648
682
  * the guidance lookup so the prompt always names the real field. */
649
683
  const FLAG_FIELD_PATHS = {
650
684
  files_changed: "execution_report.files_changed",
651
- tests_reported: "execution_report.tests_reported",
652
- criterion_claims: "execution_report.criterion_claims",
653
- contract_item_claims: "execution_report.contract_item_claims",
685
+ claims: "execution_report.claims",
686
+ criterion_claims: "execution_report.claims",
687
+ contract_item_claims: "execution_report.claims",
654
688
  };
655
689
  function flagFieldPath(field) {
656
690
  return FLAG_FIELD_PATHS[field] ?? field;
@@ -688,7 +722,7 @@ function renderFlagGuidance(flags, criteria = [], level = "l1") {
688
722
  lines.push(` → ${entry.label}: ${entry.action} [${check}]`);
689
723
  }
690
724
  }
691
- const criterionFinding = flags.some((flag) => flag.field === "execution_report.criterion_claims" ||
725
+ const criterionFinding = flags.some((flag) => flag.field === "execution_report.claims" ||
692
726
  flag.field === "criterion_claims" ||
693
727
  flag.check === "criteria_claims_unverified" ||
694
728
  flag.check === "success_with_remaining_criteria");
@@ -701,7 +735,7 @@ function renderFlagGuidance(flags, criteria = [], level = "l1") {
701
735
  /** Render the evaluation block appended to every prompt. */
702
736
  export function renderSelfEvalBlock(ctx) {
703
737
  const base = {
704
- success: false,
738
+ outcome: "partial",
705
739
  output_summary: "<what was done and how it was verified>",
706
740
  constraint_violations: [],
707
741
  should_continue: true,
@@ -712,7 +746,6 @@ export function renderSelfEvalBlock(ctx) {
712
746
  commands: ctx.commands,
713
747
  contractVerificationAvailable: ctx.contractVerificationAvailable,
714
748
  criteria: ctx.criteria ?? [],
715
- currentRequirement: ctx.currentRequirement,
716
749
  };
717
750
  const lines = [
718
751
  "### LoopForge Evaluation (Required)",
@@ -724,18 +757,17 @@ export function renderSelfEvalBlock(ctx) {
724
757
  "```json",
725
758
  JSON.stringify(base, null, 2),
726
759
  "```",
727
- "Add the execution report when the round changed files or ran tests:",
760
+ "Add the execution report when files changed or you claim items:",
728
761
  "```json",
729
762
  JSON.stringify({
730
763
  execution_report: {
731
764
  files_changed: fieldExample("execution_report.files_changed", reportCtx),
732
- tests_reported: fieldExample("execution_report.tests_reported", reportCtx),
765
+ claims: fieldExample("execution_report.claims", reportCtx) ?? [],
733
766
  },
734
767
  }, null, 1),
735
768
  "```",
736
- "`files_changed` is the net boundary change from round start (content or",
737
- "file type), including deletions. A commit alone adds no entry when content",
738
- "is unchanged. Do not list gitignored paths merely to look complete.",
769
+ "`files_changed` is the net change from round start, including deletions.",
770
+ "A commit alone adds nothing; gitignored paths are outside the machine boundary.",
739
771
  ];
740
772
  lines.push(...renderCompactTriggers(ctx));
741
773
  lines.push(...renderContractGuidance(ctx));
@@ -743,7 +775,7 @@ export function renderSelfEvalBlock(ctx) {
743
775
  if (ctx.recovery || ctx.verificationFlags.length > 0) {
744
776
  lines.push(...renderFlagGuidance(ctx.verificationFlags, ctx.criteria ?? [], ctx.level));
745
777
  }
746
- lines.push("Never invent an id. Use only ids rendered in this prompt or", "`submission_guidance`; when an id is absent, omit the field rather than", "writing a placeholder id.");
778
+ lines.push("Never invent an id; use only ids rendered here or in `submission_guidance`", "and omit the field when an id is absent.");
747
779
  return lines.join("\n");
748
780
  }
749
781
  function activeSubGoals(subGoals) {
@@ -758,27 +790,30 @@ export function deriveSubmissionGuidance(session, options) {
758
790
  const commandNames = capability.commands
759
791
  .filter((command) => command.enabled)
760
792
  .map((command) => command.commandId);
761
- const level = session.currentLevel === "l0" || session.currentLevel === "l1" ||
762
- session.currentLevel === "l2"
763
- ? session.currentLevel
764
- : (session.roundSnapshot?.promptArtifact?.level ??
765
- session.lastCompileResponse?.prompt_artifact?.level ?? "l2");
766
- const subGoals = session.lastCompileResponse?.sub_goals ??
767
- deriveSubGoals({
768
- loopId: session.loopId,
769
- currentRound: session.currentRound,
770
- rounds: options.rounds,
771
- });
793
+ // Never invent a prompt level. If no held artifact is observable, the
794
+ // level is unknown; the prompt itself is derived from current facts.
795
+ const level = session.roundSnapshot?.promptArtifact?.level ?? null;
796
+ const subGoals = deriveSubGoals({
797
+ loopId: session.loopId,
798
+ currentRound: session.currentRound,
799
+ rounds: options.rounds,
800
+ });
772
801
  const facts = deriveRoundFacts({
773
802
  rounds: options.rounds,
774
803
  currentRound: session.currentRound,
775
804
  inFlight: NO_IN_FLIGHT_ROUND,
776
- subGoals,
777
805
  commands: commandsPolicy,
778
806
  });
779
807
  const activeContract = facts.activeContract;
780
808
  const itemStatuses = facts.itemStatuses;
781
- const criteria = session.lastCompileResponse?.criterion_statuses ?? [];
809
+ const criteria = options.objective
810
+ ? deriveCriterionStatuses({
811
+ rounds: options.rounds,
812
+ currentRound: session.currentRound,
813
+ objective: options.objective,
814
+ criterionFacts: facts.criterionFacts,
815
+ })
816
+ : [];
782
817
  const guidanceFacts = {
783
818
  activeContract,
784
819
  itemStatuses,
@@ -794,18 +829,37 @@ export function deriveSubmissionGuidance(session, options) {
794
829
  commands: commandNames,
795
830
  contractVerificationAvailable: capability.contractVerificationAvailable,
796
831
  criteria,
797
- currentRequirement: session.task?.trim() || activeContract?.work_item || "",
798
832
  };
799
- const artifact = session.roundSnapshot?.promptArtifact ??
800
- session.lastCompileResponse?.prompt_artifact;
801
- const roundId = session.roundSnapshot?.roundId ??
802
- `loop:${session.loopId}:round:${session.currentRound}`;
833
+ const artifact = session.roundSnapshot?.promptArtifact;
803
834
  return {
804
- round_id: roundId,
835
+ contract_requirement: (() => {
836
+ const requirement = deriveContractRequirement({
837
+ shouldContinue: true,
838
+ blocked: false,
839
+ proposalPresent: false,
840
+ activeContract,
841
+ closure: itemStatuses.closure,
842
+ contractCapability: capability.contractVerificationAvailable,
843
+ successorRoundAvailable: session.maxRounds === undefined || session.currentRound < session.maxRounds,
844
+ });
845
+ return {
846
+ required_on_continue: requirement.required,
847
+ successor_required_if_closed: activeContract !== null && capability.contractVerificationAvailable &&
848
+ session.currentRound < (session.maxRounds ?? Number.POSITIVE_INFINITY),
849
+ reason: requirement.reason === "active_contract_closed" ||
850
+ requirement.reason === "active_contract_continues" ||
851
+ requirement.reason === "no_contract_capability" ||
852
+ requirement.reason === "no_successor_round"
853
+ ? requirement.reason
854
+ : "no_active_contract",
855
+ active_contract_id: activeContract?.id ?? null,
856
+ };
857
+ })(),
805
858
  active_contract: activeContract
806
859
  ? {
807
860
  id: activeContract.id,
808
861
  ...(activeContract.work_item ? { work_item: activeContract.work_item } : {}),
862
+ scope: [...activeContract.scope],
809
863
  declared_at_round: activeContract.declared_at_round,
810
864
  items: activeContract.items.map((item) => ({
811
865
  id: item.id,
@@ -827,13 +881,7 @@ export function deriveSubmissionGuidance(session, options) {
827
881
  text: criterion.text,
828
882
  status: criterion.status,
829
883
  })),
830
- recovery: options.recovery ?? (session.backtrackTargetGitHead
831
- ? {
832
- required: true,
833
- restore_point: Math.max(1, session.currentRound - 1),
834
- target_head: session.backtrackTargetGitHead,
835
- }
836
- : null),
884
+ recovery: options.recovery ?? null,
837
885
  prompt: {
838
886
  level,
839
887
  dropped_sections: [...(artifact?.droppedSections ?? [])],