@aragon-agent/cli 0.6.43 → 0.6.45

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 (251) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +69 -2
  3. package/dist/agent/controller.d.ts +69 -0
  4. package/dist/agent/controller.js +224 -11
  5. package/dist/agent/governance-view.d.ts +40 -0
  6. package/dist/agent/governance-view.js +40 -0
  7. package/dist/agent/governance-wiring.d.ts +162 -0
  8. package/dist/agent/governance-wiring.js +470 -0
  9. package/dist/agent/plan-prompt.d.ts +7 -0
  10. package/dist/agent/plan-prompt.js +2 -1
  11. package/dist/agent/reducer.d.ts +33 -1
  12. package/dist/agent/reducer.js +45 -9
  13. package/dist/agent/system-prompt.d.ts +19 -0
  14. package/dist/agent/system-prompt.js +9 -1
  15. package/dist/cli.js +45 -1
  16. package/dist/commands/builtins.js +10 -4
  17. package/dist/commands/governance-command.d.ts +35 -0
  18. package/dist/commands/governance-command.js +292 -0
  19. package/dist/commands/governance-runs-table.d.ts +40 -0
  20. package/dist/commands/governance-runs-table.js +130 -0
  21. package/dist/commands/registry.d.ts +12 -0
  22. package/dist/config/app-paths.d.ts +10 -0
  23. package/dist/config/app-paths.js +12 -0
  24. package/dist/config/cli-commands.d.ts +18 -0
  25. package/dist/config/cli-commands.js +47 -1
  26. package/dist/config/env.d.ts +2 -1
  27. package/dist/config/env.js +10 -1
  28. package/dist/config/governance-config.d.ts +125 -0
  29. package/dist/config/governance-config.js +164 -0
  30. package/dist/config/governance-launch.d.ts +71 -0
  31. package/dist/config/governance-launch.js +58 -0
  32. package/dist/config/load.d.ts +15 -0
  33. package/dist/config/load.js +26 -1
  34. package/dist/config/model-profile-store.d.ts +4 -2
  35. package/dist/config/schema.d.ts +22 -0
  36. package/dist/config/schema.js +6 -0
  37. package/dist/config/store.js +14 -1
  38. package/dist/config/ui-state.d.ts +11 -0
  39. package/dist/config/ui-state.js +11 -0
  40. package/dist/diagnostics/doctor.js +20 -0
  41. package/dist/governance/card-normalize.d.ts +24 -0
  42. package/dist/governance/card-normalize.js +76 -0
  43. package/dist/governance/copy.d.ts +109 -0
  44. package/dist/governance/copy.js +223 -0
  45. package/dist/governance/engine/aftermath.d.ts +49 -0
  46. package/dist/governance/engine/aftermath.js +134 -0
  47. package/dist/governance/engine/artifact-persist.d.ts +35 -0
  48. package/dist/governance/engine/artifact-persist.js +86 -0
  49. package/dist/governance/engine/budget.d.ts +52 -0
  50. package/dist/governance/engine/budget.js +112 -0
  51. package/dist/governance/engine/cards.d.ts +18 -0
  52. package/dist/governance/engine/cards.js +186 -0
  53. package/dist/governance/engine/digest.d.ts +32 -0
  54. package/dist/governance/engine/digest.js +63 -0
  55. package/dist/governance/engine/engine-core.d.ts +219 -0
  56. package/dist/governance/engine/engine-core.js +562 -0
  57. package/dist/governance/engine/engine.d.ts +68 -0
  58. package/dist/governance/engine/engine.js +314 -0
  59. package/dist/governance/engine/estimate.d.ts +38 -0
  60. package/dist/governance/engine/estimate.js +46 -0
  61. package/dist/governance/engine/inspector.d.ts +16 -0
  62. package/dist/governance/engine/inspector.js +396 -0
  63. package/dist/governance/engine/integration.d.ts +35 -0
  64. package/dist/governance/engine/integration.js +220 -0
  65. package/dist/governance/engine/lifecycle.d.ts +29 -0
  66. package/dist/governance/engine/lifecycle.js +656 -0
  67. package/dist/governance/engine/pause.d.ts +56 -0
  68. package/dist/governance/engine/pause.js +107 -0
  69. package/dist/governance/engine/phase-members.d.ts +15 -0
  70. package/dist/governance/engine/phase-members.js +34 -0
  71. package/dist/governance/engine/phase-planner.d.ts +53 -0
  72. package/dist/governance/engine/phase-planner.js +282 -0
  73. package/dist/governance/engine/rebuild.d.ts +27 -0
  74. package/dist/governance/engine/rebuild.js +57 -0
  75. package/dist/governance/engine/report.d.ts +66 -0
  76. package/dist/governance/engine/report.js +220 -0
  77. package/dist/governance/engine/run-digest.d.ts +20 -0
  78. package/dist/governance/engine/run-digest.js +42 -0
  79. package/dist/governance/engine/spec-builder.d.ts +117 -0
  80. package/dist/governance/engine/spec-builder.js +321 -0
  81. package/dist/governance/engine/stage-runner.d.ts +22 -0
  82. package/dist/governance/engine/stage-runner.js +664 -0
  83. package/dist/governance/engine/start-input.d.ts +19 -0
  84. package/dist/governance/engine/start-input.js +111 -0
  85. package/dist/governance/engine/state-machine.d.ts +101 -0
  86. package/dist/governance/engine/state-machine.js +586 -0
  87. package/dist/governance/engine/submission-queries.d.ts +42 -0
  88. package/dist/governance/engine/submission-queries.js +97 -0
  89. package/dist/governance/engine/submission-render.d.ts +12 -0
  90. package/dist/governance/engine/submission-render.js +92 -0
  91. package/dist/governance/engine/submissions.d.ts +27 -0
  92. package/dist/governance/engine/submissions.js +455 -0
  93. package/dist/governance/engine/visibility.d.ts +104 -0
  94. package/dist/governance/engine/visibility.js +223 -0
  95. package/dist/governance/governed-dispatch.d.ts +19 -0
  96. package/dist/governance/governed-dispatch.js +17 -0
  97. package/dist/governance/limits.d.ts +107 -0
  98. package/dist/governance/limits.js +131 -0
  99. package/dist/governance/posture-transition.d.ts +38 -0
  100. package/dist/governance/posture-transition.js +44 -0
  101. package/dist/governance/posture.d.ts +35 -0
  102. package/dist/governance/posture.js +59 -0
  103. package/dist/governance/profiles.d.ts +48 -0
  104. package/dist/governance/profiles.js +140 -0
  105. package/dist/governance/prompts/orchestrator-block.d.ts +40 -0
  106. package/dist/governance/prompts/orchestrator-block.js +128 -0
  107. package/dist/governance/prompts/plan-addendum.d.ts +22 -0
  108. package/dist/governance/prompts/plan-addendum.js +60 -0
  109. package/dist/governance/prompts/refusals.d.ts +48 -0
  110. package/dist/governance/prompts/refusals.js +82 -0
  111. package/dist/governance/prompts/role-blocks.d.ts +40 -0
  112. package/dist/governance/prompts/role-blocks.js +255 -0
  113. package/dist/governance/report-reader.d.ts +31 -0
  114. package/dist/governance/report-reader.js +57 -0
  115. package/dist/governance/roles.d.ts +83 -0
  116. package/dist/governance/roles.js +142 -0
  117. package/dist/governance/rules/claims.d.ts +83 -0
  118. package/dist/governance/rules/claims.js +250 -0
  119. package/dist/governance/rules/elimination.d.ts +66 -0
  120. package/dist/governance/rules/elimination.js +105 -0
  121. package/dist/governance/rules/events-table.d.ts +46 -0
  122. package/dist/governance/rules/events-table.js +94 -0
  123. package/dist/governance/rules/failure-analysis.d.ts +28 -0
  124. package/dist/governance/rules/failure-analysis.js +93 -0
  125. package/dist/governance/rules/ledger-replay.d.ts +53 -0
  126. package/dist/governance/rules/ledger-replay.js +53 -0
  127. package/dist/governance/rules/ledger-state.d.ts +85 -0
  128. package/dist/governance/rules/ledger-state.js +175 -0
  129. package/dist/governance/rules/ledger.d.ts +228 -0
  130. package/dist/governance/rules/ledger.js +560 -0
  131. package/dist/governance/rules/rca.d.ts +68 -0
  132. package/dist/governance/rules/rca.js +220 -0
  133. package/dist/governance/rules/settlement.d.ts +129 -0
  134. package/dist/governance/rules/settlement.js +363 -0
  135. package/dist/governance/rules/suspicion.d.ts +66 -0
  136. package/dist/governance/rules/suspicion.js +169 -0
  137. package/dist/governance/store/artifacts.d.ts +81 -0
  138. package/dist/governance/store/artifacts.js +173 -0
  139. package/dist/governance/store/atomic.d.ts +79 -0
  140. package/dist/governance/store/atomic.js +119 -0
  141. package/dist/governance/store/events-log.d.ts +35 -0
  142. package/dist/governance/store/events-log.js +86 -0
  143. package/dist/governance/store/run-lock.d.ts +51 -0
  144. package/dist/governance/store/run-lock.js +122 -0
  145. package/dist/governance/store/run-store.d.ts +229 -0
  146. package/dist/governance/store/run-store.js +644 -0
  147. package/dist/governance/store/scoreboard-export.d.ts +53 -0
  148. package/dist/governance/store/scoreboard-export.js +65 -0
  149. package/dist/governance/tools/child-tools.d.ts +31 -0
  150. package/dist/governance/tools/child-tools.js +65 -0
  151. package/dist/governance/tools/guard.d.ts +71 -0
  152. package/dist/governance/tools/guard.js +194 -0
  153. package/dist/governance/tools/inspector-tools.d.ts +63 -0
  154. package/dist/governance/tools/inspector-tools.js +424 -0
  155. package/dist/governance/tools/lead-tools.d.ts +42 -0
  156. package/dist/governance/tools/lead-tools.js +226 -0
  157. package/dist/governance/tools/normalize-util.d.ts +101 -0
  158. package/dist/governance/tools/normalize-util.js +230 -0
  159. package/dist/governance/tools/scan-incomplete.d.ts +79 -0
  160. package/dist/governance/tools/scan-incomplete.js +486 -0
  161. package/dist/governance/tools/shared-tools.d.ts +44 -0
  162. package/dist/governance/tools/shared-tools.js +125 -0
  163. package/dist/governance/tools/submit-normalizers.d.ts +28 -0
  164. package/dist/governance/tools/submit-normalizers.js +474 -0
  165. package/dist/governance/tools/submit-tools.d.ts +26 -0
  166. package/dist/governance/tools/submit-tools.js +171 -0
  167. package/dist/governance/types.d.ts +661 -0
  168. package/dist/governance/types.js +27 -0
  169. package/dist/proc/kill-tree.d.ts +19 -1
  170. package/dist/proc/kill-tree.js +32 -5
  171. package/dist/proc/limits.d.ts +19 -0
  172. package/dist/proc/limits.js +19 -0
  173. package/dist/proc/supervisor.js +8 -1
  174. package/dist/session/validate-session.js +32 -1
  175. package/dist/team/runtime.d.ts +8 -0
  176. package/dist/team/runtime.js +9 -0
  177. package/dist/team/subagent.d.ts +23 -0
  178. package/dist/team/subagent.js +14 -3
  179. package/dist/team/types.d.ts +6 -0
  180. package/dist/tools/bounded-spawn.d.ts +56 -0
  181. package/dist/tools/bounded-spawn.js +76 -0
  182. package/dist/tools/index.d.ts +30 -7
  183. package/dist/tools/index.js +14 -0
  184. package/dist/tools/plan-tools.d.ts +14 -2
  185. package/dist/tools/plan-tools.js +5 -2
  186. package/dist/tools/search-tools.js +52 -44
  187. package/dist/ui/App.js +145 -25
  188. package/dist/ui/BottomStatusRow.js +1 -1
  189. package/dist/ui/Composer.d.ts +7 -1
  190. package/dist/ui/Composer.js +9 -6
  191. package/dist/ui/GovernancePanel.d.ts +50 -0
  192. package/dist/ui/GovernancePanel.js +77 -0
  193. package/dist/ui/GovernanceRun.d.ts +18 -0
  194. package/dist/ui/GovernanceRun.js +11 -0
  195. package/dist/ui/ScrollbackShell.d.ts +35 -7
  196. package/dist/ui/ScrollbackShell.js +1 -1
  197. package/dist/ui/StatusBar.d.ts +15 -2
  198. package/dist/ui/StatusBar.js +10 -4
  199. package/dist/ui/Transcript.js +3 -0
  200. package/dist/ui/entries/GovernanceCard.d.ts +30 -0
  201. package/dist/ui/entries/GovernanceCard.js +32 -0
  202. package/dist/ui/governance-copy.d.ts +69 -0
  203. package/dist/ui/governance-copy.js +104 -0
  204. package/dist/ui/governance-status.d.ts +145 -0
  205. package/dist/ui/governance-status.js +326 -0
  206. package/dist/ui/interaction-copy.d.ts +4 -0
  207. package/dist/ui/interaction-copy.js +5 -0
  208. package/dist/ui/interaction-hints.d.ts +17 -0
  209. package/dist/ui/interaction-hints.js +14 -2
  210. package/dist/ui/layout/governance-board.d.ts +121 -0
  211. package/dist/ui/layout/governance-board.js +382 -0
  212. package/dist/ui/layout/governance-card.d.ts +53 -0
  213. package/dist/ui/layout/governance-card.js +134 -0
  214. package/dist/ui/layout/governance-chip.d.ts +38 -0
  215. package/dist/ui/layout/governance-chip.js +64 -0
  216. package/dist/ui/layout/governance-panel.d.ts +85 -0
  217. package/dist/ui/layout/governance-panel.js +358 -0
  218. package/dist/ui/layout/governance-roster.d.ts +65 -0
  219. package/dist/ui/layout/governance-roster.js +275 -0
  220. package/dist/ui/layout/status-detail-layout.js +3 -3
  221. package/dist/ui/layout/status-layout.d.ts +5 -2
  222. package/dist/ui/layout/status-layout.js +10 -6
  223. package/dist/ui/layout/virtual-window.js +11 -0
  224. package/dist/ui/overlays/GovernanceBoardOverlay.d.ts +41 -0
  225. package/dist/ui/overlays/GovernanceBoardOverlay.js +76 -0
  226. package/dist/ui/overlays/GovernanceSettingsPage.d.ts +48 -0
  227. package/dist/ui/overlays/GovernanceSettingsPage.js +93 -0
  228. package/dist/ui/overlays/HelpOverlay.js +4 -3
  229. package/dist/ui/overlays/QuestionOverlay.d.ts +9 -1
  230. package/dist/ui/overlays/QuestionOverlay.js +4 -2
  231. package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
  232. package/dist/ui/overlays/SettingsScreen.js +21 -1
  233. package/dist/ui/overlays/governance-settings.d.ts +79 -0
  234. package/dist/ui/overlays/governance-settings.js +140 -0
  235. package/dist/ui/overlays/model-profile-state.d.ts +6 -1
  236. package/dist/ui/overlays/model-profile-state.js +10 -0
  237. package/dist/ui/palettes.d.ts +17 -0
  238. package/dist/ui/palettes.js +9 -0
  239. package/dist/ui/scrollback/LiveTail.d.ts +3 -1
  240. package/dist/ui/scrollback/budget.d.ts +12 -6
  241. package/dist/ui/scrollback/budget.js +11 -4
  242. package/dist/ui/scrollback/live-preview.js +2 -0
  243. package/dist/ui/scrollback/settled-boundary.js +3 -0
  244. package/dist/ui/scrollback/tail-anchor.d.ts +5 -2
  245. package/dist/ui/scrollback/tail-anchor.js +3 -1
  246. package/dist/ui/shortcuts.js +2 -2
  247. package/dist/ui/theme.d.ts +7 -0
  248. package/dist/ui/theme.js +1 -0
  249. package/dist/ui/transcript-text.js +9 -0
  250. package/dist/ui/use-wheel-routing.js +3 -1
  251. package/package.json +2 -2
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Identities, roster sizing and work-package assignment (governance-mode 5.2).
3
+ *
4
+ * ASCII ONLY (`governance/` is glyph-scanned). Pure: no I/O, no clock, no randomness, so a
5
+ * resumed run recomputes exactly the same roster.
6
+ *
7
+ * IDENTITIES ARE LEDGER ENTITIES, NOT PROCESSES (5.2). Every dispatch builds a fresh
8
+ * `Agent`; continuity comes from the ledger and the artifact store. The id is also the
9
+ * subagent `label`, so it must satisfy the label rule `[a-z0-9-]{1,12}` AND must never be
10
+ * the reserved aggregate label `team` (a child labelled `team` would be addressed by
11
+ * `team_send {to:"team"}` as a broadcast). Ids here are `<prefix><slot>` and
12
+ * `<id>-r<generation>`, which cannot spell `team` by construction.
13
+ *
14
+ * "THE WRITER NEVER REVIEWS THEMSELF" IS STRUCTURAL: DEV, REV and TEST ids come from three
15
+ * disjoint prefixes, so no assignment function can hand a work package's author the review
16
+ * of it.
17
+ */
18
+ /** The subagent label rule every identity satisfies. */
19
+ export const IDENTITY_ID_PATTERN = /^[a-z0-9-]{1,12}$/;
20
+ /** The single inspector's id. Never scored, never respawned. */
21
+ export const INSPECTOR_ID = 'insp';
22
+ const ROLE_PREFIX = { DEV: 'dev', REV: 'rev', TEST: 'tst' };
23
+ const PREFIX_ROLE = { dev: 'DEV', rev: 'REV', tst: 'TEST' };
24
+ /** REV / TEST roster is `clamp(ceil(K / 2), 1, 3)` (5.2). */
25
+ const REVIEW_ROSTER_MIN = 1;
26
+ const REVIEW_ROSTER_MAX = 3;
27
+ /** Highest slot / generation an id can carry and still fit 12 characters. */
28
+ const MAX_SLOT = 999;
29
+ const MAX_GENERATION = 99;
30
+ /** The roster id of slot `slot` (1-based) of a scored role: `dev1`, `rev2`, `tst1`. */
31
+ export function makeIdentityId(role, slot) {
32
+ if (!Number.isInteger(slot) || slot < 1 || slot > MAX_SLOT) {
33
+ throw new Error(`makeIdentityId: slot must be an integer in 1..${MAX_SLOT}, got ${slot}`);
34
+ }
35
+ return `${ROLE_PREFIX[role]}${slot}`;
36
+ }
37
+ /** `dev2` -> `dev2-r1`; `dev2-r1` -> `dev2-r2`. The base slot never changes. */
38
+ export function respawnIdentityId(predecessorId) {
39
+ const parsed = parseIdentityId(predecessorId);
40
+ if (!parsed || parsed.role === 'INSP') {
41
+ throw new Error(`respawnIdentityId: "${predecessorId}" is not a respawnable identity`);
42
+ }
43
+ const generation = parsed.generation + 1;
44
+ if (generation > MAX_GENERATION) {
45
+ throw new Error(`respawnIdentityId: generation ${generation} exceeds ${MAX_GENERATION}`);
46
+ }
47
+ return `${ROLE_PREFIX[parsed.role]}${parsed.slot}-r${generation}`;
48
+ }
49
+ /** Inverse of `makeIdentityId` / `respawnIdentityId`; `null` for anything else. */
50
+ export function parseIdentityId(id) {
51
+ if (id === INSPECTOR_ID)
52
+ return { role: 'INSP', slot: 0, generation: 0 };
53
+ const m = /^(dev|rev|tst)([1-9][0-9]{0,2})(?:-r([1-9][0-9]?))?$/.exec(id);
54
+ if (!m)
55
+ return null;
56
+ const role = PREFIX_ROLE[m[1]];
57
+ if (!role)
58
+ return null;
59
+ return { role, slot: Number(m[2]), generation: m[3] ? Number(m[3]) : 0 };
60
+ }
61
+ /** The ids `dev1..devN` for `count` slots of `role`. */
62
+ export function rosterIds(role, count) {
63
+ return Array.from({ length: Math.max(0, count) }, (_, i) => makeIdentityId(role, i + 1));
64
+ }
65
+ /**
66
+ * Roster size for `wpCount` work packages (5.2): `DEV = K`, `REV = TEST =
67
+ * clamp(ceil(K/2), 1, 3)`. `stageWidthCap` (the user's `team.maxSubagents`, D-24) bounds the
68
+ * REV / TEST counts, because those are exactly the identities that run side by side in one
69
+ * review / test stage. DEV is NOT bounded: the engine splits a wide dev stage into chunks
70
+ * (the `NEXT` it prints is already chunked), so the roster may exceed one dispatch.
71
+ */
72
+ export function rosterSizes(wpCount, stageWidthCap) {
73
+ const k = Math.max(1, Math.floor(wpCount));
74
+ const cap = Math.max(1, Math.floor(stageWidthCap));
75
+ const reviewers = Math.min(cap, Math.min(REVIEW_ROSTER_MAX, Math.max(REVIEW_ROSTER_MIN, Math.ceil(k / 2))));
76
+ return { dev: k, rev: reviewers, test: reviewers };
77
+ }
78
+ /** Follow `takeover` links to the identity that now owns `id`'s work; cycle-safe. */
79
+ function resolveSuccessor(id, takeover) {
80
+ const seen = new Set();
81
+ let current = id;
82
+ while (takeover[current] !== undefined && !seen.has(current)) {
83
+ seen.add(current);
84
+ current = takeover[current];
85
+ }
86
+ return current;
87
+ }
88
+ /**
89
+ * Assign work packages to identities.
90
+ *
91
+ * 1. A WP with a sticky preference goes to that identity - or to its successor if it was
92
+ * replaced - as long as the result is an active identity.
93
+ * 2. Every other WP goes to the identity with the fewest WPs so far, ties broken by roster
94
+ * order. With no stickiness this is exactly "WP i goes to identity i mod M" (5.2); with
95
+ * stickiness it keeps the load level instead of piling every new WP on the identity that
96
+ * happens to come next in a fixed rotation.
97
+ *
98
+ * Identities that receive nothing are omitted, so the caller dispatches no idle child.
99
+ * Deterministic: the same input always yields the same output.
100
+ */
101
+ export function assignRoundRobin(options) {
102
+ const { wpIds, identities, sticky = {}, takeover = {} } = options;
103
+ if (identities.length === 0) {
104
+ throw new Error('assignRoundRobin: at least one active identity is required');
105
+ }
106
+ const active = new Set(identities);
107
+ const buckets = new Map(identities.map((id) => [id, []]));
108
+ const pinned = new Map();
109
+ for (const wpId of wpIds) {
110
+ const preferred = sticky[wpId];
111
+ if (preferred === undefined)
112
+ continue;
113
+ const owner = resolveSuccessor(preferred, takeover);
114
+ if (active.has(owner))
115
+ pinned.set(wpId, owner);
116
+ }
117
+ for (const wpId of wpIds) {
118
+ const owner = pinned.get(wpId);
119
+ if (owner !== undefined)
120
+ buckets.get(owner).push(wpId);
121
+ }
122
+ for (const wpId of wpIds) {
123
+ if (pinned.has(wpId))
124
+ continue;
125
+ buckets.get(leastLoaded(identities, buckets)).push(wpId);
126
+ }
127
+ const order = new Map(wpIds.map((id, i) => [id, i]));
128
+ return identities
129
+ .map((identity) => ({
130
+ identity,
131
+ wpIds: buckets.get(identity).sort((a, b) => order.get(a) - order.get(b)),
132
+ }))
133
+ .filter((a) => a.wpIds.length > 0);
134
+ }
135
+ function leastLoaded(identities, buckets) {
136
+ let best = identities[0];
137
+ for (const id of identities) {
138
+ if (buckets.get(id).length < buckets.get(best).length)
139
+ best = id;
140
+ }
141
+ return best;
142
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Claims and finding fingerprints (governance-mode 5.5 / 5.6).
3
+ *
4
+ * ASCII ONLY (`governance/` is glyph-scanned).
5
+ *
6
+ * A CLAIM is an accusation that books NOTHING when it is raised: a tester who says "the
7
+ * reviewer missed this" earns +10 only if the inspector upholds it in the settle stage, and
8
+ * pays -10 if it is rejected. Deferring the booking is what keeps claims from being a
9
+ * score farm ("accuse everyone, collect T2").
10
+ *
11
+ * FINGERPRINTS decide "the same problem rejected twice" (D8) WITHOUT a language model:
12
+ * `normalized file + sorted keyword set of the issue text`. Two findings count as the same
13
+ * when they name the same file and share most of their keywords. Deterministic, so the
14
+ * result is reproducible from the event log and pinned by fixed examples in the tests.
15
+ */
16
+ import type { Claim, LedgerState, ReviewSubmission, TestSubmission } from '../types.js';
17
+ import type { LedgerEvent } from './ledger.js';
18
+ /** Lower-case, forward slashes, no leading `./`: so `.\\Src\\A.ts` and `src/a.ts` agree. */
19
+ export declare function normalizeFindingPath(file: string): string;
20
+ /**
21
+ * The sorted, de-duplicated keyword set of an issue text: lower-cased, punctuation
22
+ * stripped, stop-words and bare numbers (line numbers drift between rounds) dropped.
23
+ * Falls back to the unfiltered tokens, then to `_`, so an issue never fingerprints to an
24
+ * empty set that would match every other finding on the same file.
25
+ */
26
+ export declare function issueKeywords(issue: string): string[];
27
+ export interface FingerprintInput {
28
+ file: string;
29
+ issue: string;
30
+ }
31
+ /** `file#kw1+kw2+...`. Deterministic; the unit tests pin fixed examples. */
32
+ export declare function fingerprintFinding(finding: FingerprintInput): string;
33
+ /**
34
+ * D8 test: does any new finding repeat a previously rejected one? Same file AND a keyword
35
+ * overlap of at least 60 percent of the smaller set (and at least two shared keywords
36
+ * unless the smaller set has one).
37
+ */
38
+ export declare function findingsIntersect(a: readonly string[], b: readonly string[]): boolean;
39
+ export interface ClaimContext {
40
+ wpId: string;
41
+ phase: number;
42
+ /** The DEV holding the WP. */
43
+ developer: string | null;
44
+ /** The REV assigned to the WP. */
45
+ reviewer: string | null;
46
+ }
47
+ /**
48
+ * Claims implied by a test submission: one per kind, never one per issue (a tester cannot
49
+ * multiply T2 by splitting one defect into five lines). Explicit `claims[]` come first
50
+ * and win over an implied claim of the same kind. Ids are deterministic
51
+ * (`cl-<wp>-t<attempt>-<n>`) so replay and tests need no id generator.
52
+ */
53
+ export declare function claimsFromTest(ctx: ClaimContext, submission: TestSubmission): Claim[];
54
+ /** A reviewer's `securityFinding` becomes a security claim against the developer. */
55
+ export declare function claimsFromReview(ctx: ClaimContext, submission: ReviewSubmission): Claim[];
56
+ export interface RuleClaimInput {
57
+ /** Ledger state: only used to look up the roles of the parties. */
58
+ state: LedgerState;
59
+ claim: Claim;
60
+ decision: 'upheld' | 'rejected';
61
+ ruling: string;
62
+ at: number;
63
+ /** The inspector's identity, e.g. `insp`. */
64
+ inspectorId: string;
65
+ }
66
+ export interface RuleClaimResult {
67
+ claim: Claim;
68
+ events: LedgerEvent[];
69
+ notes: string[];
70
+ }
71
+ /**
72
+ * Expand an inspector ruling into ledger events (5.6 L2):
73
+ *
74
+ * - upheld missed_by_review: R4 against the reviewer, T2 for the tester.
75
+ * - upheld incomplete_impl: R7 against the reviewer, T7 for the tester.
76
+ * - upheld security: D6 against the developer. upheld same_issue: D8 against the developer.
77
+ * - upheld false_positive: R5 / T5 against the party whose finding was wrong.
78
+ * - ANY rejected claim: T5 (tester) or R5 (reviewer) against the CLAIMANT.
79
+ *
80
+ * Idempotent: a claim that is no longer `open` yields no events, so a repeated
81
+ * `ledger_rule` call cannot pay twice.
82
+ */
83
+ export declare function ruleClaim(input: RuleClaimInput): RuleClaimResult;
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Claims and finding fingerprints (governance-mode 5.5 / 5.6).
3
+ *
4
+ * ASCII ONLY (`governance/` is glyph-scanned).
5
+ *
6
+ * A CLAIM is an accusation that books NOTHING when it is raised: a tester who says "the
7
+ * reviewer missed this" earns +10 only if the inspector upholds it in the settle stage, and
8
+ * pays -10 if it is rejected. Deferring the booking is what keeps claims from being a
9
+ * score farm ("accuse everyone, collect T2").
10
+ *
11
+ * FINGERPRINTS decide "the same problem rejected twice" (D8) WITHOUT a language model:
12
+ * `normalized file + sorted keyword set of the issue text`. Two findings count as the same
13
+ * when they name the same file and share most of their keywords. Deterministic, so the
14
+ * result is reproducible from the event log and pinned by fixed examples in the tests.
15
+ */
16
+ import { eventDef } from './events-table.js';
17
+ // ---------------------------------------------------------------------------
18
+ // Fingerprints
19
+ // ---------------------------------------------------------------------------
20
+ const STOP_WORDS = new Set([
21
+ 'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'am',
22
+ 'of', 'in', 'on', 'at', 'to', 'for', 'from', 'by', 'with', 'without', 'within',
23
+ 'and', 'or', 'but', 'nor', 'not', 'no', 'this', 'that', 'these', 'those',
24
+ 'it', 'its', 'as', 'if', 'then', 'than', 'so', 'do', 'does', 'did', 'done',
25
+ 'has', 'have', 'had', 'should', 'must', 'can', 'could', 'would', 'will', 'shall',
26
+ 'may', 'might', 'there', 'here', 'which', 'when', 'where', 'while', 'into',
27
+ 'onto', 'over', 'under', 'also', 'only', 'very', 'too', 'any', 'all', 'some',
28
+ 'each', 'every', 'more', 'most', 'less', 'such', 'per', 'via', 'about',
29
+ 'because', 'however', 'currently', 'still', 'just', 'line', 'lines',
30
+ ]);
31
+ /** Separator between the file part and the keyword part of a fingerprint. */
32
+ const FP_SEPARATOR = '#';
33
+ /** Lower-case, forward slashes, no leading `./`: so `.\\Src\\A.ts` and `src/a.ts` agree. */
34
+ export function normalizeFindingPath(file) {
35
+ return file.trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/{2,}/g, '/').toLowerCase();
36
+ }
37
+ /** Plural `s` only: enough to unify "handles"/"handle"; anything cleverer breeds collisions. */
38
+ function stem(word) {
39
+ if (word.length > 3 && word.endsWith('s') && !/(ss|us|is)$/.test(word))
40
+ return word.slice(0, -1);
41
+ return word;
42
+ }
43
+ /**
44
+ * The sorted, de-duplicated keyword set of an issue text: lower-cased, punctuation
45
+ * stripped, stop-words and bare numbers (line numbers drift between rounds) dropped.
46
+ * Falls back to the unfiltered tokens, then to `_`, so an issue never fingerprints to an
47
+ * empty set that would match every other finding on the same file.
48
+ */
49
+ export function issueKeywords(issue) {
50
+ const tokens = issue
51
+ .toLowerCase()
52
+ .replace(/[^\p{L}\p{N}]+/gu, ' ')
53
+ .split(' ')
54
+ .filter((t) => t.length > 0);
55
+ const meaningful = tokens.filter((t) => !STOP_WORDS.has(t) && !/^\d+$/.test(t)).map(stem);
56
+ const chosen = meaningful.length > 0 ? meaningful : tokens;
57
+ const unique = [...new Set(chosen)].sort();
58
+ return unique.length > 0 ? unique : ['_'];
59
+ }
60
+ /** `file#kw1+kw2+...`. Deterministic; the unit tests pin fixed examples. */
61
+ export function fingerprintFinding(finding) {
62
+ const file = normalizeFindingPath(finding.file);
63
+ return `${file}${FP_SEPARATOR}${issueKeywords(finding.issue).join('+')}`;
64
+ }
65
+ function parseFingerprint(fingerprint) {
66
+ const at = fingerprint.lastIndexOf(FP_SEPARATOR);
67
+ if (at < 0)
68
+ return { file: fingerprint, keywords: [] };
69
+ return {
70
+ file: fingerprint.slice(0, at),
71
+ keywords: fingerprint.slice(at + 1).split('+').filter((k) => k.length > 0),
72
+ };
73
+ }
74
+ function sameProblem(a, b) {
75
+ if (a === b)
76
+ return true;
77
+ const left = parseFingerprint(a);
78
+ const right = parseFingerprint(b);
79
+ if (left.file !== right.file)
80
+ return false;
81
+ const rightSet = new Set(right.keywords);
82
+ const shared = left.keywords.filter((k) => rightSet.has(k)).length;
83
+ const smaller = Math.min(left.keywords.length, right.keywords.length);
84
+ if (smaller === 0)
85
+ return false;
86
+ return shared >= Math.min(2, smaller) && shared / smaller >= 0.6;
87
+ }
88
+ /**
89
+ * D8 test: does any new finding repeat a previously rejected one? Same file AND a keyword
90
+ * overlap of at least 60 percent of the smaller set (and at least two shared keywords
91
+ * unless the smaller set has one).
92
+ */
93
+ export function findingsIntersect(a, b) {
94
+ return a.some((x) => b.some((y) => sameProblem(x, y)));
95
+ }
96
+ function finishClaim(ctx, id, draft) {
97
+ return {
98
+ id,
99
+ kind: draft.kind,
100
+ raisedBy: draft.raisedBy,
101
+ against: draft.against,
102
+ wpId: ctx.wpId,
103
+ justification: draft.justification,
104
+ status: 'open',
105
+ phase: ctx.phase,
106
+ };
107
+ }
108
+ /** Who a claim of `kind` accuses when the claimant did not say. */
109
+ function defaultAgainst(kind, ctx) {
110
+ switch (kind) {
111
+ case 'security':
112
+ case 'same_issue':
113
+ return ctx.developer;
114
+ case 'missed_by_review':
115
+ case 'incomplete_impl':
116
+ case 'false_positive':
117
+ return ctx.reviewer;
118
+ }
119
+ }
120
+ /** First `max` justifications joined, so one claim can carry several flagged issues. */
121
+ function joinJustifications(parts, max) {
122
+ const kept = parts.filter((p) => p.trim().length > 0).slice(0, max);
123
+ return kept.join(' | ');
124
+ }
125
+ function flaggedIssueClaim(ctx, submission) {
126
+ const flagged = submission.issues.filter((i) => i.missedByReview === true);
127
+ if (flagged.length === 0 || ctx.reviewer === null)
128
+ return null;
129
+ const kind = submission.verdict === 'INCOMPLETE_IMPL' ? 'incomplete_impl' : 'missed_by_review';
130
+ const justification = joinJustifications(flagged.map((i) => i.reviewMissJustification ?? i.description), 3);
131
+ return { kind, raisedBy: submission.identity, against: ctx.reviewer, justification };
132
+ }
133
+ /**
134
+ * Claims implied by a test submission: one per kind, never one per issue (a tester cannot
135
+ * multiply T2 by splitting one defect into five lines). Explicit `claims[]` come first
136
+ * and win over an implied claim of the same kind. Ids are deterministic
137
+ * (`cl-<wp>-t<attempt>-<n>`) so replay and tests need no id generator.
138
+ */
139
+ export function claimsFromTest(ctx, submission) {
140
+ const drafts = [];
141
+ for (const c of submission.claims) {
142
+ const against = c.against ?? defaultAgainst(c.kind, ctx);
143
+ if (against === null || drafts.some((d) => d.kind === c.kind))
144
+ continue;
145
+ drafts.push({
146
+ kind: c.kind,
147
+ raisedBy: submission.identity,
148
+ against,
149
+ justification: c.justification,
150
+ });
151
+ }
152
+ const implied = flaggedIssueClaim(ctx, submission);
153
+ if (implied !== null && !drafts.some((d) => d.kind === implied.kind))
154
+ drafts.push(implied);
155
+ return drafts.map((d, i) => finishClaim(ctx, `cl-${ctx.wpId}-t${submission.attempt}-${i + 1}`, d));
156
+ }
157
+ /** A reviewer's `securityFinding` becomes a security claim against the developer. */
158
+ export function claimsFromReview(ctx, submission) {
159
+ const finding = submission.securityFinding;
160
+ if (finding === undefined || ctx.developer === null)
161
+ return [];
162
+ return [
163
+ finishClaim(ctx, `cl-${ctx.wpId}-r${submission.attempt}-1`, {
164
+ kind: 'security',
165
+ raisedBy: submission.identity,
166
+ against: ctx.developer,
167
+ justification: joinJustifications([finding.description, finding.evidence], 2),
168
+ }),
169
+ ];
170
+ }
171
+ function roleOf(state, agentId) {
172
+ return state.agents[agentId]?.role;
173
+ }
174
+ /** The "you were wrong" event for an agent of `role` (T5 tester, R5 reviewer). */
175
+ function falsePositiveCode(role) {
176
+ if (role === 'TEST')
177
+ return 'T5';
178
+ if (role === 'REV')
179
+ return 'R5';
180
+ return null;
181
+ }
182
+ const UPHELD_CODES = {
183
+ missed_by_review: { against: 'R4', raisedBy: 'T2' },
184
+ incomplete_impl: { against: 'R7', raisedBy: 'T7' },
185
+ security: { against: 'D6', raisedBy: null },
186
+ same_issue: { against: 'D8', raisedBy: null },
187
+ false_positive: { against: null, raisedBy: null },
188
+ };
189
+ function claimEvent(input, code, agentId) {
190
+ const { claim } = input;
191
+ return {
192
+ code,
193
+ agentId,
194
+ at: input.at,
195
+ phase: claim.phase,
196
+ by: `inspector:${input.inspectorId}`,
197
+ reason: `Claim ${claim.id} ${input.decision}: ${eventDef(code).title}`,
198
+ evidence: input.ruling,
199
+ wpId: claim.wpId,
200
+ };
201
+ }
202
+ function upheldEvents(input, notes) {
203
+ const { claim, state } = input;
204
+ const events = [];
205
+ const codes = UPHELD_CODES[claim.kind];
206
+ if (claim.kind === 'false_positive') {
207
+ const code = falsePositiveCode(roleOf(state, claim.against));
208
+ if (code === null)
209
+ notes.push(`No false-positive event exists for ${claim.against}.`);
210
+ else
211
+ events.push(claimEvent(input, code, claim.against));
212
+ return events;
213
+ }
214
+ if (codes.against !== null)
215
+ events.push(claimEvent(input, codes.against, claim.against));
216
+ if (codes.raisedBy !== null)
217
+ events.push(claimEvent(input, codes.raisedBy, claim.raisedBy));
218
+ return events;
219
+ }
220
+ function rejectedEvents(input, notes) {
221
+ const { claim, state } = input;
222
+ const code = falsePositiveCode(roleOf(state, claim.raisedBy));
223
+ if (code === null) {
224
+ notes.push(`Rejected claim ${claim.id}: no penalty event exists for ${claim.raisedBy}.`);
225
+ return [];
226
+ }
227
+ return [claimEvent(input, code, claim.raisedBy)];
228
+ }
229
+ /**
230
+ * Expand an inspector ruling into ledger events (5.6 L2):
231
+ *
232
+ * - upheld missed_by_review: R4 against the reviewer, T2 for the tester.
233
+ * - upheld incomplete_impl: R7 against the reviewer, T7 for the tester.
234
+ * - upheld security: D6 against the developer. upheld same_issue: D8 against the developer.
235
+ * - upheld false_positive: R5 / T5 against the party whose finding was wrong.
236
+ * - ANY rejected claim: T5 (tester) or R5 (reviewer) against the CLAIMANT.
237
+ *
238
+ * Idempotent: a claim that is no longer `open` yields no events, so a repeated
239
+ * `ledger_rule` call cannot pay twice.
240
+ */
241
+ export function ruleClaim(input) {
242
+ const notes = [];
243
+ if (input.claim.status !== 'open') {
244
+ notes.push(`Claim ${input.claim.id} was already ${input.claim.status}; nothing booked.`);
245
+ return { claim: input.claim, events: [], notes };
246
+ }
247
+ const events = input.decision === 'upheld' ? upheldEvents(input, notes) : rejectedEvents(input, notes);
248
+ const claim = { ...input.claim, status: input.decision, rulingText: input.ruling };
249
+ return { claim, events, notes };
250
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Elimination rules and team health (governance-mode 5.8).
3
+ *
4
+ * ASCII ONLY (`governance/` is glyph-scanned).
5
+ *
6
+ * TWO WAYS OUT, BOTH READ `confirmed` ONLY (I-G3):
7
+ *
8
+ * - KILL: `confirmed <= 0`, immediate, no appeal. `apply()` performs it inside the
9
+ * booking that depleted the balance; `killDepleted` is the sweep the engine runs after
10
+ * a batch as a safety net (and for a state loaded from an older checkpoint).
11
+ * - TAIL: a role with at least three active members drops its lowest member when that
12
+ * member is BOTH the unique lowest and below 60 for two phase ends in a row. The first
13
+ * time is a warning. A tie for the lowest place eliminates nobody (a coin toss between
14
+ * equals would be arbitrary) and is reported as `tie_skip`.
15
+ *
16
+ * Provisional escrow never saves or condemns an agent: a clawback can slash an agent's
17
+ * total without touching survival.
18
+ */
19
+ import type { LedgerState, ScoredRole } from '../types.js';
20
+ export { AVOID_ADVICE, FAILURE_ANALYSIS_TOP_PENALTIES, buildFailureAnalysis, negativeEventCodes, } from './failure-analysis.js';
21
+ export interface KillSweepResult {
22
+ state: LedgerState;
23
+ eliminated: string[];
24
+ }
25
+ /** Eliminate every active agent whose `confirmed` is <= 0. Idempotent. */
26
+ export declare function killDepleted(state: LedgerState, at: number): KillSweepResult;
27
+ export interface TailWarning {
28
+ agentId: string;
29
+ role: ScoredRole;
30
+ strikes: number;
31
+ }
32
+ export interface TailTie {
33
+ role: ScoredRole;
34
+ agentIds: string[];
35
+ confirmed: number;
36
+ }
37
+ export interface TailResult {
38
+ state: LedgerState;
39
+ /** First (or later, not yet fatal) strike: the agent is told it is in the tail. */
40
+ warnings: TailWarning[];
41
+ eliminated: string[];
42
+ /** Ties for the lowest place: nobody is eliminated (`tie_skip`). */
43
+ tieSkips: TailTie[];
44
+ notes: string[];
45
+ }
46
+ export interface TailRequest {
47
+ phase: number;
48
+ at: number;
49
+ /** Stored-event seq to stamp on any elimination. */
50
+ seq?: number;
51
+ }
52
+ /**
53
+ * Phase-end tail check for every role. Order is fixed (DEV, REV, TEST) so the result and
54
+ * any seq stamping are reproducible on replay.
55
+ */
56
+ export declare function evaluateTail(state: LedgerState, request: TailRequest): TailResult;
57
+ export type TeamHealthLabel = 'UNKNOWN' | 'HEALTHY' | 'CHECKPOINT_ADVISED';
58
+ export interface TeamHealthStatus {
59
+ /** Mean `confirmed` over active agents; null before any agent exists. */
60
+ health: number | null;
61
+ label: TeamHealthLabel;
62
+ /** Text for the status / settle brief when a coordination checkpoint is advised. */
63
+ advisory: string | null;
64
+ }
65
+ /** `< 70` advises a coordination checkpoint (and a focused interface check at settle). */
66
+ export declare function teamHealthStatus(state: LedgerState): TeamHealthStatus;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Elimination rules and team health (governance-mode 5.8).
3
+ *
4
+ * ASCII ONLY (`governance/` is glyph-scanned).
5
+ *
6
+ * TWO WAYS OUT, BOTH READ `confirmed` ONLY (I-G3):
7
+ *
8
+ * - KILL: `confirmed <= 0`, immediate, no appeal. `apply()` performs it inside the
9
+ * booking that depleted the balance; `killDepleted` is the sweep the engine runs after
10
+ * a batch as a safety net (and for a state loaded from an older checkpoint).
11
+ * - TAIL: a role with at least three active members drops its lowest member when that
12
+ * member is BOTH the unique lowest and below 60 for two phase ends in a row. The first
13
+ * time is a warning. A tie for the lowest place eliminates nobody (a coin toss between
14
+ * equals would be arbitrary) and is reported as `tie_skip`.
15
+ *
16
+ * Provisional escrow never saves or condemns an agent: a clawback can slash an agent's
17
+ * total without touching survival.
18
+ */
19
+ import { GOV_LIMITS } from '../limits.js';
20
+ import { eliminate } from './ledger.js';
21
+ import { activeAgents, teamHealth } from './ledger-state.js';
22
+ export { AVOID_ADVICE, FAILURE_ANALYSIS_TOP_PENALTIES, buildFailureAnalysis, negativeEventCodes, } from './failure-analysis.js';
23
+ const SCORED_ROLES = ['DEV', 'REV', 'TEST'];
24
+ /** Eliminate every active agent whose `confirmed` is <= 0. Idempotent. */
25
+ export function killDepleted(state, at) {
26
+ let current = state;
27
+ const eliminated = [];
28
+ for (const agent of activeAgents(state)) {
29
+ if (agent.confirmed > 0)
30
+ continue;
31
+ const outcome = eliminate(current, { agentId: agent.id, reason: 'depleted', at });
32
+ current = outcome.state;
33
+ if (outcome.eliminated)
34
+ eliminated.push(agent.id);
35
+ }
36
+ return { state: current, eliminated };
37
+ }
38
+ function withStrikes(state, agent, strikes) {
39
+ if (agent.tailStrikes === strikes)
40
+ return state;
41
+ return { ...state, agents: { ...state.agents, [agent.id]: { ...agent, tailStrikes: strikes } } };
42
+ }
43
+ /** Clear strikes: the streak must be CONSECUTIVE phase ends. */
44
+ function resetStrikes(state, agents) {
45
+ return agents.reduce((s, a) => withStrikes(s, s.agents[a.id], 0), state);
46
+ }
47
+ function evaluateRole(state, role, request, out) {
48
+ const members = activeAgents(state, role);
49
+ if (members.length < GOV_LIMITS.tailEliminationMinRoleSize) {
50
+ return resetStrikes(state, members);
51
+ }
52
+ const lowest = Math.min(...members.map((m) => m.confirmed));
53
+ if (lowest >= GOV_LIMITS.tailEliminationThreshold)
54
+ return resetStrikes(state, members);
55
+ const tied = members.filter((m) => m.confirmed === lowest);
56
+ if (tied.length > 1) {
57
+ out.tieSkips.push({ role, agentIds: tied.map((m) => m.id), confirmed: lowest });
58
+ out.notes.push(`tie_skip: ${role} ${tied.map((m) => m.id).join(', ')} tie at ${lowest}.`);
59
+ return resetStrikes(state, members);
60
+ }
61
+ const target = tied[0];
62
+ let current = resetStrikes(state, members.filter((m) => m.id !== target.id));
63
+ const strikes = target.tailStrikes + 1;
64
+ current = withStrikes(current, current.agents[target.id], strikes);
65
+ if (strikes < 2) {
66
+ out.warnings.push({ agentId: target.id, role, strikes });
67
+ out.notes.push(`Tail warning: ${target.id} is lowest in ${role} at ${lowest}.`);
68
+ return current;
69
+ }
70
+ const outcome = eliminate(current, {
71
+ agentId: target.id,
72
+ reason: 'tail',
73
+ at: request.at,
74
+ ...(request.seq === undefined ? {} : { seq: request.seq }),
75
+ });
76
+ if (outcome.eliminated)
77
+ out.eliminated.push(target.id);
78
+ return outcome.state;
79
+ }
80
+ /**
81
+ * Phase-end tail check for every role. Order is fixed (DEV, REV, TEST) so the result and
82
+ * any seq stamping are reproducible on replay.
83
+ */
84
+ export function evaluateTail(state, request) {
85
+ const out = { state, warnings: [], eliminated: [], tieSkips: [], notes: [] };
86
+ let current = state;
87
+ for (const role of SCORED_ROLES)
88
+ current = evaluateRole(current, role, request, out);
89
+ out.state = current;
90
+ return out;
91
+ }
92
+ /** `< 70` advises a coordination checkpoint (and a focused interface check at settle). */
93
+ export function teamHealthStatus(state) {
94
+ const health = teamHealth(state);
95
+ if (health === null)
96
+ return { health, label: 'UNKNOWN', advisory: null };
97
+ if (health >= GOV_LIMITS.healthWarnThreshold)
98
+ return { health, label: 'HEALTHY', advisory: null };
99
+ return {
100
+ health,
101
+ label: 'CHECKPOINT_ADVISED',
102
+ advisory: `Team health ${health.toFixed(1)} is below ${GOV_LIMITS.healthWarnThreshold}: ` +
103
+ 'a coordination checkpoint is advised; the next settle checks interface conflicts.',
104
+ };
105
+ }