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
@@ -11,47 +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";
16
+ import { verificationActionFor } from "./verification-actions.js";
17
+ import { deriveContractRequirement } from "./contract-requirement.js";
15
18
  /** Field order used by the prompt, guidance and receipts. Core fields first,
16
19
  * then the execution report, then contract/sub-goal state, then the
17
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
+ ]);
18
29
  export const FIELD_ORDER = [
19
- "success",
30
+ "outcome",
20
31
  "output_summary",
21
32
  "constraint_violations",
22
33
  "should_continue",
23
34
  "execution_report.files_changed",
24
- "execution_report.tests_reported",
25
- "execution_report.contract_item_claims",
26
- "execution_report.criterion_claims",
35
+ "execution_report.claims",
36
+ "objective_refinement",
27
37
  "subgoal_updates",
28
38
  "emerged_subtasks",
29
39
  "discovered_constraints",
30
40
  "wrong_assumptions",
31
- "objective_refinement",
41
+ "open_questions",
42
+ "resolved_open_question_ids",
32
43
  "retracted_constraints",
33
- "revised_success_criteria",
34
44
  "round_contract",
35
45
  "worker_results",
36
- "prompt_requests",
37
- "compression_checkpoint",
38
- "checkpoint_label",
46
+ "subtask_boundary",
47
+ "subtask_boundary_label",
39
48
  "retroactiveClaims",
40
49
  "gate_ids",
41
- "outcome",
42
50
  "blocker",
43
- "stop_reason",
44
51
  "no_change_reason",
45
52
  ];
46
53
  const GUIDES = [
47
54
  {
48
- path: "success",
49
- purpose: "Whether the whole objective and every hard constraint are satisfied.",
50
- 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.",
51
59
  nature: "claim",
52
- downstream: "Stop decision, success trajectory, verification posture.",
53
- toolDescription: "true ONLY if all hard constraints were met AND the task goal was achieved. " +
54
- "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.",
55
66
  core: true,
56
67
  },
57
68
  {
@@ -59,13 +70,15 @@ const GUIDES = [
59
70
  purpose: "What was actually done this round, not what was attempted.",
60
71
  when: "Every round.",
61
72
  nature: "report",
62
- 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.",
63
75
  toolDescription: "Specific, actionable summary of what was DONE this round — not what was " +
64
76
  "attempted. Do not restate the plan.",
65
77
  core: true,
66
78
  },
67
79
  {
68
80
  path: "constraint_violations",
81
+ array: true,
69
82
  purpose: "Hard constraints actually violated this round.",
70
83
  when: "Every round; empty when none were violated.",
71
84
  nature: "report",
@@ -86,6 +99,7 @@ const GUIDES = [
86
99
  },
87
100
  {
88
101
  path: "execution_report.files_changed",
102
+ array: true,
89
103
  purpose: "The boundary net change: paths whose content or file type differs between " +
90
104
  "round preparation and this submission, including deleted paths.",
91
105
  when: "Every round that changed the workspace. A commit alone adds no entry when content is unchanged.",
@@ -98,42 +112,43 @@ const GUIDES = [
98
112
  reportOnly: false,
99
113
  },
100
114
  {
101
- path: "execution_report.tests_reported",
102
- purpose: "The agent's own test counts.",
103
- when: "When tests were run (or a test run is relevant to the claim).",
104
- nature: "report",
105
- downstream: "Compared with parsed command output when available; never converted into a verification verdict.",
106
- toolDescription: "Self-reported test counts. Informational: the runtime never translates " +
107
- "an exit code into a test count, and the receipt shows self-report and " +
108
- "command observation separately.",
109
- },
110
- {
111
- path: "execution_report.contract_item_claims",
112
- purpose: "Claims about items of the ACTIVE Round Contract.",
113
- 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.",
114
120
  nature: "claim",
115
- decision: "Claim completed items of the active contract with their real rci- ids",
116
- downstream: "Starts machine evaluation of the item; the item becomes verified only when its bound commands pass.",
117
- toolDescription: "Claims about items of the ACTIVE contract, cited by the rci-XXXXXXXX id " +
118
- "the prompt rendered. A claim never verifies anything by itself — the " +
119
- "runtime marks an item verified only when its bound commands are observed " +
120
- "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.",
121
132
  },
122
133
  {
123
- path: "execution_report.criterion_claims",
124
- purpose: "Advisory claims about objective criteria.",
125
- when: "When you want to state which criteria are met or still remaining.",
126
- nature: "claim",
127
- decision: "You can state which criteria are met or still outstanding",
128
- promptLevels: ["l1", "l2"],
129
- promptRank: 10,
130
- downstream: "Advisory only: unbacked claims warn; they never upgrade a criterion to verified.",
131
- toolDescription: "Advisory claims about objective criteria. LENIENT: unknown or malformed " +
132
- "ids are dropped with a diagnostic, never a rejection. A criterion claim " +
133
- "never becomes machine-verified by itself.",
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)",
141
+ compact: true,
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.",
134
148
  },
135
149
  {
136
150
  path: "subgoal_updates",
151
+ array: true,
137
152
  purpose: "Explicit status transitions for existing sub-goals.",
138
153
  when: "When an active sg-XXXXXXXX sub-goal starts, is blocked, completes, or is canceled.",
139
154
  nature: "transition",
@@ -147,142 +162,171 @@ const GUIDES = [
147
162
  },
148
163
  {
149
164
  path: "emerged_subtasks",
150
- purpose: "New sub-problems that surfaced during execution.",
151
- 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.",
152
169
  nature: "transition",
153
170
  decision: "A new sub-problem surfaced (plain text, never sg- ids)",
154
- promptLevels: ["l1", "l2"],
171
+ compact: true,
155
172
  promptRank: 40,
156
173
  downstream: "The only sub-goal creation channel; the runtime derives stable sg- ids.",
157
- toolDescription: "Sub-problems that surfaced during execution. The ONLY sub-goal creation " +
158
- "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.",
159
178
  },
160
179
  {
161
180
  path: "discovered_constraints",
181
+ array: true,
162
182
  purpose: "New constraints that later rounds must respect.",
163
183
  when: "When this round discovered a limit that was not known before.",
164
184
  nature: "report",
165
185
  decision: "You discovered a limit later rounds must respect",
166
- promptLevels: ["l0", "l1", "l2"],
186
+ compact: true,
167
187
  promptRank: 20,
168
- 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.",
169
191
  toolDescription: "New constraints that must apply to later rounds. This is the ONLY " +
170
192
  "promotion channel for a worker-discovered constraint: a worker result " +
171
193
  "stays a report until the main agent states the constraint here.",
172
194
  },
173
195
  {
174
196
  path: "wrong_assumptions",
197
+ array: true,
175
198
  purpose: "Assumptions from earlier rounds that this round falsified.",
176
199
  when: "When evidence proved an earlier assumption wrong; omit when none.",
177
200
  nature: "report",
178
201
  decision: "Evidence falsified an earlier assumption",
179
- promptLevels: ["l0", "l1", "l2"],
202
+ compact: true,
180
203
  promptRank: 30,
181
- 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.",
182
207
  toolDescription: "Assumptions from earlier rounds that turned out to be wrong. Record one " +
183
208
  "only when this round actually falsified it; otherwise omit. This is not " +
184
209
  "a reflection quota.",
185
210
  },
186
211
  {
187
- path: "objective_refinement",
188
- purpose: "A deeper or more accurate understanding of the objective.",
189
- 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.",
190
218
  nature: "report",
191
- downstream: "Appended to the objective with a version bump; never replaces the original objective.",
192
- toolDescription: "Optional. A refinement or deepening of the task objective based on this " +
193
- "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.",
194
230
  },
195
231
  {
196
- path: "retracted_constraints",
197
- purpose: "Existing constraints that are no longer correct or relevant.",
198
- 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.",
199
236
  nature: "report",
200
- downstream: "Retired from the active set (with the configured retirement window for plan/hard constraints).",
201
- toolDescription: "Optional. Constraints the agent now believes are wrong or irrelevant, by " +
202
- "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.",
203
245
  },
204
246
  {
205
- path: "revised_success_criteria",
206
- purpose: "Success criteria that need reformulation.",
207
- 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.",
208
251
  nature: "report",
209
- downstream: "Applied to the Loop Objective with a version bump.",
210
- toolDescription: "Optional. Success criteria that need reformulation: each entry has the " +
211
- "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.",
212
259
  },
213
260
  {
214
261
  path: "round_contract",
215
- purpose: "A proposal for the NEXT round's evidence-bound contract.",
216
- when: "When no contract is active and a contract command is available; while a " +
217
- "contract is open, omit it (or restate it unchanged) unless this round " +
218
- "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.",
219
268
  nature: "proposal",
220
- 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",
221
270
  downstream: "Becomes active only after this round commits; item ids rci-XXXXXXXX are " +
222
- "derived by the runtime.",
223
- toolDescription: "Proposal for the NEXT round's contract (items + verify_with + scope). " +
224
- "Declared here, it becomes the ACTIVE contract only after this round " +
225
- "commits. While it is open, omitting it continues the contract; a " +
226
- "different proposal is ignored (contract_premature). DECLARATION IS " +
227
- "STRICT: an item with no bound command, an unknown/disabled command, a " +
228
- "scope entry outside the workspace, a malformed reference, or a count " +
229
- "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.",
230
288
  },
231
289
  {
232
290
  path: "worker_results",
291
+ array: true,
233
292
  purpose: "Results of sub-agent / worker delegations.",
234
293
  when: "Only when work was actually delegated.",
235
294
  nature: "report",
236
295
  decision: "You delegated work",
237
- promptLevels: ["l1", "l2"],
296
+ compact: true,
238
297
  promptRank: 50,
239
- downstream: "Saved with its source and written to the delegation journal; worker " +
240
- "discoveredConstraints is NOT auto-promoted.",
298
+ downstream: "Saved inside the committed transaction as an audit report. A worker " +
299
+ "finding is not auto-promoted.",
241
300
  toolDescription: "Optional. Results of sub-agent / Worker delegations this round. Each " +
242
301
  "entry must declare an outcome. Entries without a valid outcome are " +
243
- "dropped with a diagnostic. A worker's discoveredConstraints stays in " +
244
- "this report; to make it a global constraint, submit it in top-level " +
245
- "discovered_constraints.",
302
+ "dropped with a diagnostic. To make a worker finding a global " +
303
+ "constraint, submit it in top-level discovered_constraints.",
246
304
  reportOnly: true,
247
305
  },
248
306
  {
249
- path: "prompt_requests",
250
- purpose: "Information needs for the next round's prompt.",
251
- when: "When you know what context the next round will need.",
252
- nature: "control",
253
- decision: "You know what the next prompt needs",
254
- promptLevels: ["l1", "l2"],
255
- promptRank: 70,
256
- downstream: "Matched exactly against state items; applied to the next prompt only " +
257
- "(L0 ignores it). Unmatched entries have no effect.",
258
- toolDescription: "Optional. Information needs for the NEXT prompt. emphasize entries are " +
259
- "matched by stable id or normalized-exact text; a paraphrase matches " +
260
- "nothing. confusion_points are echoed at the top. L0 retry prompts ignore " +
261
- "both arms. This never becomes long-term knowledge.",
262
- },
263
- {
264
- path: "compression_checkpoint",
307
+ path: "subtask_boundary",
265
308
  purpose: "Mark a meaningful work boundary.",
266
- when: "At a phase/subtask boundary worth a full-state milestone.",
309
+ when: "At a subtask boundary that warrants an L2 rehydration.",
267
310
  nature: "report",
268
- decision: "This round reached a real phase boundary; pair with checkpoint_label",
269
- promptLevels: ["l1", "l2"],
311
+ decision: "This round reached a real phase boundary; pair with subtask_boundary_label",
312
+ compact: true,
270
313
  promptRank: 60,
271
- downstream: "Creates an agent-declared milestone and forces an L2 rehydration boundary.",
272
- toolDescription: "Optional. true at a subtask boundary to create a recoverable milestone " +
273
- "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.",
274
317
  },
275
318
  {
276
- path: "checkpoint_label",
277
- purpose: "A human-readable label for the checkpoint.",
278
- 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.",
279
322
  nature: "report",
280
- downstream: "Milestone heading in later prompts and the state file.",
281
- toolDescription: "Optional. Short label for the checkpoint (for example " +
282
- "'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.",
283
326
  },
284
327
  {
285
328
  path: "retroactiveClaims",
329
+ array: true,
286
330
  purpose: "A claim that an earlier round satisfied a criterion.",
287
331
  when: "When you can point at an earlier round's evidence.",
288
332
  nature: "claim",
@@ -293,6 +337,7 @@ const GUIDES = [
293
337
  },
294
338
  {
295
339
  path: "gate_ids",
340
+ array: true,
296
341
  purpose: "Approved gate ids this round's work depended on.",
297
342
  when: "Only when policy.gate.enabled is true and an approval was actually obtained.",
298
343
  nature: "control",
@@ -301,34 +346,16 @@ const GUIDES = [
301
346
  "Enforced only when policy.gate.enabled: a cited, unapproved gate rejects " +
302
347
  "the round. Without the gate layer this is saved but has no effect.",
303
348
  },
304
- {
305
- path: "outcome",
306
- purpose: "The declared round outcome.",
307
- when: "When the round is partial, failed, or blocked (success is derivable).",
308
- nature: "claim",
309
- downstream: "Wins over `success` for the stop decision and success-class checks.",
310
- toolDescription: "Optional. success | partial | failed | blocked. When absent, derived " +
311
- "from success. A declared non-success outcome suppresses the " +
312
- "success-class checks even when success=true.",
313
- },
314
349
  {
315
350
  path: "blocker",
316
351
  purpose: "A concrete description of what blocks the work.",
317
- when: "Required when outcome=blocked; useful with stop_reason=blocked.",
352
+ when: "Required when outcome=blocked; ignored (with a diagnostic) otherwise.",
318
353
  nature: "report",
319
- downstream: "Stop decision, handoff open risks, and gate classification when enabled.",
320
- toolDescription: "Optional. A flat blocker description, required when outcome=blocked. " +
321
- "State what cannot proceed and why; do not use it as a substitute for " +
322
- "`success=false`.",
323
- },
324
- {
325
- path: "stop_reason",
326
- purpose: "Why the agent is stopping.",
327
- when: "Only when should_continue=false and success=false.",
328
- nature: "control",
329
- downstream: "blocked/needs_human_input stop without claiming success; gave_up is the default stop behavior.",
330
- toolDescription: "Optional. Why the agent is stopping: gave_up | blocked | needs_human_input. " +
331
- "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.",
332
359
  },
333
360
  {
334
361
  path: "no_change_reason",
@@ -343,13 +370,31 @@ const GUIDES = [
343
370
  },
344
371
  ];
345
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));
346
377
  /** Fields that existed in earlier releases and are now unknown. A diagnostic
347
378
  * for one of these names explains the removal instead of silently dropping. */
348
379
  const RETIRED_FIELDS = {
349
380
  progress_estimate: "removed in 3.9.0 — progress is machine-derived; remove this field",
350
381
  progress_at_boundary: "removed in 3.9.0 — milestones render a round range, not a percentage",
351
- 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",
352
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",
353
398
  };
354
399
  export function fieldGuide(path) {
355
400
  return GUIDE_BY_PATH.get(path);
@@ -375,24 +420,21 @@ function firstActiveSubGoal(subGoals) {
375
420
  * never invents an sg-/rci- reference. */
376
421
  export function fieldExample(path, ctx) {
377
422
  switch (path) {
378
- case "success": return false;
423
+ case "outcome": return "partial";
379
424
  case "output_summary": return "<what was done and how it was verified>";
380
425
  case "constraint_violations": return [];
381
426
  case "should_continue": return true;
382
427
  case "execution_report.files_changed":
383
428
  return ["<path whose content or type differs from round start>"];
384
- case "execution_report.tests_reported":
385
- return { passed: 0, failed: 0, skipped: 0 };
386
- case "execution_report.contract_item_claims": {
429
+ case "execution_report.claims": {
387
430
  const item = firstActiveItem(ctx.activeContract);
388
- return item ? [{ item_id: item.id, outcome: "met" }] : null;
389
- }
390
- case "execution_report.criterion_claims": {
431
+ if (item)
432
+ return [{ id: item.id, outcome: "met" }];
391
433
  const criterion = ctx.criteria?.[0];
392
- return criterion
393
- ? [{ criterion_id: criterion.id, outcome: "met" }]
394
- : null;
434
+ return criterion ? [{ id: criterion.id, outcome: "met" }] : null;
395
435
  }
436
+ case "objective_refinement":
437
+ return "<the overall objective as it now stands>";
396
438
  case "subgoal_updates": {
397
439
  const subGoal = firstActiveSubGoal(ctx.activeSubGoals);
398
440
  return subGoal ? [{ id: subGoal.id, status: "done" }] : null;
@@ -403,58 +445,47 @@ export function fieldExample(path, ctx) {
403
445
  return ["<a constraint later rounds must respect>"];
404
446
  case "wrong_assumptions":
405
447
  return ["<an assumption from an earlier round that was falsified>"];
406
- case "objective_refinement":
407
- 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>"];
408
452
  case "retracted_constraints":
409
453
  return ["<constraint text or c-XXXXXXXX id that is no longer valid>"];
410
- case "revised_success_criteria":
411
- return [{ old: "<old criterion>", new: "<new criterion>" }];
412
454
  case "round_contract": {
413
455
  if (!ctx.contractVerificationAvailable || ctx.commands.length === 0)
414
456
  return null;
415
457
  return {
416
- work_item: "<the next slice, in one line>",
458
+ work_item: "<bounded deliverable this slice produces>",
417
459
  items: [{
418
- description: "<machine-checkable outcome for the next slice>",
460
+ description: "<machine-checkable acceptance condition>",
419
461
  criterion_refs: [],
420
- subgoal_refs: [],
421
462
  verify_with: [ctx.commands[0]],
422
463
  }],
423
- scope: ["<workspace-relative file or directory>"],
424
464
  };
425
465
  }
426
466
  case "worker_results":
427
467
  return [{
428
468
  agentId: "<worker id>",
429
- subAgentType: "general-purpose",
430
469
  subTask: "<self-contained subtask you delegated>",
431
470
  resultSummary: "<what the worker reported>",
432
471
  outcome: "success",
433
- discoveredConstraints: [],
434
472
  }];
435
- case "prompt_requests":
436
- return {
437
- emphasize: ["<exact constraint text or stable id>"],
438
- confusion_points: ["<what is unclear about the current state>"],
439
- };
440
- case "compression_checkpoint": return false;
441
- case "checkpoint_label": return "<short phase label>";
473
+ case "subtask_boundary": return false;
474
+ case "subtask_boundary_label": return "<short phase label>";
442
475
  case "retroactiveClaims":
443
476
  return [{ round: 1, claim: "<what that earlier round satisfied>" }];
444
477
  case "gate_ids":
445
478
  return ["<approved gateId from loopforge_gate_check>"];
446
- case "outcome": return "partial";
447
479
  case "blocker": return "<what cannot proceed and why>";
448
- case "stop_reason": return "blocked";
449
480
  case "no_change_reason": return "<why no workspace change was needed>";
450
481
  default: return null;
451
482
  }
452
483
  }
453
- /** Semantic applicability — independent of prompt density. Core fields and
454
- * the two baseline execution-report fields are always applicable; the rest
455
- * depend on facts the runtime can actually observe. Future events
456
- * (delegation, a checkpoint boundary, a falsified assumption) remain
457
- * conditional opportunities, not render-time facts. */
484
+ /** Semantic applicability — the ONE rule, deliberately independent of prompt
485
+ * level. Core fields and the two baseline execution-report fields are always
486
+ * applicable; the rest depend on facts the runtime can actually observe.
487
+ * Future events (delegation, a subtask boundary, a falsified assumption)
488
+ * remain conditional opportunities, not render-time facts. */
458
489
  export function isFieldApplicable(path, facts) {
459
490
  const guide = GUIDE_BY_PATH.get(path);
460
491
  if (!guide)
@@ -463,21 +494,18 @@ export function isFieldApplicable(path, facts) {
463
494
  return true;
464
495
  switch (path) {
465
496
  case "execution_report.files_changed":
466
- case "execution_report.tests_reported":
467
497
  return true;
468
- case "execution_report.contract_item_claims":
469
- return facts.activeContract !== null;
470
- case "execution_report.criterion_claims":
471
- 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;
472
502
  case "subgoal_updates":
473
503
  return firstActiveSubGoal(facts.subGoals) !== null;
474
504
  case "round_contract":
475
- // A first declaration needs no active contract and a usable command.
476
- // While a contract is open, the dedicated active-contract block carries
477
- // the successor instruction instead of a competing proposal example.
478
- return facts.activeContract === null &&
479
- facts.contractVerificationAvailable &&
480
- 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;
481
509
  case "gate_ids":
482
510
  return facts.gateEnabled;
483
511
  case "no_change_reason":
@@ -486,13 +514,14 @@ export function isFieldApplicable(path, facts) {
486
514
  return true;
487
515
  }
488
516
  }
489
- /** Prompt density is a separate decision: which applicable fields the compact
490
- * trigger checklist renders at this level. */
517
+ /** Compact-checklist membership: the explicit `compact` marker plus semantic
518
+ * applicability. The level is deliberately absent — L0/L1/L2 offer the same
519
+ * evaluation fields; only state density changes. Dedicated renderer fields
520
+ * (`round_contract`, `subgoal_updates`) are never compact members, so they
521
+ * cannot duplicate their own blocks. */
491
522
  export function isFieldPrompted(path, facts) {
492
523
  const guide = GUIDE_BY_PATH.get(path);
493
- if (!guide?.decision || !guide.promptLevels)
494
- return false;
495
- if (!guide.promptLevels.includes(facts.level))
524
+ if (!guide?.compact)
496
525
  return false;
497
526
  return isFieldApplicable(path, facts);
498
527
  }
@@ -508,10 +537,23 @@ function statusOfItem(itemId, statuses) {
508
537
  }
509
538
  function renderContractGuidance(ctx) {
510
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
+ });
511
553
  if (contract) {
512
554
  const lines = [
513
- "Active Round Contract — continue it; you do NOT need to restate it:",
514
- `- ${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})`,
515
557
  ];
516
558
  const shown = contract.items.slice(0, 5);
517
559
  for (const item of shown) {
@@ -522,37 +564,52 @@ function renderContractGuidance(ctx) {
522
564
  if (contract.items.length > shown.length) {
523
565
  lines.push(` - … and ${contract.items.length - shown.length} more in the Current Task.`);
524
566
  }
525
- const claimDecision = GUIDE_BY_PATH.get("execution_report.contract_item_claims")?.decision
526
- ?? "Claim completed items of the active contract with their real rci- ids";
527
- 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
+ }
528
583
  return lines;
529
584
  }
530
- if (!ctx.contractVerificationAvailable || ctx.commands.length === 0) {
585
+ if (requirement.reason === "no_contract_capability") {
531
586
  return [
532
- "No enabled after-capable evidence command is configured. Contract items cannot",
533
- "be machine-verified, so run without a contract and do not declare items.",
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") {
592
+ return [
593
+ "This is the final configured round. Do not submit `round_contract`;",
594
+ "there is no successor round to consume a proposal.",
534
595
  ];
535
596
  }
536
597
  const commands = ctx.commands.slice(0, 3).join(", ");
537
- const proposalDecision = GUIDE_BY_PATH.get("round_contract")?.decision
538
- ?? "No active contract: if the next slice is machine-checkable, declare it";
598
+ const example = fieldExample("round_contract", {
599
+ activeContract: null,
600
+ activeSubGoals: ctx.subGoals,
601
+ commands: [...ctx.commands],
602
+ contractVerificationAvailable: true,
603
+ criteria: ctx.criteria,
604
+ });
539
605
  return [
540
- `${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:",
541
609
  "```json",
542
- JSON.stringify({
543
- round_contract: {
544
- work_item: "<next slice, one line>",
545
- items: [{
546
- description: "<machine-checkable outcome>",
547
- criterion_refs: [],
548
- subgoal_refs: [],
549
- verify_with: [ctx.commands[0]],
550
- }],
551
- scope: ["<workspace-relative path>"],
552
- },
553
- }, null, 1),
610
+ JSON.stringify({ round_contract: example }, null, 1),
554
611
  "```",
555
- `Usable commands: ${commands}. The proposal activates only after this round commits.`,
612
+ `Usable commands: ${commands}.`,
556
613
  ];
557
614
  }
558
615
  function renderSubGoalGuidance(ctx) {
@@ -593,7 +650,6 @@ function criteriaDetailLines(criteria) {
593
650
  * per-field shapes remain in `submission_guidance`. */
594
651
  function renderCompactTriggers(ctx) {
595
652
  const facts = {
596
- level: ctx.level,
597
653
  activeContract: ctx.activeContract,
598
654
  itemStatuses: ctx.itemStatuses,
599
655
  subGoals: ctx.subGoals,
@@ -615,37 +671,71 @@ function renderCompactTriggers(ctx) {
615
671
  if (!guide?.decision)
616
672
  continue;
617
673
  lines.push(`- ${guide.decision} → \`${path}\` → ${guide.downstream}`);
618
- if (path === "execution_report.criterion_claims") {
674
+ if (path === "execution_report.claims") {
619
675
  lines.push(...criteriaDetailLines(ctx.criteria ?? []));
620
676
  }
621
677
  }
622
678
  return lines;
623
679
  }
624
- function renderFlagGuidance(flags, criteria = []) {
680
+ /** Verification flags name the concrete evaluation field, which is often the
681
+ * dotted path the Agent must fill. Normalize the legacy short names before
682
+ * the guidance lookup so the prompt always names the real field. */
683
+ const FLAG_FIELD_PATHS = {
684
+ files_changed: "execution_report.files_changed",
685
+ claims: "execution_report.claims",
686
+ criterion_claims: "execution_report.claims",
687
+ contract_item_claims: "execution_report.claims",
688
+ };
689
+ function flagFieldPath(field) {
690
+ return FLAG_FIELD_PATHS[field] ?? field;
691
+ }
692
+ function renderFlagGuidance(flags, criteria = [], level = "l1") {
625
693
  if (flags.length === 0)
626
694
  return [];
627
- const fields = [...new Set(flags.map((flag) => flag.field).filter(Boolean))];
695
+ const fields = [
696
+ ...new Set(flags.map((flag) => flagFieldPath(flag.field)).filter(Boolean)),
697
+ ];
628
698
  const lines = [
629
- "This submission is a retry or recovery. The previous findings name these fields:",
699
+ "Previous machine findings name these fields:",
630
700
  ...fields.slice(0, 8).map((field) => {
631
701
  const guide = GUIDE_BY_PATH.get(field);
632
702
  return `- \`${field}\`${guide ? ` — ${guide.purpose}` : ""}`;
633
703
  }),
634
704
  ];
635
- const criterionFinding = flags.some((flag) => flag.field === "execution_report.criterion_claims" ||
705
+ // L0's Verification Gate deliberately stays compact and carries no action
706
+ // continuation. The evaluation block is the Agent's composition surface,
707
+ // so the concrete action belongs here. L1/L2 already render it once in the
708
+ // gate; do not duplicate it.
709
+ if (level === "l0") {
710
+ const actions = new Map();
711
+ for (const flag of flags) {
712
+ if (flag.severity === "info" || actions.has(flag.check))
713
+ continue;
714
+ actions.set(flag.check, {
715
+ label: flag.severity === "error" ? "Fix" : "Action",
716
+ action: verificationActionFor(flag.check),
717
+ });
718
+ if (actions.size >= 3)
719
+ break;
720
+ }
721
+ for (const [check, entry] of actions) {
722
+ lines.push(` → ${entry.label}: ${entry.action} [${check}]`);
723
+ }
724
+ }
725
+ const criterionFinding = flags.some((flag) => flag.field === "execution_report.claims" ||
636
726
  flag.field === "criterion_claims" ||
637
727
  flag.check === "criteria_claims_unverified" ||
638
728
  flag.check === "success_with_remaining_criteria");
639
729
  if (criterionFinding && criteria.length > 0) {
640
730
  lines.push(...criteriaDetailLines(criteria));
641
731
  }
642
- lines.push("Correct the relevant field(s) in the same round. If this attempt falsified an", "earlier assumption, record it in `wrong_assumptions`; otherwise omit it.");
732
+ lines.push("Correct the relevant field(s) in this round if the finding still applies.", "If this attempt falsified an earlier assumption, record it in", "`wrong_assumptions`; otherwise omit it.");
643
733
  return lines;
644
734
  }
645
735
  /** Render the evaluation block appended to every prompt. */
646
736
  export function renderSelfEvalBlock(ctx) {
647
737
  const base = {
648
- success: false,
738
+ outcome: "partial",
649
739
  output_summary: "<what was done and how it was verified>",
650
740
  constraint_violations: [],
651
741
  should_continue: true,
@@ -667,26 +757,25 @@ export function renderSelfEvalBlock(ctx) {
667
757
  "```json",
668
758
  JSON.stringify(base, null, 2),
669
759
  "```",
670
- "Add the execution report when the round changed files or ran tests:",
760
+ "Add the execution report when files changed or you claim items:",
671
761
  "```json",
672
762
  JSON.stringify({
673
763
  execution_report: {
674
764
  files_changed: fieldExample("execution_report.files_changed", reportCtx),
675
- tests_reported: fieldExample("execution_report.tests_reported", reportCtx),
765
+ claims: fieldExample("execution_report.claims", reportCtx) ?? [],
676
766
  },
677
767
  }, null, 1),
678
768
  "```",
679
- "`files_changed` is the net boundary change from round start (content or",
680
- "file type), including deletions. A commit alone adds no entry when content",
681
- "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.",
682
771
  ];
683
772
  lines.push(...renderCompactTriggers(ctx));
684
773
  lines.push(...renderContractGuidance(ctx));
685
774
  lines.push(...renderSubGoalGuidance(ctx));
686
775
  if (ctx.recovery || ctx.verificationFlags.length > 0) {
687
- lines.push(...renderFlagGuidance(ctx.verificationFlags, ctx.criteria ?? []));
776
+ lines.push(...renderFlagGuidance(ctx.verificationFlags, ctx.criteria ?? [], ctx.level));
688
777
  }
689
- 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.");
690
779
  return lines.join("\n");
691
780
  }
692
781
  function activeSubGoals(subGoals) {
@@ -701,29 +790,31 @@ export function deriveSubmissionGuidance(session, options) {
701
790
  const commandNames = capability.commands
702
791
  .filter((command) => command.enabled)
703
792
  .map((command) => command.commandId);
704
- const level = session.currentLevel === "l0" || session.currentLevel === "l1" ||
705
- session.currentLevel === "l2"
706
- ? session.currentLevel
707
- : (session.roundSnapshot?.promptArtifact?.level ??
708
- session.lastCompileResponse?.prompt_artifact?.level ?? "l2");
709
- const subGoals = session.lastCompileResponse?.sub_goals ??
710
- deriveSubGoals({
711
- loopId: session.loopId,
712
- currentRound: session.currentRound,
713
- rounds: options.rounds,
714
- });
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
+ });
715
801
  const facts = deriveRoundFacts({
716
802
  rounds: options.rounds,
717
803
  currentRound: session.currentRound,
718
804
  inFlight: NO_IN_FLIGHT_ROUND,
719
- subGoals,
720
805
  commands: commandsPolicy,
721
806
  });
722
807
  const activeContract = facts.activeContract;
723
808
  const itemStatuses = facts.itemStatuses;
724
- 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
+ : [];
725
817
  const guidanceFacts = {
726
- level,
727
818
  activeContract,
728
819
  itemStatuses,
729
820
  subGoals,
@@ -739,16 +830,36 @@ export function deriveSubmissionGuidance(session, options) {
739
830
  contractVerificationAvailable: capability.contractVerificationAvailable,
740
831
  criteria,
741
832
  };
742
- const artifact = session.roundSnapshot?.promptArtifact ??
743
- session.lastCompileResponse?.prompt_artifact;
744
- const roundId = session.roundSnapshot?.roundId ??
745
- `loop:${session.loopId}:round:${session.currentRound}`;
833
+ const artifact = session.roundSnapshot?.promptArtifact;
746
834
  return {
747
- 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
+ })(),
748
858
  active_contract: activeContract
749
859
  ? {
750
860
  id: activeContract.id,
751
861
  ...(activeContract.work_item ? { work_item: activeContract.work_item } : {}),
862
+ scope: [...activeContract.scope],
752
863
  declared_at_round: activeContract.declared_at_round,
753
864
  items: activeContract.items.map((item) => ({
754
865
  id: item.id,
@@ -770,13 +881,7 @@ export function deriveSubmissionGuidance(session, options) {
770
881
  text: criterion.text,
771
882
  status: criterion.status,
772
883
  })),
773
- recovery: options.recovery ?? (session.backtrackTargetGitHead
774
- ? {
775
- required: true,
776
- restore_point: Math.max(1, session.currentRound - 1),
777
- target_head: session.backtrackTargetGitHead,
778
- }
779
- : null),
884
+ recovery: options.recovery ?? null,
780
885
  prompt: {
781
886
  level,
782
887
  dropped_sections: [...(artifact?.droppedSections ?? [])],
@@ -798,6 +903,7 @@ export function deriveSubmissionGuidance(session, options) {
798
903
  check: flag.check,
799
904
  severity: flag.severity,
800
905
  detail: flag.detail,
906
+ ...(flag.ref ? { ref: flag.ref } : {}),
801
907
  })),
802
908
  };
803
909
  }