loopforge 3.6.0 → 3.7.0

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 (85) hide show
  1. package/README.md +12 -9
  2. package/dist/audit.d.ts.map +1 -1
  3. package/dist/audit.js +3 -8
  4. package/dist/audit.js.map +1 -1
  5. package/dist/canonical-state.d.ts +0 -2
  6. package/dist/canonical-state.d.ts.map +1 -1
  7. package/dist/canonical-state.js +0 -20
  8. package/dist/canonical-state.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +1 -9
  11. package/dist/cli.js.map +1 -1
  12. package/dist/cognitive-governance.d.ts +0 -1
  13. package/dist/cognitive-governance.d.ts.map +1 -1
  14. package/dist/cognitive-governance.js +1 -4
  15. package/dist/cognitive-governance.js.map +1 -1
  16. package/dist/committed-round.d.ts +41 -0
  17. package/dist/committed-round.d.ts.map +1 -1
  18. package/dist/committed-round.js +129 -2
  19. package/dist/committed-round.js.map +1 -1
  20. package/dist/enforcement-gate.d.ts.map +1 -1
  21. package/dist/enforcement-gate.js +120 -130
  22. package/dist/enforcement-gate.js.map +1 -1
  23. package/dist/engine.d.ts.map +1 -1
  24. package/dist/engine.js +2 -1
  25. package/dist/engine.js.map +1 -1
  26. package/dist/evidence-claims.d.ts.map +1 -1
  27. package/dist/evidence-claims.js +3 -9
  28. package/dist/evidence-claims.js.map +1 -1
  29. package/dist/evidence-provider.d.ts +6 -17
  30. package/dist/evidence-provider.d.ts.map +1 -1
  31. package/dist/evidence-provider.js +12 -92
  32. package/dist/evidence-provider.js.map +1 -1
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/loop-compiler.d.ts +0 -6
  38. package/dist/loop-compiler.d.ts.map +1 -1
  39. package/dist/loop-compiler.js +43 -265
  40. package/dist/loop-compiler.js.map +1 -1
  41. package/dist/loop-store.d.ts +11 -16
  42. package/dist/loop-store.d.ts.map +1 -1
  43. package/dist/loop-store.js +21 -63
  44. package/dist/loop-store.js.map +1 -1
  45. package/dist/mcp/round-lifecycle.d.ts +8 -3
  46. package/dist/mcp/round-lifecycle.d.ts.map +1 -1
  47. package/dist/mcp/round-lifecycle.js +36 -18
  48. package/dist/mcp/round-lifecycle.js.map +1 -1
  49. package/dist/mcp/server.js +1 -1
  50. package/dist/mcp/session.d.ts +3 -2
  51. package/dist/mcp/session.d.ts.map +1 -1
  52. package/dist/mcp/session.js +3 -2
  53. package/dist/mcp/session.js.map +1 -1
  54. package/dist/mcp/tools.js +1 -1
  55. package/dist/mcp/tools.js.map +1 -1
  56. package/dist/policy.d.ts +0 -11
  57. package/dist/policy.d.ts.map +1 -1
  58. package/dist/policy.js +0 -4
  59. package/dist/policy.js.map +1 -1
  60. package/dist/prompt-assembler.d.ts +0 -3
  61. package/dist/prompt-assembler.d.ts.map +1 -1
  62. package/dist/prompt-assembler.js +5 -52
  63. package/dist/prompt-assembler.js.map +1 -1
  64. package/dist/protocol.d.ts +5 -24
  65. package/dist/protocol.d.ts.map +1 -1
  66. package/dist/protocol.js +0 -22
  67. package/dist/protocol.js.map +1 -1
  68. package/dist/round-coordinator.d.ts.map +1 -1
  69. package/dist/round-coordinator.js +2 -1
  70. package/dist/round-coordinator.js.map +1 -1
  71. package/dist/round-driver.d.ts +0 -3
  72. package/dist/round-driver.d.ts.map +1 -1
  73. package/dist/round-driver.js +1 -10
  74. package/dist/round-driver.js.map +1 -1
  75. package/dist/token-utils.d.ts +7 -3
  76. package/dist/token-utils.d.ts.map +1 -1
  77. package/dist/token-utils.js +18 -3
  78. package/dist/token-utils.js.map +1 -1
  79. package/dist/verification-gate.d.ts +19 -9
  80. package/dist/verification-gate.d.ts.map +1 -1
  81. package/dist/verification-gate.js +102 -87
  82. package/dist/verification-gate.js.map +1 -1
  83. package/loop_policy.json +0 -4
  84. package/package.json +2 -2
  85. package/skills/perception/SKILL.md +5 -3
@@ -7,7 +7,7 @@ import { createHash } from "node:crypto";
7
7
  import { getPolicy } from "./policy.js";
8
8
  import { AgentStatus, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeConstraintMeta, makeMilestoneSummary, makeRollingSummary, makeSubGoal, makeTaskAlignment, } from "./protocol.js";
9
9
  import { createCanonicalLoopState, renderCanonicalStateMarkdown, } from "./canonical-state.js";
10
- import { decodeCommittedRound, decodeMergedRound, machineGitMotionSeries, mergedRoundsFromEntries, } from "./committed-round.js";
10
+ import { decodeRound, entryLineage, entryCriteriaMet, entryCriteriaRemaining, entryActiveConstraints, entryRetractedConstraints, entryProgressEstimate, entryEmergedSubtasks, machineGitMotionSeries, mergedRoundsFromEntries, } from "./committed-round.js";
11
11
  import { contractRoundEvaluations, deriveActiveRoundContract, } from "./round-contract.js";
12
12
  import { assemblePromptArtifact } from "./prompt-assembler.js";
13
13
  import { decidePromptLevel, } from "./prompt-policy.js";
@@ -17,15 +17,9 @@ function contextEntries(context) {
17
17
  return [];
18
18
  return context.results.filter((value) => value !== null && typeof value === "object" && !Array.isArray(value));
19
19
  }
20
- function lineage(entry) {
21
- const value = entry.loop_lineage ?? entry.lineage;
22
- return value !== null && typeof value === "object" && !Array.isArray(value)
23
- ? value
24
- : {};
25
- }
26
20
  function loopEntries(loopId, context) {
27
21
  return contextEntries(context)
28
- .filter((entry) => entry.loop_id === loopId || lineage(entry).loop_id === loopId)
22
+ .filter((entry) => entry.loop_id === loopId || entryLineage(entry).loop_id === loopId)
29
23
  .sort((a, b) => entryRound(a) - entryRound(b));
30
24
  }
31
25
  /** v3.3.1: The canonical entry for a round — its compile-time lineage entry
@@ -51,12 +45,6 @@ function roundCanonicalEntry(entries, round) {
51
45
  }) ??
52
46
  null);
53
47
  }
54
- /** Return the canonical read model when an entry is a committed round.
55
- * Uncommitted compiler lineage has no durable decision yet and deliberately
56
- * remains on the local compile path; it cannot be treated as history. */
57
- function committedView(entry) {
58
- return decodeMergedRound(entry) ?? decodeCommittedRound(entry);
59
- }
60
48
  export function computeGoalTextHash(text) {
61
49
  const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
62
50
  return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
@@ -75,7 +63,7 @@ export function readPresentedBaseline(loopId, round, context) {
75
63
  const entry = roundCanonicalEntry(loopEntries(loopId, context), round - 1);
76
64
  if (!entry)
77
65
  return null;
78
- const data = lineage(entry);
66
+ const data = entryLineage(entry);
79
67
  const constraintIds = data.presented_constraint_ids;
80
68
  const subGoals = data.presented_subgoals;
81
69
  const milestoneRanges = data.presented_milestone_ranges;
@@ -97,7 +85,7 @@ export function getPreviousRound(loopId, round, context) {
97
85
  const entry = roundCanonicalEntry(loopEntries(loopId, context), round);
98
86
  if (!entry)
99
87
  return null;
100
- const data = lineage(entry);
88
+ const data = entryLineage(entry);
101
89
  return {
102
90
  round,
103
91
  goal_id: typeof data.goal_id === "string" ? data.goal_id : "",
@@ -188,7 +176,7 @@ function evolveConstraints(request, objective, previous, context) {
188
176
  const lastRetractedRound = new Map();
189
177
  if (window > 0) {
190
178
  for (const entry of loopEntries(request.loop_id, context)) {
191
- const retractedList = lineage(entry).retracted_constraints;
179
+ const retractedList = entryLineage(entry).retracted_constraints;
192
180
  if (!Array.isArray(retractedList))
193
181
  continue;
194
182
  const rnd = entryRound(entry);
@@ -221,47 +209,6 @@ function evolveConstraints(request, objective, previous, context) {
221
209
  // ═══════════════════════════════════════════════════════════════════════════
222
210
  // Hierarchical Summary (v2.1)
223
211
  // ═══════════════════════════════════════════════════════════════════════════
224
- /** Extract execution_evidence from a vault entry, handling both direct-field
225
- * and lineage-nested shapes. Returns null when absent. */
226
- function entryExecutionEvidence(entry) {
227
- const view = committedView(entry);
228
- if (view)
229
- return view.executionEvidence;
230
- const direct = entry.execution_evidence;
231
- if (direct && typeof direct === "object" && !Array.isArray(direct)) {
232
- return direct;
233
- }
234
- const lin = lineage(entry);
235
- const nested = lin.execution_evidence;
236
- if (nested && typeof nested === "object" && !Array.isArray(nested)) {
237
- return nested;
238
- }
239
- return null;
240
- }
241
- /** Read success_criteria_met from an entry's execution_evidence. */
242
- function entryCriteriaMet(entry) {
243
- const view = committedView(entry);
244
- if (view)
245
- return view.executionEvidence?.success_criteria_met
246
- ?.filter((value) => typeof value === "string") ?? [];
247
- const ev = entryExecutionEvidence(entry);
248
- if (!ev)
249
- return [];
250
- const arr = ev.success_criteria_met;
251
- return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
252
- }
253
- /** Read success_criteria_remaining from an entry's execution_evidence. */
254
- function entryCriteriaRemaining(entry) {
255
- const view = committedView(entry);
256
- if (view)
257
- return view.executionEvidence?.success_criteria_remaining
258
- ?.filter((value) => typeof value === "string") ?? [];
259
- const ev = entryExecutionEvidence(entry);
260
- if (!ev)
261
- return [];
262
- const arr = ev.success_criteria_remaining;
263
- return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
264
- }
265
212
  /** v3.2: Deterministic lessons learned — constraints violated repeatedly or
266
213
  * verification checks failing repeatedly across rounds (full history, unlike
267
214
  * the enforcement gate's R2 3-round window). Presentation only: the output
@@ -273,7 +220,7 @@ export function deriveLessons(loopId, context, currentRound) {
273
220
  const warns = new Map();
274
221
  for (const entry of entries) {
275
222
  const rnd = entryRound(entry);
276
- const view = committedView(entry);
223
+ const view = decodeRound(entry);
277
224
  const viols = view
278
225
  ? view.constraintViolations ?? []
279
226
  : Array.isArray(entry.constraint_violations)
@@ -454,41 +401,10 @@ function detectNewCriteria(current, previous) {
454
401
  return currMet;
455
402
  return currMet.filter((curr) => prevMet.every((prev) => !criteriaMatch(curr, prev)));
456
403
  }
457
- /** Read constraints_active from an entry's lineage. */
458
- function entryActiveConstraints(entry) {
459
- const view = committedView(entry);
460
- if (view)
461
- return view.activeConstraints ?? [];
462
- const lin = lineage(entry);
463
- const arr = lin.constraints_active;
464
- return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
465
- }
466
- /** Read retracted_constraints from an entry. */
467
- function entryRetractedConstraints(entry) {
468
- const view = committedView(entry);
469
- if (view)
470
- return view.retractedConstraints ?? [];
471
- const direct = entry.retracted_constraints;
472
- if (Array.isArray(direct))
473
- return direct.filter((v) => typeof v === "string");
474
- const lin = lineage(entry);
475
- const nested = lin.retracted_constraints;
476
- if (Array.isArray(nested))
477
- return nested.filter((v) => typeof v === "string");
478
- return [];
479
- }
480
- /** Read progress_estimate from an entry's execution_evidence. */
481
- function entryProgressEstimate(entry) {
482
- const ev = entryExecutionEvidence(entry);
483
- if (!ev)
484
- return 0;
485
- const pe = ev.progress_estimate;
486
- return typeof pe === "number" ? pe : 0;
487
- }
488
404
  /** Build a single MilestoneSummary from a range of completed round entries. */
489
405
  function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, kind) {
490
406
  const last = phaseEntries.at(-1);
491
- const lastView = last ? committedView(last) : null;
407
+ const lastView = last ? decodeRound(last) : null;
492
408
  const outcomeRaw = lastView
493
409
  ? (lastView.outputSummary ?? "")
494
410
  : last && typeof last.output_summary === "string" ? last.output_summary : "";
@@ -572,8 +488,8 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
572
488
  const outcomes = [];
573
489
  const issues = [];
574
490
  for (const entry of windowEntries) {
575
- const view = committedView(entry);
576
- const data = lineage(entry);
491
+ const view = decodeRound(entry);
492
+ const data = entryLineage(entry);
577
493
  const round = entryRound(entry);
578
494
  const success = view ? view.success : entry.success ?? data.success;
579
495
  const summary = view
@@ -606,7 +522,7 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
606
522
  for (let i = 0; i < completedEntries.length; i++) {
607
523
  const entry = completedEntries[i];
608
524
  const rnd = entryRound(entry);
609
- const lin = lineage(entry);
525
+ const lin = entryLineage(entry);
610
526
  // Signal 1: Agent-declared checkpoint
611
527
  const isAgentCheckpoint = lin.compression_checkpoint === true;
612
528
  // Signal 2: New criteria met (compare with previous vault entry)
@@ -685,60 +601,13 @@ export function deriveConstraintId(text) {
685
601
  export function deriveCriterionId(text) {
686
602
  return "cr-" + deriveItemId(text);
687
603
  }
688
- /** v2.11: Check whether a user-provided reference looks like a constraint ID.
689
- * Matches the pattern c-XXXXXXXX where X is a hex digit. */
690
- export function isConstraintId(ref) {
691
- return /^c-[a-f0-9]{8}$/.test(ref);
692
- }
693
604
  /** v2.11: Check whether a user-provided reference looks like a criterion ID.
694
- * Matches the pattern cr-XXXXXXXX where X is a hex digit. */
695
- export function isCriterionId(ref) {
605
+ * Matches the pattern cr-XXXXXXXX where X is a hex digit. Module-local:
606
+ * consumers with the same need (round-contract.ts) keep their own copy to
607
+ * avoid an import cycle. */
608
+ function isCriterionId(ref) {
696
609
  return /^cr-[a-f0-9]{8}$/.test(ref);
697
610
  }
698
- /** Read emerged_subtasks from a vault entry (handles direct + lineage nesting). */
699
- function entryEmergedSubtasks(entry) {
700
- const view = committedView(entry);
701
- if (view)
702
- return view.emergedSubtasks ?? [];
703
- const direct = entry.emerged_subtasks;
704
- if (Array.isArray(direct))
705
- return direct.filter((v) => typeof v === "string");
706
- const lin = lineage(entry);
707
- const nested = lin.emerged_subtasks;
708
- if (Array.isArray(nested))
709
- return nested.filter((v) => typeof v === "string");
710
- return [];
711
- }
712
- /** Read completed_subtasks from an entry. */
713
- function entryCompletedSubtasks(entry) {
714
- const view = committedView(entry);
715
- if (view)
716
- return view.completedSubtasks ?? [];
717
- const direct = entry.completed_subtasks;
718
- if (Array.isArray(direct))
719
- return direct.filter((v) => typeof v === "string");
720
- return [];
721
- }
722
- /** Read blocked_subtasks from an entry. */
723
- function entryBlockedSubtasks(entry) {
724
- const view = committedView(entry);
725
- if (view)
726
- return view.blockedSubtasks ?? [];
727
- const direct = entry.blocked_subtasks;
728
- if (Array.isArray(direct))
729
- return direct.filter((v) => typeof v === "string");
730
- return [];
731
- }
732
- /** Read canceled_subtasks from an entry. */
733
- function entryCanceledSubtasks(entry) {
734
- const view = committedView(entry);
735
- if (view)
736
- return view.canceledSubtasks ?? [];
737
- const direct = entry.canceled_subtasks;
738
- if (Array.isArray(direct))
739
- return direct.filter((v) => typeof v === "string");
740
- return [];
741
- }
742
611
  /**
743
612
  * Match a user-provided sub-task reference against existing SubGoals.
744
613
  *
@@ -846,35 +715,10 @@ function manageSubGoals(loopId, currentRound, lastRoundResult, vaultContext) {
846
715
  subGoals[idx].status_changed_at_round = rnd;
847
716
  }
848
717
  }
849
- // Phase 3 — Auto-derive: next_action match → in_progress
850
- const nextAction = lastRoundResult.next_action?.trim();
851
- if (nextAction) {
852
- for (const sg of subGoals) {
853
- if (sg.status === "pending") {
854
- if (jaccardSimilarity(nextAction, sg.description) >= policy.subgoal_auto_in_progress_threshold) {
855
- sg.status = "in_progress";
856
- sg.status_changed_at_round = rnd;
857
- break; // Only one sub-goal can be in_progress at a time
858
- }
859
- }
860
- }
861
- }
862
- // Phase 4 — Auto-complete: new criteria match → done
863
- const newCriteria = lastRoundResult.execution_evidence?.success_criteria_met ?? [];
864
- if (newCriteria.length > 0) {
865
- for (const sg of subGoals) {
866
- if (sg.status === "pending" || sg.status === "in_progress") {
867
- for (const crit of newCriteria) {
868
- if (jaccardSimilarity(crit, sg.description) >= policy.subgoal_auto_complete_threshold) {
869
- sg.status = "done";
870
- sg.status_changed_at_round = rnd;
871
- sg.completed_at_round = rnd;
872
- break;
873
- }
874
- }
875
- }
876
- }
877
- }
718
+ // v3.7: no auto status inference. The v2.3 fuzzy heuristics
719
+ // (next_action Jaccard → in_progress, criteria Jaccard → done) were
720
+ // removed — they fabricated dashboard motion the machine never
721
+ // verified. Statuses change only via the agent declarations above.
878
722
  }
879
723
  // Sort for display: in_progress first, then pending by age desc, then blocked, then done, then canceled
880
724
  subGoals.sort((a, b) => {
@@ -915,40 +759,6 @@ function constraintSource(text, objective, constraintsFromPlan) {
915
759
  return "criteria";
916
760
  return "discovered";
917
761
  }
918
- /** Find the earliest round a constraint was discovered by scanning vault entries.
919
- * v2.11: ID-first matching. Checks exact constraint ID match (c-XXXXXXXX)
920
- * against the derived ID of each vault entry's discovered_constraints, then
921
- * falls back to Jaccard similarity on text. */
922
- function findDiscoveredRound(text, vaultEntries) {
923
- const targetId = deriveConstraintId(text);
924
- let earliest = 0;
925
- for (const entry of vaultEntries) {
926
- const rnd = entryRound(entry);
927
- if (rnd < 1)
928
- continue;
929
- const view = committedView(entry);
930
- const discovered = view
931
- ? view.discoveredConstraints ?? []
932
- : Array.isArray(entry.discovered_constraints)
933
- ? entry.discovered_constraints.filter((v) => typeof v === "string")
934
- : [];
935
- for (const d of discovered) {
936
- // Phase 1: exact ID match (v2.11)
937
- if (d === targetId || deriveConstraintId(d) === targetId) {
938
- if (earliest === 0 || rnd < earliest)
939
- earliest = rnd;
940
- break;
941
- }
942
- // Phase 2: Jaccard fallback for natural-language references.
943
- if (jaccardSimilarity(text, d) >= getPolicy().evolution.constraint_match_threshold) {
944
- if (earliest === 0 || rnd < earliest)
945
- earliest = rnd;
946
- break;
947
- }
948
- }
949
- }
950
- return earliest;
951
- }
952
762
  /** Find the latest round a constraint was violated by scanning vault entries.
953
763
  * v2.11: ID-first matching. Checks exact constraint ID match (c-XXXXXXXX)
954
764
  * against the derived ID of each vault entry's constraint_violations, then
@@ -969,7 +779,7 @@ function findLastViolatedRound(text, vaultEntries) {
969
779
  const rnd = entryRound(entry);
970
780
  if (rnd < 1)
971
781
  continue;
972
- const view = committedView(entry);
782
+ const view = decodeRound(entry);
973
783
  const violations = view
974
784
  ? view.constraintViolations ?? []
975
785
  : Array.isArray(entry.constraint_violations)
@@ -986,15 +796,17 @@ function findLastViolatedRound(text, vaultEntries) {
986
796
  return latest;
987
797
  }
988
798
  /**
989
- * Manage constraint lifecycle with time-aware decay.
799
+ * Manage constraint lifecycle metadata for the active set.
990
800
  *
991
- * Discovered constraints that haven't been violated for N rounds are demoted
992
- * to inactive — removed from prompts but kept in state. Hard/plan/criteria
993
- * constraints never auto-decay. Inactive constraints are auto-reactivated
994
- * if violated again in the current round.
801
+ * v3.7: the v2.3 time-aware decay (discovered constraints demoted to
802
+ * inactive after N quiet rounds, auto-reactivation on a fresh violation)
803
+ * is gone — every active constraint stays active. What remains is the
804
+ * display metadata the v3 prompt surface needs: per-text source
805
+ * classification (v3.3.1: criteria texts carry the cr-XXXXXXXX namespace),
806
+ * and last_violated_at_round for the L1 collapse "(violated this round)"
807
+ * annotation (diffConstraints compares it to the current round).
995
808
  */
996
809
  function manageConstraintLifecycle(active, objective, constraintsFromPlan, vaultContext, currentRound, lastRoundViolations = []) {
997
- const policy = getPolicy().evolution;
998
810
  const allEntries = loopEntries(objective.loop_id || "", vaultContext);
999
811
  const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
1000
812
  const metadata = [];
@@ -1004,57 +816,25 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
1004
816
  // as a violation at currentRound — the previous rounds' scan cannot see
1005
817
  // it (this round is not committed yet). This makes the L1 collapse
1006
818
  // render "(violated this round)" (diffConstraints compares
1007
- // last_violated_at_round === round) and re-activates decayed
1008
- // discovered constraints.
819
+ // last_violated_at_round === round).
1009
820
  const violatedThisRound = lastRoundViolations.some((v) => matchesConstraintText(text, v));
1010
- // Structural constraints never decay
1011
- if (source === "hard" || source === "plan" || source === "criteria") {
1012
- // v3.3.1: one text, one identity — a success-criterion text that also
1013
- // lives in the merged active set belongs to the cr-XXXXXXXX namespace
1014
- // (its own Success Criteria section and the verification gate's
1015
- // criteriaMatch are ID-first on cr-). Rendering it under c-XXXXXXXX in
1016
- // the active list meant the same text carried two IDs in one prompt,
1017
- // and an agent echoing the active-list ID into success_criteria_met
1018
- // could never match.
1019
- metadata.push(makeConstraintMeta({
1020
- id: source === "criteria" ? deriveCriterionId(text) : deriveConstraintId(text),
1021
- text,
1022
- discovered_at_round: objective.created_at_round || 1,
1023
- last_violated_at_round: violatedThisRound
1024
- ? currentRound
1025
- : findLastViolatedRound(text, completedEntries),
1026
- source,
1027
- status: "active",
1028
- }));
1029
- continue;
1030
- }
1031
- // Discovered constraints: check for inactivity
1032
- const discoveredAt = findDiscoveredRound(text, completedEntries);
1033
- const lastViolated = violatedThisRound
1034
- ? currentRound
1035
- : findLastViolatedRound(text, completedEntries);
1036
- const roundsSinceRelevant = lastViolated > 0
1037
- ? currentRound - lastViolated
1038
- : currentRound - (discoveredAt || currentRound);
1039
- const status = roundsSinceRelevant > policy.constraint_inactive_rounds
1040
- ? "inactive"
1041
- : "active";
821
+ // v3.3.1: one text, one identity — a success-criterion text that also
822
+ // lives in the merged active set belongs to the cr-XXXXXXXX namespace
823
+ // (its own Success Criteria section and the verification gate's
824
+ // criteriaMatch are ID-first on cr-). Rendering it under c-XXXXXXXX in
825
+ // the active list meant the same text carried two IDs in one prompt,
826
+ // and an agent echoing the active-list ID into success_criteria_met
827
+ // could never match.
1042
828
  metadata.push(makeConstraintMeta({
1043
- id: deriveConstraintId(text),
829
+ id: source === "criteria" ? deriveCriterionId(text) : deriveConstraintId(text),
1044
830
  text,
1045
- discovered_at_round: discoveredAt || 0,
1046
- last_violated_at_round: lastViolated,
1047
- source: "discovered",
1048
- status,
831
+ last_violated_at_round: violatedThisRound
832
+ ? currentRound
833
+ : findLastViolatedRound(text, completedEntries),
834
+ source,
1049
835
  }));
1050
836
  }
1051
- const newActive = metadata
1052
- .filter((m) => m.status === "active")
1053
- .map((m) => m.text);
1054
- const inactive = metadata
1055
- .filter((m) => m.status === "inactive")
1056
- .map((m) => m.text);
1057
- return { active: newActive, inactive, metadata };
837
+ return { active, metadata };
1058
838
  }
1059
839
  export function alignTask(proposedTask, request, context) {
1060
840
  const objective = request.loop_objective ?? latestObjective(request.loop_id, context);
@@ -1099,7 +879,7 @@ function levelDecision(request, context) {
1099
879
  last?.revised_success_criteria?.length ||
1100
880
  last?.wrong_assumptions?.length);
1101
881
  const lastFullRound = loopEntries(request.loop_id, context)
1102
- .filter((entry) => lineage(entry).recompile_level === "l2")
882
+ .filter((entry) => entryLineage(entry).recompile_level === "l2")
1103
883
  .map(entryRound)
1104
884
  .filter((round) => round < request.round)
1105
885
  .at(-1) ?? 1;
@@ -1108,7 +888,7 @@ function levelDecision(request, context) {
1108
888
  // rehydration (the decidePromptLevel recovery_boundary branch existed
1109
889
  // but no caller ever set it).
1110
890
  const recoveryBoundary = loopEntries(request.loop_id, context)
1111
- .some((entry) => lineage(entry).committed_action === "backtrack" &&
891
+ .some((entry) => entryLineage(entry).committed_action === "backtrack" &&
1112
892
  entryRound(entry) === request.round);
1113
893
  return decidePromptLevel({
1114
894
  round: request.round,
@@ -1285,7 +1065,6 @@ export function compileLoop(request, context) {
1285
1065
  lineage: [`${request.loop_id}:r${request.round}`],
1286
1066
  constraints_active: constraintLifecycle.active,
1287
1067
  constraints_retired: constraints.retired,
1288
- constraints_inactive: constraintLifecycle.inactive,
1289
1068
  constraint_metadata: constraintLifecycle.metadata,
1290
1069
  loop_id: request.loop_id,
1291
1070
  round: request.round,
@@ -1329,7 +1108,6 @@ export function compileLoop(request, context) {
1329
1108
  state,
1330
1109
  level: decision.level,
1331
1110
  reasons: decision.reasons,
1332
- mode: policy.prompt.injection_mode,
1333
1111
  budgets: {
1334
1112
  l0: policy.prompt.l0_max_chars,
1335
1113
  l1: policy.prompt.l1_max_chars,