loopforge 3.7.1 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +171 -169
  2. package/dist/audit.d.ts +53 -1
  3. package/dist/audit.d.ts.map +1 -1
  4. package/dist/audit.js +106 -22
  5. package/dist/audit.js.map +1 -1
  6. package/dist/canonical-state.d.ts +52 -67
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +134 -147
  9. package/dist/canonical-state.js.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +103 -4
  12. package/dist/cli.js.map +1 -1
  13. package/dist/cognitive-facts.d.ts +13 -0
  14. package/dist/cognitive-facts.d.ts.map +1 -1
  15. package/dist/cognitive-facts.js +28 -13
  16. package/dist/cognitive-facts.js.map +1 -1
  17. package/dist/committed-round.d.ts +103 -21
  18. package/dist/committed-round.d.ts.map +1 -1
  19. package/dist/committed-round.js +188 -45
  20. package/dist/committed-round.js.map +1 -1
  21. package/dist/contract-items.d.ts +55 -0
  22. package/dist/contract-items.d.ts.map +1 -0
  23. package/dist/contract-items.js +180 -0
  24. package/dist/contract-items.js.map +1 -0
  25. package/dist/enforcement-gate.d.ts +20 -2
  26. package/dist/enforcement-gate.d.ts.map +1 -1
  27. package/dist/enforcement-gate.js +197 -386
  28. package/dist/enforcement-gate.js.map +1 -1
  29. package/dist/engine.d.ts +1 -1
  30. package/dist/engine.d.ts.map +1 -1
  31. package/dist/engine.js +53 -38
  32. package/dist/engine.js.map +1 -1
  33. package/dist/evidence-claims.d.ts +4 -4
  34. package/dist/evidence-claims.d.ts.map +1 -1
  35. package/dist/evidence-claims.js +16 -14
  36. package/dist/evidence-claims.js.map +1 -1
  37. package/dist/evidence-provider.d.ts +85 -89
  38. package/dist/evidence-provider.d.ts.map +1 -1
  39. package/dist/evidence-provider.js +328 -184
  40. package/dist/evidence-provider.js.map +1 -1
  41. package/dist/explain.d.ts +76 -0
  42. package/dist/explain.d.ts.map +1 -0
  43. package/dist/explain.js +127 -0
  44. package/dist/explain.js.map +1 -0
  45. package/dist/generate-schema.js +152 -16
  46. package/dist/generate-schema.js.map +1 -1
  47. package/dist/index.d.ts +6 -6
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +5 -5
  50. package/dist/index.js.map +1 -1
  51. package/dist/loop-compiler.d.ts +40 -53
  52. package/dist/loop-compiler.d.ts.map +1 -1
  53. package/dist/loop-compiler.js +229 -553
  54. package/dist/loop-compiler.js.map +1 -1
  55. package/dist/loop-extras-parser.d.ts +0 -1
  56. package/dist/loop-extras-parser.d.ts.map +1 -1
  57. package/dist/loop-extras-parser.js +0 -4
  58. package/dist/loop-extras-parser.js.map +1 -1
  59. package/dist/loop-projection.d.ts.map +1 -1
  60. package/dist/loop-projection.js +3 -1
  61. package/dist/loop-projection.js.map +1 -1
  62. package/dist/mcp/round-lifecycle.d.ts +30 -21
  63. package/dist/mcp/round-lifecycle.d.ts.map +1 -1
  64. package/dist/mcp/round-lifecycle.js +47 -103
  65. package/dist/mcp/round-lifecycle.js.map +1 -1
  66. package/dist/mcp/server.d.ts.map +1 -1
  67. package/dist/mcp/server.js +51 -7
  68. package/dist/mcp/server.js.map +1 -1
  69. package/dist/mcp/session.d.ts +39 -23
  70. package/dist/mcp/session.d.ts.map +1 -1
  71. package/dist/mcp/session.js +148 -163
  72. package/dist/mcp/session.js.map +1 -1
  73. package/dist/mcp/submission-boundary.d.ts +89 -0
  74. package/dist/mcp/submission-boundary.d.ts.map +1 -0
  75. package/dist/mcp/submission-boundary.js +203 -0
  76. package/dist/mcp/submission-boundary.js.map +1 -0
  77. package/dist/mcp/tools.d.ts.map +1 -1
  78. package/dist/mcp/tools.js +249 -128
  79. package/dist/mcp/tools.js.map +1 -1
  80. package/dist/policy-metrics.js +2 -2
  81. package/dist/policy-metrics.js.map +1 -1
  82. package/dist/policy.d.ts +55 -93
  83. package/dist/policy.d.ts.map +1 -1
  84. package/dist/policy.js +149 -38
  85. package/dist/policy.js.map +1 -1
  86. package/dist/prompt-assembler.d.ts +1 -32
  87. package/dist/prompt-assembler.d.ts.map +1 -1
  88. package/dist/prompt-assembler.js +282 -435
  89. package/dist/prompt-assembler.js.map +1 -1
  90. package/dist/prompt-policy.d.ts +1 -4
  91. package/dist/prompt-policy.d.ts.map +1 -1
  92. package/dist/prompt-policy.js +6 -7
  93. package/dist/prompt-policy.js.map +1 -1
  94. package/dist/protocol.d.ts +365 -142
  95. package/dist/protocol.d.ts.map +1 -1
  96. package/dist/protocol.js +21 -66
  97. package/dist/protocol.js.map +1 -1
  98. package/dist/replay.js +2 -2
  99. package/dist/replay.js.map +1 -1
  100. package/dist/round-contract.d.ts +69 -57
  101. package/dist/round-contract.d.ts.map +1 -1
  102. package/dist/round-contract.js +104 -64
  103. package/dist/round-contract.js.map +1 -1
  104. package/dist/round-coordinator.d.ts +36 -16
  105. package/dist/round-coordinator.d.ts.map +1 -1
  106. package/dist/round-coordinator.js +166 -102
  107. package/dist/round-coordinator.js.map +1 -1
  108. package/dist/round-driver.d.ts +3 -5
  109. package/dist/round-driver.d.ts.map +1 -1
  110. package/dist/round-driver.js +0 -1
  111. package/dist/round-driver.js.map +1 -1
  112. package/dist/round-facts.d.ts +96 -0
  113. package/dist/round-facts.d.ts.map +1 -0
  114. package/dist/round-facts.js +216 -0
  115. package/dist/round-facts.js.map +1 -0
  116. package/dist/round-transaction.d.ts +19 -9
  117. package/dist/round-transaction.d.ts.map +1 -1
  118. package/dist/round-transaction.js +90 -22
  119. package/dist/round-transaction.js.map +1 -1
  120. package/dist/self-eval.d.ts +87 -9
  121. package/dist/self-eval.d.ts.map +1 -1
  122. package/dist/self-eval.js +396 -27
  123. package/dist/self-eval.js.map +1 -1
  124. package/dist/subgoal-state.d.ts +77 -0
  125. package/dist/subgoal-state.d.ts.map +1 -0
  126. package/dist/subgoal-state.js +198 -0
  127. package/dist/subgoal-state.js.map +1 -0
  128. package/dist/token-utils.d.ts +52 -19
  129. package/dist/token-utils.d.ts.map +1 -1
  130. package/dist/token-utils.js +83 -72
  131. package/dist/token-utils.js.map +1 -1
  132. package/dist/verification-gate.d.ts +14 -31
  133. package/dist/verification-gate.d.ts.map +1 -1
  134. package/dist/verification-gate.js +178 -496
  135. package/dist/verification-gate.js.map +1 -1
  136. package/dist/version.d.ts +1 -1
  137. package/dist/version.js +1 -1
  138. package/dist/workspace.d.ts +9 -0
  139. package/dist/workspace.d.ts.map +1 -1
  140. package/dist/workspace.js +17 -0
  141. package/dist/workspace.js.map +1 -1
  142. package/loop_policy.json +6 -30
  143. package/package.json +1 -1
  144. package/skills/perception/SKILL.md +243 -79
@@ -13,7 +13,8 @@
13
13
  * SessionManager → RoundDriver → RoundCoordinator path.
14
14
  */
15
15
  import type { GateActionDescriptor } from "../protocol.js";
16
- import type { ExternalContextProvider, LoopTerminalSink, SelfEvaluation } from "../protocol.js";
16
+ import type { ExternalContextProvider, LoopTerminalSink } from "../protocol.js";
17
+ import type { ActiveContractView } from "../round-contract.js";
17
18
  import type { LoopStore } from "../loop-store.js";
18
19
  import type { PolicyMetricsSnapshot } from "../policy-metrics.js";
19
20
  import type { SessionStateStore } from "../storage.js";
@@ -94,41 +95,40 @@ export declare class SessionManager implements SessionRegistry {
94
95
  * canonicalized action hash — if the action changed, the match fails and
95
96
  * the old approval expires automatically. */
96
97
  resolveGate(sessionId: string, gateId: string, approved: boolean, note?: string): Record<string, unknown>;
97
- /** Compile the current round context exactly once per derivation path.
98
- * v3.0.1: prefer the round-boundary compile cached on the session (the
99
- * artifact's deterministic roundId guards against stale reuse); fall
100
- * back to a read-only compile (persistLineage: false) that never writes
101
- * the vault. Single derivation — shared by the projection view and the
102
- * subgoal_updates preflight so they can never disagree. */
98
+ /** The compile context for this round.
99
+ *
100
+ * v3.8.1: delegates to the SHARED implementation in round-lifecycle.ts —
101
+ * the runtime's submission boundary reads the same context (the sub-goal
102
+ * reference space is exactly the compiled set the prompt carried), and two
103
+ * copies would let the projection and the boundary disagree about it. */
103
104
  private compileContext;
104
- /** v3.7.1: pre-advance referential check for subgoal_updates. The
105
- * reference space is the SAME derivation the agent saw in its prompt
106
- * (the compiled sub_goals of the current round) plus this payload's own
107
- * emerged items (a sub-goal may be created and transitioned in one
108
- * round). Unknown IDs, terminal references, and illegal migrations
109
- * return evaluation_invalid before anything mutates. Compile failure
110
- * fails open (the shape checks above stay strict). */
111
- preflightSubGoalUpdates(sessionId: string, roundId: string, updates: import("../protocol.js").SubGoalUpdate[], emerged: string[]): Array<{
112
- field: string;
113
- reason: string;
114
- detail: string;
115
- }>;
116
105
  /** Typed cognitive state projection for an active session. Derived on
117
106
  * demand — zero persistence. Null when nothing meaningful exists yet. */
118
107
  getProjection(sessionId: string): Record<string, unknown> | null;
108
+ /** v3.8: Read-only per-round "why" view over committed facts. Never
109
+ * rebuilds history and never writes. */
110
+ getExplain(loopId: string, round?: number): Record<string, unknown>;
119
111
  /** Read-only end-of-loop audit (verification view). Never writes. */
120
112
  getAudit(loopId: string): Record<string, unknown> | null;
121
113
  /** v3.5: The ACTIVE Round Contract governing the session's next round —
122
114
  * derived from the committed :feedback evals (the SAME adapter + walker
123
115
  * the verification gate uses; display-only, zero persistence). Null when
124
116
  * nothing is active (whole-task round). */
125
- getActiveContract(sessionId: string): import("../protocol.js").RoundContract | null;
117
+ getActiveContract(sessionId: string): ActiveContractView | null;
126
118
  /** v2.12: Policy metrics that survive restarts — vault-derived round
127
119
  * statistics (A4 port) folded with this process's live observations.
128
120
  * Non-durable fields (evidence, vault errors) come from live only. */
129
121
  getPolicyMetrics(loopId: string): PolicyMetricsSnapshot;
130
- /** Get loop health for a loop (in-memory or vault).
131
- * Computes goal alignment, constraint integrity, drift, strategy stability. */
122
+ /** Get machine facts about a loop (in-memory or vault).
123
+ *
124
+ * v3.8.1: this view used to report `goal_alignment`, `drift_detected`,
125
+ * `strategy_stability` and `task_continuity`. Every one was a
126
+ * text-similarity verdict rather than a fact, and two were degenerate in
127
+ * THIS method specifically: `task_continuity` was pinned to 1.0 because the
128
+ * request was built with `round: 1`, so `getPreviousRound(loopId, 0)`
129
+ * returned null and the code took its hardcoded `?? 1` branch; and
130
+ * `strategy_stability` was a literal `true`. What remains is counted
131
+ * directly from committed round flags. */
132
132
  getHealth(loopId: string): Record<string, unknown> | null;
133
133
  /** Core cycle: extract self-eval → record feedback → check stop → compile next.
134
134
  * The lease + per-session queue wrap the RoundLifecycle state machine.
@@ -138,7 +138,23 @@ export declare class SessionManager implements SessionRegistry {
138
138
  * (from the last start/next/resume response). Anchors the submission so a
139
139
  * stale or duplicate submission is not processed against a later round.
140
140
  * Optional for library callers — when absent the anchor check is skipped. */
141
- advance(sessionId: string, output: string, preExtractedEval?: SelfEvaluation, roundId?: string): Promise<AdvanceResult>;
141
+ /** \`submission\` is the RAW evaluation payload, not a built one.
142
+ *
143
+ * v3.8.1: this method used to carry a partial copy of the submission
144
+ * boundary (an object check plus core-field validation) while the MCP tool
145
+ * handler carried the rest — one rule, three copies, across two layers.
146
+ * The boundary now lives entirely in the runtime
147
+ * (`RoundLifecycle.submissionBoundary`), so every caller — this manager, a
148
+ * library user, a future CLI command — gets the same strictness. */
149
+ /** \`submission\` is the RAW evaluation payload, not a built one.
150
+ *
151
+ * v3.8.1: this method used to carry a partial copy of the submission
152
+ * boundary (an object check plus core-field validation) while the MCP tool
153
+ * handler carried the rest — one rule, three copies, across two layers.
154
+ * The whole boundary now runs HERE, before the queue and before the lease
155
+ * heartbeat, so every caller of the public API gets it and a rejected
156
+ * payload leaves nothing durable behind. */
157
+ advance(sessionId: string, output: string, submission?: unknown, roundId?: string): Promise<AdvanceResult>;
142
158
  /** Replay timeline for a session — creates ReplayBackend from the stored backend. */
143
159
  replayTimeline(sessionId: string): Record<string, unknown>[] | null;
144
160
  /** v3.3.1: Replay a loop straight from the vault — no in-memory session
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/mcp/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAWH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EACV,uBAAuB,EAGvB,gBAAgB,EAChB,cAAc,EACf,MAAM,gBAAgB,CAAC;AAoBxB,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,kBAAkB,CAAC;AAS9D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAKlE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGvD,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,eAAe,EAChB,MAAM,sBAAsB,CAAC;AAG9B,YAAY,EACV,UAAU,EACV,iBAAiB,EACjB,UAAU,EACV,aAAa,GACd,MAAM,sBAAsB,CAAC;AAI9B,qBAAa,cAAe,YAAW,eAAe;IACpD,OAAO,CAAC,QAAQ,CAAiC;IACjD,qDAAqD;IACrD,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiB;IAC3C,wDAAwD;IACxD,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA+B;gBAG3D,KAAK,CAAC,EAAE,SAAS,EACjB,YAAY,CAAC,EAAE,iBAAiB;IA4BlC,8EAA8E;IAC9E,UAAU,IAAI,MAAM;IAIpB,eAAe,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,IAAI;IAKnD,yDAAyD;IACzD,KAAK,IAAI,IAAI;IAUb,MAAM,IAAI,QAAQ,CAAC,UAAU,CAAC;IAI9B,iEAAiE;IACjE,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAMjC,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,mBAAmB;YAcb,gBAAgB;IAyBxB,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC;IAwIvD,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAI9C,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAoB9D,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAiBlC;;2EAEuE;IACvE,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IAgB9E;8EAC0E;IAC1E,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAI/B;;;yEAGqE;IAC/D,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IA8B3D;;2BAEuB;IACjB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAoB5D;;;kDAG8C;IAC9C,aAAa,IAAI,MAAM;IA4CvB,IAAI,IAAI,iBAAiB,EAAE;IAsC3B;;6CAEyC;IACzC,OAAO,CAAC,gBAAgB;IAwBxB;;;;;;mCAM+B;IAC/B,SAAS,CACP,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,oBAAoB,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAoC1B;;kDAE8C;IAC9C,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,EACjB,IAAI,CAAC,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAgH1B;;;;;gEAK4D;IAC5D,OAAO,CAAC,cAAc;IAkCtB;;;;;;2DAMuD;IACvD,uBAAuB,CACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,gBAAgB,EAAE,aAAa,EAAE,EACjD,OAAO,EAAE,MAAM,EAAE,GAChB,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IA8B3D;8EAC0E;IAC1E,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAmBhE,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAqBxD;;;gDAG4C;IAC5C,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,gBAAgB,EAAE,aAAa,GAAG,IAAI;IAanF;;2EAEuE;IACvE,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,qBAAqB;IAiBvD;oFACgF;IAChF,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAiDzD;;;;;;;oFAOgF;IAC1E,OAAO,CACX,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,gBAAgB,CAAC,EAAE,cAAc,EACjC,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,aAAa,CAAC;IAoDzB,qFAAqF;IACrF,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI;IAOnE;;;;;8DAK0D;IAC1D,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI;CAM/D"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/mcp/session.ts"],"names":[],"mappings":"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"}
@@ -15,18 +15,17 @@
15
15
  import { randomUUID } from "node:crypto";
16
16
  import { LoopForgeEngine } from "../engine.js";
17
17
  import { canonicalizeGateAction, deriveGate, preflightStructuredGate, } from "../cognitive-governance.js";
18
- import { makeGateDecision, Mode } from "../protocol.js";
18
+ import { makeGateDecision } from "../protocol.js";
19
19
  import { buildLoopProjection } from "../loop-projection.js";
20
20
  import { deriveCognitiveFacts } from "../cognitive-facts.js";
21
+ import { NO_IN_FLIGHT_ROUND, deriveRoundFacts } from "../round-facts.js";
21
22
  import { listVerifiedClaims } from "../evidence-claims.js";
22
23
  import { buildAudit } from "../audit.js";
23
- import { checkLoopHealth, deriveSubGoalId, validateSubGoalUpdates } from "../loop-compiler.js";
24
- import { committedRoundsFromEntries } from "../committed-round.js";
25
- import { contractRoundEvaluations, deriveActiveRoundContract, } from "../round-contract.js";
24
+ import { buildExplain } from "../explain.js";
25
+ import { CHECK_CONTRACT_ITEMS_UNVERIFIED } from "../verification-gate.js";
26
+ import { readOnlyRounds } from "../committed-round.js";
26
27
  import { getPolicy, validateLoopId } from "../policy.js";
27
28
  import { isRecord } from "../token-utils.js";
28
- import { buildSelfEvaluation, validateCoreSelfEvaluation, } from "../self-eval.js";
29
- import { makeLoopCompileRequest } from "../protocol.js";
30
29
  import { ReplayBackend } from "../replay.js";
31
30
  import { FileLoopStore, queryLoopEntries } from "../loop-store.js";
32
31
  import { makeRoundId, prepareRoundTransaction } from "../round-transaction.js";
@@ -36,6 +35,7 @@ import { policyMetrics, derivePolicyMetrics, mergePolicyMetrics, } from "../poli
36
35
  import { SessionLeaseConflictError, VaultSessionStateStore, } from "../storage.js";
37
36
  import { StorageCorruptionError } from "../loop-store.js";
38
37
  import { RoundLifecycle, buildLoopRequest } from "./round-lifecycle.js";
38
+ import { activeContractOf, compileSessionContext, validateSubmission, } from "./submission-boundary.js";
39
39
  // ── SessionManager ─────────────────────────────────────────────────────────
40
40
  export class SessionManager {
41
41
  sessions = new Map();
@@ -193,7 +193,6 @@ export class SessionManager {
193
193
  maxRounds, successTrajectory: [], status: "running", createdAt: Date.now(),
194
194
  consecutiveRejections: 0,
195
195
  lastRejectionCheck: "",
196
- driftClarificationStreak: 0,
197
196
  backtrackSkippedFiles: [],
198
197
  backtrackSkippedFingerprints: {},
199
198
  evidenceBaseline: [],
@@ -235,7 +234,6 @@ export class SessionManager {
235
234
  // v1.13: Enforcement gate state
236
235
  consecutiveRejections: 0,
237
236
  lastRejectionCheck: "",
238
- driftClarificationStreak: 0,
239
237
  backtrackSkippedFiles: [],
240
238
  backtrackSkippedFingerprints: {},
241
239
  evidenceBaseline,
@@ -509,12 +507,22 @@ export class SessionManager {
509
507
  checkGate(sessionId, roundId, action) {
510
508
  const session = this.sessions.get(sessionId);
511
509
  if (!session) {
512
- return { error: `session not found: ${sessionId}` };
510
+ return {
511
+ error: "session_not_found",
512
+ errorMessage: `session not found: ${sessionId}`,
513
+ sessionId,
514
+ };
513
515
  }
514
516
  const expectedRoundId = session.roundSnapshot?.roundId
515
517
  ?? makeRoundId(session.loopId, session.currentRound);
516
518
  if (expectedRoundId !== roundId) {
517
- return { error: "roundId does not match the current round — re-run loopforge_gate_check with the roundId from the latest response" };
519
+ return {
520
+ error: "round_id_mismatch",
521
+ errorMessage: "roundId does not match the current round — re-run loopforge_gate_check " +
522
+ "with the roundId from the latest response",
523
+ sessionId,
524
+ roundId,
525
+ };
518
526
  }
519
527
  const verdict = preflightStructuredGate(action);
520
528
  if (verdict.kind === "user") {
@@ -561,8 +569,13 @@ export class SessionManager {
561
569
  return lineage.session_id === sessionId;
562
570
  });
563
571
  loopId = persistedEntry?.loop_id;
564
- if (!loopId)
565
- return { error: `session not found: ${sessionId}` };
572
+ if (!loopId) {
573
+ return {
574
+ error: "session_not_found",
575
+ errorMessage: `session not found: ${sessionId}`,
576
+ sessionId,
577
+ };
578
+ }
566
579
  const claimed = this.claimSessionEntry(loopId);
567
580
  if (!claimed) {
568
581
  return { ...this.leaseConflictResult(loopId, persistedEntry) };
@@ -573,14 +586,23 @@ export class SessionManager {
573
586
  ? lineage.current_round
574
587
  : 0;
575
588
  }
576
- if (!loopId)
577
- return { error: `session not found: ${sessionId}` };
589
+ if (!loopId) {
590
+ return {
591
+ error: "session_not_found",
592
+ errorMessage: `session not found: ${sessionId}`,
593
+ sessionId,
594
+ };
595
+ }
578
596
  try {
579
597
  const prefix = `loop:${loopId}:gate:`;
580
598
  const opened = queryLoopEntries(this.loopStore, loopId, { prefix })
581
599
  .find((entry) => entry.task_type === "gate_opened" && entry.gate_id === gateId);
582
600
  if (!opened) {
583
- return { error: `no gate_opened record for gate ${gateId} — the gate may have expired or was never recorded` };
601
+ return {
602
+ error: "state_unavailable",
603
+ errorMessage: `no gate_opened record for gate ${gateId} — the gate may have ` +
604
+ "expired or was never recorded",
605
+ };
584
606
  }
585
607
  const actionText = typeof opened.gate_action === "string" ? opened.gate_action : "";
586
608
  const openedLineage = isRecord(opened.loop_lineage) ? opened.loop_lineage : {};
@@ -604,7 +626,11 @@ export class SessionManager {
604
626
  descriptor = JSON.parse(actionText);
605
627
  }
606
628
  catch {
607
- return { error: `gate ${gateId} has a corrupted structured action — re-run loopforge_gate_check` };
629
+ return {
630
+ error: "state_unavailable",
631
+ errorMessage: `gate ${gateId} has a corrupted structured action — ` +
632
+ "re-run loopforge_gate_check",
633
+ };
608
634
  }
609
635
  const verdict = preflightStructuredGate(descriptor);
610
636
  matchedId = verdict.gateId;
@@ -619,10 +645,18 @@ export class SessionManager {
619
645
  userScope = derived.gate.blockedScope ?? [];
620
646
  }
621
647
  if (matchedId !== gateId) {
622
- return { error: "gateId does not match the recorded action — the action changed, old approvals expire" };
648
+ return {
649
+ error: "invalid_argument",
650
+ errorMessage: "gateId does not match the recorded action — the action changed, " +
651
+ "old approvals expire",
652
+ };
623
653
  }
624
654
  if (!userKind) {
625
- return { error: "agent gates are resolved by submitting the required evidence, not by user approval" };
655
+ return {
656
+ error: "invalid_argument",
657
+ errorMessage: "agent gates are resolved by submitting the required evidence, " +
658
+ "not by user approval",
659
+ };
626
660
  }
627
661
  const decision = makeGateDecision({
628
662
  gateId,
@@ -658,77 +692,14 @@ export class SessionManager {
658
692
  }
659
693
  }
660
694
  // ── v2.12: Typed projection + audit ────────────────────────────────────
661
- /** Compile the current round context exactly once per derivation path.
662
- * v3.0.1: prefer the round-boundary compile cached on the session (the
663
- * artifact's deterministic roundId guards against stale reuse); fall
664
- * back to a read-only compile (persistLineage: false) that never writes
665
- * the vault. Single derivation — shared by the projection view and the
666
- * subgoal_updates preflight so they can never disagree. */
695
+ /** The compile context for this round.
696
+ *
697
+ * v3.8.1: delegates to the SHARED implementation in round-lifecycle.ts —
698
+ * the runtime's submission boundary reads the same context (the sub-goal
699
+ * reference space is exactly the compiled set the prompt carried), and two
700
+ * copies would let the projection and the boundary disagree about it. */
667
701
  compileContext(session) {
668
- const cached = session.lastCompileResponse;
669
- const expectedRoundId = session.roundSnapshot?.roundId
670
- ?? makeRoundId(session.loopId, session.currentRound);
671
- if (cached?.prompt_artifact && cached.prompt_artifact.roundId === expectedRoundId) {
672
- return cached;
673
- }
674
- try {
675
- const request = {
676
- task: session.task,
677
- mode: Mode.LOOP_COMPILE,
678
- feedback: null,
679
- skill_name: null,
680
- task_id: null,
681
- loop_id: session.loopId,
682
- round: session.currentRound,
683
- max_rounds: session.maxRounds,
684
- verification_flags: [],
685
- };
686
- const compiled = session.engine.invokeLoopCompile(request, undefined, { persistLineage: false });
687
- return compiled.response ?? null;
688
- }
689
- catch {
690
- return null; // projection/preflight degrades gracefully
691
- }
692
- }
693
- /** v3.7.1: pre-advance referential check for subgoal_updates. The
694
- * reference space is the SAME derivation the agent saw in its prompt
695
- * (the compiled sub_goals of the current round) plus this payload's own
696
- * emerged items (a sub-goal may be created and transitioned in one
697
- * round). Unknown IDs, terminal references, and illegal migrations
698
- * return evaluation_invalid before anything mutates. Compile failure
699
- * fails open (the shape checks above stay strict). */
700
- preflightSubGoalUpdates(sessionId, roundId, updates, emerged) {
701
- if (updates.length === 0)
702
- return [];
703
- const session = this.sessions.get(sessionId);
704
- if (!session)
705
- return [];
706
- const expectedRoundId = session.roundSnapshot?.roundId
707
- ?? makeRoundId(session.loopId, session.currentRound);
708
- if (expectedRoundId !== roundId)
709
- return []; // stale/foreign submission — advance handles it
710
- const response = this.compileContext(session);
711
- if (!response?.sub_goals)
712
- return []; // fail open: cannot observe
713
- const known = [...response.sub_goals];
714
- for (const desc of emerged) {
715
- const id = deriveSubGoalId(desc);
716
- if (!known.some((sg) => sg.id === id)) {
717
- known.push({
718
- id,
719
- description: desc.trim(),
720
- status: "pending",
721
- declared_at_round: session.currentRound,
722
- status_changed_at_round: session.currentRound,
723
- priority: known.length,
724
- });
725
- }
726
- }
727
- return validateSubGoalUpdates(known, updates).map((error) => ({
728
- field: "subgoal_updates",
729
- reason: error.reason,
730
- detail: `${error.reason.replace(/_/g, " ")}: ${error.id}`,
731
- }));
702
+ return compileSessionContext(session);
732
703
  }
733
704
  /** Typed cognitive state projection for an active session. Derived on
734
705
  * demand — zero persistence. Null when nothing meaningful exists yet. */
@@ -743,14 +714,43 @@ export class SessionManager {
743
714
  ];
744
715
  const compileResponse = this.compileContext(session);
745
716
  const openGates = this.lifecycle.listOpenGateDescriptions(session.loopId);
717
+ // v3.8: the projection reads the WHOLE committed history. The old
718
+ // `beforeRound: session.currentRound` bound is only correct while the loop
719
+ // is running, where currentRound is always one past the last committed
720
+ // round; a stop / terminate / max_rounds leaves currentRound ON the round
721
+ // it just committed, so the bound silently dropped the loop's final round
722
+ // and the projection disagreed with audit and explain about it.
723
+ // v3.8.1: one window, one decode, one derivation. The contract facts come
724
+ // from the same `deriveRoundFacts` the compile path calls, so the prompt
725
+ // and the projection cannot tell different stories about what the machine
726
+ // verified.
727
+ const rounds = readOnlyRounds(entries);
746
728
  const projection = buildLoopProjection(deriveCognitiveFacts({
747
729
  compileResponse,
748
- rounds: committedRoundsFromEntries(entries, session.currentRound),
730
+ rounds,
731
+ facts: deriveRoundFacts({
732
+ rounds,
733
+ currentRound: (rounds[rounds.length - 1]?.round ?? 0) + 1,
734
+ inFlight: NO_IN_FLIGHT_ROUND,
735
+ subGoals: compileResponse?.sub_goals ?? [],
736
+ commands: getPolicy().evidence.commands ?? [],
737
+ }),
749
738
  verifiedClaims: listVerifiedClaims(entries, session.loopId),
750
739
  openGates,
751
740
  }));
752
741
  return projection ? { ...projection } : null;
753
742
  }
743
+ /** v3.8: Read-only per-round "why" view over committed facts. Never
744
+ * rebuilds history and never writes. */
745
+ getExplain(loopId, round) {
746
+ validateLoopId(loopId);
747
+ const prefix = `loop:${loopId}:`;
748
+ const entries = [
749
+ ...queryLoopEntries(this.loopStore, loopId, { prefix }),
750
+ ...queryLoopEntries(this.loopStore, loopId, { prefix, feedbackOnly: true }),
751
+ ];
752
+ return buildExplain(loopId, entries, round);
753
+ }
754
754
  /** Read-only end-of-loop audit (verification view). Never writes. */
755
755
  getAudit(loopId) {
756
756
  validateLoopId(loopId);
@@ -767,7 +767,7 @@ export class SessionManager {
767
767
  // Zero committed decisions → nothing to audit. Returning null lets the
768
768
  // tools layer report "no audit data" instead of the external auditor
769
769
  // solemnly passing a loop that never ran (or a mistyped loopId).
770
- const hasCommittedDecision = committedRoundsFromEntries(entries).length > 0;
770
+ const hasCommittedDecision = readOnlyRounds(entries).length > 0;
771
771
  if (!hasCommittedDecision)
772
772
  return null;
773
773
  const audit = buildAudit(loopId, entries, this.loopStore);
@@ -781,12 +781,8 @@ export class SessionManager {
781
781
  const session = this.sessions.get(sessionId);
782
782
  if (!session)
783
783
  return null;
784
- const prefix = `loop:${session.loopId}:`;
785
- const entries = [
786
- ...queryLoopEntries(this.loopStore, session.loopId, { prefix }),
787
- ...queryLoopEntries(this.loopStore, session.loopId, { prefix, feedbackOnly: true }),
788
- ];
789
- return deriveActiveRoundContract(contractRoundEvaluations(committedRoundsFromEntries(entries, session.currentRound)));
784
+ // One derivation, shared with the submission boundary.
785
+ return activeContractOf(this.loopStore, session);
790
786
  }
791
787
  /** v2.12: Policy metrics that survive restarts — vault-derived round
792
788
  * statistics (A4 port) folded with this process's live observations.
@@ -808,48 +804,35 @@ export class SessionManager {
808
804
  return live;
809
805
  return mergePolicyMetrics(derived, live);
810
806
  }
811
- /** Get loop health for a loop (in-memory or vault).
812
- * Computes goal alignment, constraint integrity, drift, strategy stability. */
807
+ /** Get machine facts about a loop (in-memory or vault).
808
+ *
809
+ * v3.8.1: this view used to report `goal_alignment`, `drift_detected`,
810
+ * `strategy_stability` and `task_continuity`. Every one was a
811
+ * text-similarity verdict rather than a fact, and two were degenerate in
812
+ * THIS method specifically: `task_continuity` was pinned to 1.0 because the
813
+ * request was built with `round: 1`, so `getPreviousRound(loopId, 0)`
814
+ * returned null and the code took its hardcoded `?? 1` branch; and
815
+ * `strategy_stability` was a literal `true`. What remains is counted
816
+ * directly from committed round flags. */
813
817
  getHealth(loopId) {
814
- // Find the task — check in-memory sessions first, then vault
815
- let task = "";
816
- let goalId = loopId;
817
- for (const s of this.sessions.values()) {
818
- if (s.loopId === loopId) {
819
- task = s.task;
820
- if (s.engine.state?.task_id) {
821
- goalId = s.engine.state.task_id;
822
- }
823
- break;
824
- }
825
- }
826
- // Fall back to vault for task
827
- if (!task) {
828
- const sessionEntry = this.findSessionEntry(loopId);
829
- if (sessionEntry) {
830
- task = sessionEntry.task ?? "";
831
- }
832
- }
833
- if (!task)
834
- return null;
835
- // Hydrate vault context
836
818
  const engine = new LoopForgeEngine(this.loopStore);
837
- const vaultContext = engine.hydrateLoopContext(loopId);
838
- // Build a minimal request for health check
839
- const request = makeLoopCompileRequest({
840
- task,
841
- loop_id: loopId,
842
- goal_id: goalId,
843
- round: 1, // round doesn't matter for health check
844
- });
845
- const health = checkLoopHealth(loopId, request, vaultContext);
819
+ const context = engine.hydrateLoopContext(loopId);
820
+ // No hydratable context at all → unknown loop. A started-but-uncommitted
821
+ // loop DOES have a context (the compile persists a lineage entry), so it
822
+ // still gets a view — with zeros — exactly as it did before.
823
+ if (!context)
824
+ return null;
825
+ const entries = Array.isArray(context.results) ? context.results : [];
826
+ const views = readOnlyRounds(entries);
827
+ const roundsWithUnverifiedItems = views.filter((view) => view.verificationFlags.some((flag) => flag.check === CHECK_CONTRACT_ITEMS_UNVERIFIED)).length;
846
828
  return {
847
829
  loopId,
848
- goal_alignment: health.goal_alignment,
849
- constraint_integrity: health.constraint_integrity,
850
- drift_detected: health.drift_detected,
851
- strategy_stability: health.strategy_stability,
852
- task_continuity: health.task_continuity,
830
+ committed_rounds: views.length,
831
+ rounds_with_unverified_items: roundsWithUnverifiedItems,
832
+ // The policy value the enforcement gate escalates on. The streak itself
833
+ // is derived in the gate — re-deriving it here would be a second
834
+ // implementation of the same rule, which is what this release removes.
835
+ unverified_streak_limit: getPolicy().engine.unverified_claim_streak_limit,
853
836
  policy_metrics: this.getPolicyMetrics(loopId),
854
837
  };
855
838
  }
@@ -861,34 +844,36 @@ export class SessionManager {
861
844
  * (from the last start/next/resume response). Anchors the submission so a
862
845
  * stale or duplicate submission is not processed against a later round.
863
846
  * Optional for library callers — when absent the anchor check is skipped. */
864
- async advance(sessionId, output, preExtractedEval, roundId) {
865
- const current = this.sessions.get(sessionId);
866
- if (current && (current.status === "running" || current.status === "stalled")) {
867
- const rawEvaluation = preExtractedEval;
868
- if (!isRecord(rawEvaluation)) {
869
- return {
870
- sessionId,
871
- round: current.currentRound,
872
- roundId: current.roundSnapshot?.roundId
873
- ?? makeRoundId(current.loopId, current.currentRound),
874
- prompt: null,
875
- stopReason: "evaluation_invalid",
876
- stopDetail: "A structured evaluation object is required. Resubmit the same roundId with success, output_summary, constraint_violations, and should_continue.",
877
- };
878
- }
879
- const validation = validateCoreSelfEvaluation(rawEvaluation);
880
- if (validation.missing.length > 0 || validation.invalid.length > 0) {
881
- return {
882
- sessionId,
883
- round: current.currentRound,
884
- roundId: current.roundSnapshot?.roundId
885
- ?? makeRoundId(current.loopId, current.currentRound),
886
- prompt: null,
887
- stopReason: "evaluation_invalid",
888
- stopDetail: "The structured evaluation has missing or invalid core fields. Correct it and resubmit the same roundId.",
889
- };
890
- }
891
- preExtractedEval = buildSelfEvaluation(rawEvaluation);
847
+ /** \`submission\` is the RAW evaluation payload, not a built one.
848
+ *
849
+ * v3.8.1: this method used to carry a partial copy of the submission
850
+ * boundary (an object check plus core-field validation) while the MCP tool
851
+ * handler carried the rest — one rule, three copies, across two layers.
852
+ * The boundary now lives entirely in the runtime
853
+ * (`RoundLifecycle.submissionBoundary`), so every caller — this manager, a
854
+ * library user, a future CLI command — gets the same strictness. */
855
+ /** \`submission\` is the RAW evaluation payload, not a built one.
856
+ *
857
+ * v3.8.1: this method used to carry a partial copy of the submission
858
+ * boundary (an object check plus core-field validation) while the MCP tool
859
+ * handler carried the rest — one rule, three copies, across two layers.
860
+ * The whole boundary now runs HERE, before the queue and before the lease
861
+ * heartbeat, so every caller of the public API gets it and a rejected
862
+ * payload leaves nothing durable behind. */
863
+ async advance(sessionId, output, submission, roundId) {
864
+ // Judged before the session is even resolved: a malformed evaluation is a
865
+ // payload defect whatever the session's state. See submission-boundary.ts.
866
+ const boundary = validateSubmission(this.loopStore, this.sessions.get(sessionId), submission);
867
+ if (!boundary.ok) {
868
+ return {
869
+ sessionId,
870
+ round: this.sessions.get(sessionId)?.currentRound ?? 0,
871
+ ...(roundId !== undefined ? { roundId } : {}),
872
+ prompt: null,
873
+ stopReason: boundary.submissionError.code,
874
+ stopDetail: boundary.stopDetail,
875
+ submissionError: boundary.submissionError,
876
+ };
892
877
  }
893
878
  return this.withSessionQueue(sessionId, async () => {
894
879
  const session = this.sessions.get(sessionId);
@@ -897,7 +882,7 @@ export class SessionManager {
897
882
  return this.leaseConflictResult(session.loopId, this.findSessionEntry(session.loopId));
898
883
  }
899
884
  try {
900
- return await this.lifecycle.advance(sessionId, output, preExtractedEval, roundId);
885
+ return await this.lifecycle.advance(sessionId, output, boundary.evaluation, roundId);
901
886
  }
902
887
  catch (error) {
903
888
  if (error instanceof SessionLeaseConflictError) {