@ai-sdlc/orchestrator 0.10.0 → 0.13.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 (156) hide show
  1. package/dist/adapters.d.ts +42 -3
  2. package/dist/adapters.js +133 -3
  3. package/dist/admission-composite.d.ts +112 -1
  4. package/dist/admission-composite.js +85 -4
  5. package/dist/admission-enrichment.d.ts +12 -3
  6. package/dist/admission-enrichment.js +64 -13
  7. package/dist/admission-score.d.ts +30 -0
  8. package/dist/admission-score.js +4 -1
  9. package/dist/backlog-adapter.d.ts +45 -0
  10. package/dist/backlog-adapter.js +145 -1
  11. package/dist/calibration.d.ts +81 -0
  12. package/dist/calibration.js +76 -0
  13. package/dist/cli/commands/init-features.d.ts +301 -2
  14. package/dist/cli/commands/init-features.js +634 -7
  15. package/dist/cli/commands/init-templates.d.ts +198 -1
  16. package/dist/cli/commands/init-templates.js +943 -1
  17. package/dist/cli/commands/init.d.ts +45 -0
  18. package/dist/cli/commands/init.js +147 -5
  19. package/dist/cli/commands/run.js +9 -1
  20. package/dist/cli/index.d.ts +11 -0
  21. package/dist/cli/index.js +42 -8
  22. package/dist/compliance/composer.d.ts +79 -0
  23. package/dist/compliance/composer.js +258 -0
  24. package/dist/compliance/errors.d.ts +64 -0
  25. package/dist/compliance/errors.js +85 -0
  26. package/dist/compliance/loader.d.ts +52 -0
  27. package/dist/compliance/loader.js +124 -0
  28. package/dist/compliance/types.d.ts +184 -0
  29. package/dist/compliance/types.js +41 -0
  30. package/dist/compliance-clearance.d.ts +269 -0
  31. package/dist/compliance-clearance.js +269 -0
  32. package/dist/config.js +17 -0
  33. package/dist/cost-tracker.d.ts +22 -0
  34. package/dist/cost-tracker.js +41 -0
  35. package/dist/database/adapters/external.js +5 -1
  36. package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
  37. package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
  38. package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
  39. package/dist/embedding/consumers/tessellation-drift.js +76 -0
  40. package/dist/embedding/cross-provider.d.ts +78 -0
  41. package/dist/embedding/cross-provider.js +75 -0
  42. package/dist/embedding/deprecation.d.ts +151 -0
  43. package/dist/embedding/deprecation.js +229 -0
  44. package/dist/embedding/errors.d.ts +90 -0
  45. package/dist/embedding/errors.js +150 -0
  46. package/dist/embedding/index.d.ts +29 -0
  47. package/dist/embedding/index.js +24 -0
  48. package/dist/embedding/pipeline-load.d.ts +146 -0
  49. package/dist/embedding/pipeline-load.js +178 -0
  50. package/dist/embedding/registry.d.ts +45 -0
  51. package/dist/embedding/registry.js +61 -0
  52. package/dist/embedding/stale-vector.d.ts +110 -0
  53. package/dist/embedding/stale-vector.js +92 -0
  54. package/dist/embedding/storage/index.d.ts +51 -0
  55. package/dist/embedding/storage/index.js +43 -0
  56. package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
  57. package/dist/embedding/storage/jsonl-backend.js +332 -0
  58. package/dist/embedding/storage/types.d.ts +135 -0
  59. package/dist/embedding/storage/types.js +13 -0
  60. package/dist/embedding/types.d.ts +180 -0
  61. package/dist/embedding/types.js +10 -0
  62. package/dist/execute.d.ts +29 -2
  63. package/dist/execute.js +171 -30
  64. package/dist/index.d.ts +14 -2
  65. package/dist/index.js +19 -2
  66. package/dist/journey/inheritance-validator.d.ts +396 -0
  67. package/dist/journey/inheritance-validator.js +370 -0
  68. package/dist/journey/state-id-drift-rule.d.ts +137 -0
  69. package/dist/journey/state-id-drift-rule.js +245 -0
  70. package/dist/journey-sa2-router.d.ts +395 -0
  71. package/dist/journey-sa2-router.js +308 -0
  72. package/dist/runners/review-agent.js +6 -2
  73. package/dist/runners/runner-registry.d.ts +36 -0
  74. package/dist/runners/runner-registry.js +90 -0
  75. package/dist/runtime/attestations.d.ts +173 -13
  76. package/dist/runtime/attestations.js +252 -40
  77. package/dist/runtime/index.d.ts +1 -1
  78. package/dist/runtime/index.js +1 -1
  79. package/dist/sa-scoring/layer3-llm.js +6 -1
  80. package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
  81. package/dist/sa-scoring/revision-proposal-config.js +198 -0
  82. package/dist/sa-scoring/revision-proposal.d.ts +285 -0
  83. package/dist/sa-scoring/revision-proposal.js +417 -0
  84. package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
  85. package/dist/signal-ingestion/adapters/community-thread.js +55 -0
  86. package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
  87. package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
  88. package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
  89. package/dist/signal-ingestion/adapters/manual.js +112 -0
  90. package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
  91. package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
  92. package/dist/signal-ingestion/classifier.d.ts +205 -0
  93. package/dist/signal-ingestion/classifier.js +494 -0
  94. package/dist/signal-ingestion/clustering-types.d.ts +36 -0
  95. package/dist/signal-ingestion/clustering-types.js +14 -0
  96. package/dist/signal-ingestion/clustering.d.ts +200 -0
  97. package/dist/signal-ingestion/clustering.js +413 -0
  98. package/dist/signal-ingestion/config.d.ts +351 -0
  99. package/dist/signal-ingestion/config.js +587 -0
  100. package/dist/signal-ingestion/d1.d.ts +252 -0
  101. package/dist/signal-ingestion/d1.js +235 -0
  102. package/dist/signal-ingestion/errors.d.ts +73 -0
  103. package/dist/signal-ingestion/errors.js +108 -0
  104. package/dist/signal-ingestion/governance-events.d.ts +181 -0
  105. package/dist/signal-ingestion/governance-events.js +189 -0
  106. package/dist/signal-ingestion/index.d.ts +35 -0
  107. package/dist/signal-ingestion/index.js +53 -0
  108. package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
  109. package/dist/signal-ingestion/manual-share-metric.js +106 -0
  110. package/dist/signal-ingestion/registry.d.ts +40 -0
  111. package/dist/signal-ingestion/registry.js +137 -0
  112. package/dist/signal-ingestion/residency.d.ts +227 -0
  113. package/dist/signal-ingestion/residency.js +238 -0
  114. package/dist/signal-ingestion/significance.d.ts +554 -0
  115. package/dist/signal-ingestion/significance.js +555 -0
  116. package/dist/signal-ingestion/types.d.ts +191 -0
  117. package/dist/signal-ingestion/types.js +8 -0
  118. package/dist/substrate/drift-composition.d.ts +270 -0
  119. package/dist/substrate/drift-composition.js +306 -0
  120. package/dist/substrate/drift-tui-surface.d.ts +61 -0
  121. package/dist/substrate/drift-tui-surface.js +102 -0
  122. package/dist/substrate/identity-class.d.ts +176 -0
  123. package/dist/substrate/identity-class.js +201 -0
  124. package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
  125. package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
  126. package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
  127. package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
  128. package/dist/tessellation/rule-registry.d.ts +269 -0
  129. package/dist/tessellation/rule-registry.js +92 -0
  130. package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
  131. package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
  132. package/dist/tessellation-admission.d.ts +162 -0
  133. package/dist/tessellation-admission.js +146 -0
  134. package/dist/tessellation-drift.d.ts +246 -0
  135. package/dist/tessellation-drift.js +250 -0
  136. package/dist/validate-config.js +13 -0
  137. package/dist/validate-issue.js +2 -2
  138. package/dist/variant/cardinality-activation.d.ts +126 -0
  139. package/dist/variant/cardinality-activation.js +101 -0
  140. package/dist/variant/deprecation-lifecycle.d.ts +184 -0
  141. package/dist/variant/deprecation-lifecycle.js +208 -0
  142. package/dist/variant/drift-extension.d.ts +136 -0
  143. package/dist/variant/drift-extension.js +164 -0
  144. package/dist/variant/engineering-review.d.ts +185 -0
  145. package/dist/variant/engineering-review.js +142 -0
  146. package/dist/variant/index.d.ts +32 -0
  147. package/dist/variant/index.js +32 -0
  148. package/dist/variant/inheritance-validator.d.ts +165 -0
  149. package/dist/variant/inheritance-validator.js +139 -0
  150. package/dist/variant/internal-adopter/index.d.ts +11 -0
  151. package/dist/variant/internal-adopter/index.js +10 -0
  152. package/dist/variant/internal-adopter/products.d.ts +156 -0
  153. package/dist/variant/internal-adopter/products.js +366 -0
  154. package/dist/variant-admission.d.ts +316 -0
  155. package/dist/variant-admission.js +247 -0
  156. package/package.json +10 -8
@@ -0,0 +1,308 @@
1
+ /**
2
+ * RFC-0018 Phase 2 — In-Soul Journey Pattern admission scorer composition.
3
+ *
4
+ * Implements the journey-scope routing algorithm described in RFC-0018 §5.4
5
+ * and §10.1 (OQ-4 resolution):
6
+ *
7
+ * resolveTargetedJourneys(w) = set of (soulId, [variantId,] journeyId) triples
8
+ * declared on the work item via `targetedJourneys[]` (URI shapes
9
+ * `<soul-id>/<journey-id>` or `<soul-id>/<variant-id>/<journey-id>`).
10
+ *
11
+ * If no `targetedJourneys` declared:
12
+ * Scoring proceeds at soul/variant scope (backward-compatible — unchanged
13
+ * from RFC-0009 + RFC-0017 baseline).
14
+ *
15
+ * Else if |targeted| == 1 (single-journey):
16
+ * Sα₂(w) = scoreSα₂(journey.designImperatives UNION variant's UNION soul's;
17
+ * most-specific wins: journey > variant > soul)
18
+ * Cκ(w) = boosted when journey.successMetrics.completion-rate < alertBelow
19
+ * Eρ₅(w) = elevated when journey.accessibility.wcagLevel > soul/variant default
20
+ *
21
+ * Else (multi-journey):
22
+ * Per-journey scores aggregated via `crossJourneyAggregation` config
23
+ * (default `min` per RFC-0018 §5.4 + RFC-0017 OQ-4 cross-variant pattern).
24
+ *
25
+ * ### Completion-criteria closed enum (OQ-4 resolution)
26
+ *
27
+ * v1 allows only: `terminal-success-state` | `all-states-reached`
28
+ * `custom-predicate` is rejected at schema validation time.
29
+ * Future activation via `Decision: journey-custom-predicate-activation-request`
30
+ * (Stage A counter, auto-promote at ≥2 distinct adopter requests).
31
+ * Future language: CEL (Google Common Expression Language) per OQ-4 resolution.
32
+ *
33
+ * ### Backward compatibility
34
+ *
35
+ * Work items without `targetedJourneys` score against soul / variant (existing
36
+ * RFC-0017 behavior preserved). Soul DIDs without `journeys[]` behave identically.
37
+ *
38
+ * @see spec/rfcs/RFC-0018-in-soul-journey-pattern.md §5.4 + §10 + §10.1
39
+ * @see orchestrator/src/variant-admission.ts — sibling RFC-0017 Phase 2 router
40
+ * @see orchestrator/src/compliance-clearance.ts — Eρ₅ base scorer
41
+ */
42
+ /**
43
+ * The set of valid v1 completion-criteria kind values.
44
+ * Used by `validateCompletionCriteriaKind` for closed-enum rejection.
45
+ */
46
+ export const COMPLETION_CRITERIA_V1_KINDS = new Set([
47
+ 'terminal-success-state',
48
+ 'all-states-reached',
49
+ ]);
50
+ /**
51
+ * Decision kind for the `custom-predicate` activation request counter.
52
+ * Stage A counter per RFC-0035 G0 non-blocking contract.
53
+ * Auto-promotes at ≥2 distinct adopter requests (RFC-0018 OQ-4 resolution).
54
+ */
55
+ export const JOURNEY_CUSTOM_PREDICATE_DECISION_KIND = 'journey-custom-predicate-activation-request';
56
+ /**
57
+ * Validate a completion-criteria `kind` value against the v1 closed enum.
58
+ *
59
+ * Returns `{ valid: true }` for `terminal-success-state` and `all-states-reached`.
60
+ * Returns `{ valid: false, rejectedKind, decisionKind }` for `custom-predicate`
61
+ * (or any other unrecognized value). The caller is responsible for emitting the
62
+ * Decision event to the RFC-0035 catalog.
63
+ */
64
+ export function validateCompletionCriteriaKind(kind) {
65
+ if (COMPLETION_CRITERIA_V1_KINDS.has(kind)) {
66
+ return { valid: true };
67
+ }
68
+ const result = {
69
+ valid: false,
70
+ rejectedKind: kind,
71
+ };
72
+ // Wire the Decision counter for custom-predicate specifically (OQ-4 auto-promote mechanism)
73
+ if (kind === 'custom-predicate') {
74
+ result.decisionKind = JOURNEY_CUSTOM_PREDICATE_DECISION_KIND;
75
+ }
76
+ return result;
77
+ }
78
+ /**
79
+ * Increment the Stage A counter for a `journey-custom-predicate-activation-request`.
80
+ *
81
+ * @param counter - The counter to increment (mutated in-place).
82
+ * @param adopterId - Adopter ID submitting the request (for deduplication).
83
+ * Use `'__unknown__'` when adopter identity is unavailable.
84
+ */
85
+ export function incrementDecisionCounter(counter, adopterId) {
86
+ counter.totalRequests++;
87
+ counter.distinctAdopterIds.add(adopterId);
88
+ counter.shouldAutoPromote = counter.distinctAdopterIds.size >= 2;
89
+ }
90
+ /**
91
+ * Create a fresh Stage A counter for `journey-custom-predicate-activation-request`.
92
+ */
93
+ export function createCustomPredicateDecisionCounter() {
94
+ return {
95
+ decisionKind: JOURNEY_CUSTOM_PREDICATE_DECISION_KIND,
96
+ distinctAdopterIds: new Set(),
97
+ totalRequests: 0,
98
+ shouldAutoPromote: false,
99
+ };
100
+ }
101
+ /**
102
+ * Ordinal index for WCAG levels — higher = stricter.
103
+ * `A` < `AA` < `AAA`.
104
+ */
105
+ const WCAG_LEVEL_ORDER = {
106
+ A: 0,
107
+ AA: 1,
108
+ AAA: 2,
109
+ };
110
+ /**
111
+ * Returns true when `journeyLevel` is strictly ABOVE `soulDefault`.
112
+ * Journeys MAY raise the WCAG level above the parent; they MAY NOT lower it.
113
+ */
114
+ export function isWcagElevated(journeyLevel, soulDefault) {
115
+ return WCAG_LEVEL_ORDER[journeyLevel] > WCAG_LEVEL_ORDER[soulDefault];
116
+ }
117
+ // ── URI parsing ──────────────────────────────────────────────────────────
118
+ const SLUG_RE = /^[a-z][a-z0-9-]*$/;
119
+ /**
120
+ * Parse a targeted-journey reference.
121
+ *
122
+ * Accepts two URI forms per RFC-0018 §6.1:
123
+ *
124
+ * 1. `<soul-id>/<journey-id>` — soul-scoped journey
125
+ * 2. `<soul-id>/<variant-id>/<journey-id>` — variant-scoped journey
126
+ *
127
+ * All slug segments must match `^[a-z][a-z0-9-]*$`.
128
+ * Returns `undefined` for malformed input (silent skip; schema-side validator
129
+ * is Phase 1's concern per RFC-0018 §6.1 pattern).
130
+ */
131
+ export function parseTargetedJourneyRef(raw) {
132
+ const parts = raw.split('/');
133
+ if (parts.length === 2) {
134
+ const [soulId, journeyId] = parts;
135
+ if (SLUG_RE.test(soulId) && SLUG_RE.test(journeyId)) {
136
+ return { soulId, journeyId, raw };
137
+ }
138
+ }
139
+ else if (parts.length === 3) {
140
+ const [soulId, variantId, journeyId] = parts;
141
+ if (SLUG_RE.test(soulId) && SLUG_RE.test(variantId) && SLUG_RE.test(journeyId)) {
142
+ return { soulId, variantId, journeyId, raw };
143
+ }
144
+ }
145
+ return undefined;
146
+ }
147
+ // ── resolveTargetedJourneys ──────────────────────────────────────────────
148
+ /**
149
+ * Resolve the set of targeted (soulId, [variantId,] journeyId) triples for a work item.
150
+ *
151
+ * Algorithm:
152
+ * 1. Find the work item by case-insensitive ID match in `workItemTargeting`.
153
+ * 2. Parse each entry of `targetedJourneys[]` via `parseTargetedJourneyRef`.
154
+ * 3. Filter parsed refs against `journeysBySoul` — a (soulId, journeyId) pair
155
+ * only survives if the soul exists AND the journeyId is declared on it.
156
+ * 4. Return the validated intersection (empty = backward-compat soul/variant scope).
157
+ */
158
+ export function resolveTargetedJourneys(workItemId, journeyCtx) {
159
+ if (!journeyCtx || !journeyCtx.workItemTargeting || journeyCtx.workItemTargeting.length === 0) {
160
+ return [];
161
+ }
162
+ const normalizedId = workItemId.toLowerCase();
163
+ const entry = journeyCtx.workItemTargeting.find((e) => e.id.toLowerCase() === normalizedId);
164
+ if (!entry || !entry.targetedJourneys || entry.targetedJourneys.length === 0) {
165
+ return [];
166
+ }
167
+ const out = [];
168
+ for (const raw of entry.targetedJourneys) {
169
+ const parsed = parseTargetedJourneyRef(raw);
170
+ if (!parsed)
171
+ continue;
172
+ const journeys = journeyCtx.journeysBySoul[parsed.soulId];
173
+ if (!journeys)
174
+ continue;
175
+ if (!journeys.some((j) => j.id === parsed.journeyId))
176
+ continue;
177
+ out.push(parsed);
178
+ }
179
+ return out;
180
+ }
181
+ // ── Cross-journey aggregation ────────────────────────────────────────────
182
+ /**
183
+ * Apply a per-Soul `crossJourneyAggregation` rule over per-journey scores.
184
+ * Mirrors `applyCrossVariantRule` in `variant-admission.ts` but for journey scope.
185
+ *
186
+ * @param values - Per-journey score samples (one per targeted journey).
187
+ * @param rule - The aggregation rule (defaults to `min` per RFC-0018 §5.4 + OQ-4).
188
+ * @param fallback - Returned when `values` is empty.
189
+ */
190
+ export function applyCrossJourneyRule(values, rule, fallback = 0.5) {
191
+ if (values.length === 0)
192
+ return fallback;
193
+ switch (rule ?? 'min') {
194
+ case 'min':
195
+ return Math.min(...values);
196
+ case 'max':
197
+ return Math.max(...values);
198
+ case 'mean':
199
+ return values.reduce((sum, v) => sum + v, 0) / values.length;
200
+ }
201
+ }
202
+ /**
203
+ * Compute the journey-scope-refined Sα₂ + Cκ + Eρ₅ for a work item.
204
+ *
205
+ * This runs AFTER variant-scope resolution: the caller has already routed the
206
+ * work item to its target Soul(s)/Variant(s) and obtained the soul/variant
207
+ * Sα₂ (the `fallbackSa2` argument) and Cκ (the `fallbackCk` argument).
208
+ * Journey routing refines those values when the work item declares
209
+ * `targetedJourneys` of one of the affected Souls (RFC-0018 §5.4).
210
+ *
211
+ * **Sα₂ Vibe Coherence** — journey's `designImperatives` UNION variant's UNION
212
+ * soul's; conflict resolution: most-specific wins (journey > variant > soul).
213
+ * The pre-computed `JourneyScores.sa2` in the context encodes this resolution.
214
+ *
215
+ * **Cκ Capability Coverage** — boosted when `completion-rate < alertBelow`.
216
+ * The pre-computed `JourneyScores.ck` encodes the boost; callers supply live
217
+ * metric snapshots to the loader that builds `journeyScores`.
218
+ *
219
+ * **Eρ₅ Compliance Clearance** — elevated when journey WCAG > soul-default.
220
+ * The pre-computed `JourneyScores.er5Elevated` flag captures this; the caller
221
+ * applies Eρ₅ gating at the journey's elevated level when `er5Elevated = true`.
222
+ *
223
+ * **Cross-journey aggregation** (work touches multiple journeys) — same `min`
224
+ * as RFC-0018 §5.4 / RFC-0009 §7.2 / RFC-0017 OQ-4 by default. Per-Soul
225
+ * override via `journeyConfig.crossJourneyAggregation`.
226
+ *
227
+ * @param workItemId - The canonical work item ID.
228
+ * @param fallbackSa2 - Soul/variant Sα₂ to use when no journey routing applies.
229
+ * @param fallbackCk - Soul/variant Cκ to use when no journey routing applies.
230
+ * @param fallbackEr5Elevated - Soul/variant Eρ₅ elevation flag (usually false).
231
+ * @param journeyCtx - Journey-scope context; undefined → backward-compat passthrough.
232
+ */
233
+ export function computeJourneyScopedScores(workItemId, fallbackSa2, fallbackCk, fallbackEr5Elevated, journeyCtx) {
234
+ const targetedJourneys = resolveTargetedJourneys(workItemId, journeyCtx);
235
+ // ── Backward-compat: no targeted journeys → soul/variant scope passthrough ──
236
+ if (targetedJourneys.length === 0 || !journeyCtx) {
237
+ return {
238
+ sa2: fallbackSa2,
239
+ ck: fallbackCk,
240
+ er5Elevated: fallbackEr5Elevated,
241
+ routingPath: 'no-journey-routing',
242
+ targetedJourneys: [],
243
+ };
244
+ }
245
+ // ── Single-journey fast path ──
246
+ if (targetedJourneys.length === 1) {
247
+ const ref = targetedJourneys[0];
248
+ const scores = journeyCtx.journeyScores[ref.soulId]?.[ref.journeyId];
249
+ return {
250
+ sa2: scores?.sa2 ?? fallbackSa2,
251
+ ck: scores?.ck ?? fallbackCk,
252
+ er5Elevated: scores?.er5Elevated ?? fallbackEr5Elevated,
253
+ routingPath: 'single-journey',
254
+ targetedJourneys,
255
+ };
256
+ }
257
+ // ── Multi-journey: aggregate per-Soul, then aggregate cross-Soul ──
258
+ // Per-Soul aggregation uses the Soul's `crossJourneyAggregation` config
259
+ // (default `min`). When journeys span multiple souls (rare), the cross-Soul
260
+ // layer also uses `min` as the safest default (RFC-0009 §7.2).
261
+ const bySoul = new Map();
262
+ for (const ref of targetedJourneys) {
263
+ const bucket = bySoul.get(ref.soulId);
264
+ if (bucket)
265
+ bucket.push(ref);
266
+ else
267
+ bySoul.set(ref.soulId, [ref]);
268
+ }
269
+ const perSoulSa2 = [];
270
+ const perSoulCk = [];
271
+ const perSoulEr5Elevated = [];
272
+ let firstAggregationRule;
273
+ for (const [soulId, refs] of bySoul) {
274
+ const cfg = journeyCtx.configBySoul?.[soulId];
275
+ const rule = cfg?.crossJourneyAggregation ?? 'min';
276
+ if (firstAggregationRule === undefined)
277
+ firstAggregationRule = rule;
278
+ const sa2Samples = [];
279
+ const ckSamples = [];
280
+ const er5Samples = [];
281
+ for (const ref of refs) {
282
+ const scores = journeyCtx.journeyScores[soulId]?.[ref.journeyId];
283
+ sa2Samples.push(scores?.sa2 ?? fallbackSa2);
284
+ ckSamples.push(scores?.ck ?? fallbackCk);
285
+ er5Samples.push(scores?.er5Elevated ?? fallbackEr5Elevated);
286
+ }
287
+ perSoulSa2.push(applyCrossJourneyRule(sa2Samples, rule, fallbackSa2));
288
+ perSoulCk.push(applyCrossJourneyRule(ckSamples, rule, fallbackCk));
289
+ // Eρ₅ elevation: `true` wins if ANY journey requires elevation
290
+ // (conservative — if at least one journey has elevated WCAG, the
291
+ // work item is evaluated at that stricter level).
292
+ perSoulEr5Elevated.push(er5Samples.some(Boolean));
293
+ }
294
+ // Cross-Soul layering: `min` between Souls for numeric scores (RFC-0009 §7.2).
295
+ // Eρ₅ elevation: `true` if any Soul has elevated WCAG (conservative/safe).
296
+ const sa2 = perSoulSa2.length === 1 ? perSoulSa2[0] : Math.min(...perSoulSa2);
297
+ const ck = perSoulCk.length === 1 ? perSoulCk[0] : Math.min(...perSoulCk);
298
+ const er5Elevated = perSoulEr5Elevated.some(Boolean);
299
+ return {
300
+ sa2,
301
+ ck,
302
+ er5Elevated,
303
+ routingPath: 'multi-journey',
304
+ targetedJourneys,
305
+ aggregationRule: firstAggregationRule ?? 'min',
306
+ };
307
+ }
308
+ //# sourceMappingURL=journey-sa2-router.js.map
@@ -291,8 +291,12 @@ export class ReviewAgentRunner {
291
291
  }
292
292
  }
293
293
  parseVerdict(text) {
294
- // Strip markdown fences if the model wraps the JSON
295
- const cleaned = text.replace(/^```(?:json)?\s*/m, '').replace(/\s*```$/m, '');
294
+ // Strip markdown fences if the model wraps the JSON. Horizontal-whitespace
295
+ // classes ([ \t\r]) instead of \s so the trailing-whitespace group can't
296
+ // overlap the \n the anchors already consume — avoids polynomial
297
+ // backtracking on adversarial model output (CodeQL js/polynomial-redos).
298
+ // JSON.parse tolerates the leading/trailing newline left behind.
299
+ const cleaned = text.replace(/^```(?:json)?[ \t\r]*/m, '').replace(/[ \t\r]*```$/m, '');
296
300
  try {
297
301
  const parsed = JSON.parse(cleaned);
298
302
  const rawFindings = Array.isArray(parsed.findings)
@@ -38,6 +38,17 @@ export declare class RunnerRegistry {
38
38
  * Check if a runner is registered and available.
39
39
  */
40
40
  has(name: string): boolean;
41
+ /**
42
+ * Load and register a runner from an external plugin module.
43
+ *
44
+ * The module must export a default export or a named `runner` export that
45
+ * satisfies the `AgentRunner` interface (i.e. has a `run(ctx)` method).
46
+ *
47
+ * @param pluginPath - Absolute or resolvable path to the plugin module (e.g. `/path/to/runner.mjs`).
48
+ * @param name - Registry name for the loaded runner (defaults to the basename of the path).
49
+ * @throws Error when the module cannot be imported or does not export a valid AgentRunner.
50
+ */
51
+ loadFromPlugin(pluginPath: string, name?: string): Promise<string>;
41
52
  /**
42
53
  * Auto-discover runners from environment variables and register them.
43
54
  */
@@ -47,4 +58,29 @@ export declare class RunnerRegistry {
47
58
  * Create a runner registry with auto-discovery.
48
59
  */
49
60
  export declare function createRunnerRegistry(env?: Record<string, string | undefined>): RunnerRegistry;
61
+ /**
62
+ * Resolve the agent runner to use, applying the full precedence chain:
63
+ *
64
+ * 1. `injectedRunner` — programmatic override (options.runner from caller / tests)
65
+ * 2. `runnerName` — explicit `--runner <name>` flag (must already be registered after discoverFromEnv)
66
+ * 3. `AI_SDLC_RUNNER_PLUGIN` env — path to a dynamic plugin module (loaded + registered)
67
+ * 4. ClaudeCodeRunner (hard-coded default)
68
+ *
69
+ * IMPORTANT — env-discovered runners do NOT auto-win (AISDLC-529 code review). They are
70
+ * registered by `discoverFromEnv()` so they are *selectable by name* via `--runner <name>`,
71
+ * but the mere PRESENCE of an ambient env var (ANTHROPIC_API_KEY, OPENAI_API_KEY, GH_TOKEN,
72
+ * etc. — commonly set for unrelated tools) must NOT silently switch the runner. Before this
73
+ * seam existed the orchestrator always used ClaudeCodeRunner; preserving that as the default
74
+ * (absent an explicit selector) avoids a breaking, surprising change for existing adopters.
75
+ *
76
+ * This function is async because step 3 may dynamically import a module.
77
+ *
78
+ * @throws Error when `runnerName` is provided but not registered in the registry.
79
+ * @throws Error when `AI_SDLC_RUNNER_PLUGIN` points to an invalid module.
80
+ */
81
+ export declare function resolveRunner(registry: RunnerRegistry, opts?: {
82
+ injectedRunner?: AgentRunner;
83
+ runnerName?: string;
84
+ env?: Record<string, string | undefined>;
85
+ }): Promise<AgentRunner>;
50
86
  //# sourceMappingURL=runner-registry.d.ts.map
@@ -58,6 +58,45 @@ export class RunnerRegistry {
58
58
  const entry = this.runners.get(name);
59
59
  return entry?.available ?? false;
60
60
  }
61
+ /**
62
+ * Load and register a runner from an external plugin module.
63
+ *
64
+ * The module must export a default export or a named `runner` export that
65
+ * satisfies the `AgentRunner` interface (i.e. has a `run(ctx)` method).
66
+ *
67
+ * @param pluginPath - Absolute or resolvable path to the plugin module (e.g. `/path/to/runner.mjs`).
68
+ * @param name - Registry name for the loaded runner (defaults to the basename of the path).
69
+ * @throws Error when the module cannot be imported or does not export a valid AgentRunner.
70
+ */
71
+ async loadFromPlugin(pluginPath, name) {
72
+ let mod;
73
+ try {
74
+ mod = await import(pluginPath);
75
+ }
76
+ catch (err) {
77
+ throw new Error(`AI_SDLC_RUNNER_PLUGIN: failed to import plugin module "${pluginPath}": ${err instanceof Error ? err.message : String(err)}.\n` +
78
+ `Ensure the path is correct and the module is a valid ESM/CJS module.`);
79
+ }
80
+ // Accept default export or named 'runner' export
81
+ const exported = mod.default ?? mod.runner;
82
+ if (!exported || typeof exported.run !== 'function') {
83
+ throw new Error(`AI_SDLC_RUNNER_PLUGIN: plugin module "${pluginPath}" does not export a valid AgentRunner.\n` +
84
+ `Expected a default export (or named 'runner' export) with a \`run(ctx: AgentContext): Promise<AgentResult>\` method.\n` +
85
+ `Got: ${JSON.stringify(Object.keys(mod))}`);
86
+ }
87
+ const runnerName = name ??
88
+ pluginPath
89
+ .split('/')
90
+ .pop()
91
+ .replace(/\.(m|c)?[jt]s$/, '');
92
+ this.runners.set(runnerName, {
93
+ name: runnerName,
94
+ runner: exported,
95
+ available: true,
96
+ source: 'manual',
97
+ });
98
+ return runnerName;
99
+ }
61
100
  /**
62
101
  * Auto-discover runners from environment variables and register them.
63
102
  */
@@ -163,4 +202,55 @@ export function createRunnerRegistry(env) {
163
202
  registry.discoverFromEnv(env);
164
203
  return registry;
165
204
  }
205
+ /**
206
+ * Resolve the agent runner to use, applying the full precedence chain:
207
+ *
208
+ * 1. `injectedRunner` — programmatic override (options.runner from caller / tests)
209
+ * 2. `runnerName` — explicit `--runner <name>` flag (must already be registered after discoverFromEnv)
210
+ * 3. `AI_SDLC_RUNNER_PLUGIN` env — path to a dynamic plugin module (loaded + registered)
211
+ * 4. ClaudeCodeRunner (hard-coded default)
212
+ *
213
+ * IMPORTANT — env-discovered runners do NOT auto-win (AISDLC-529 code review). They are
214
+ * registered by `discoverFromEnv()` so they are *selectable by name* via `--runner <name>`,
215
+ * but the mere PRESENCE of an ambient env var (ANTHROPIC_API_KEY, OPENAI_API_KEY, GH_TOKEN,
216
+ * etc. — commonly set for unrelated tools) must NOT silently switch the runner. Before this
217
+ * seam existed the orchestrator always used ClaudeCodeRunner; preserving that as the default
218
+ * (absent an explicit selector) avoids a breaking, surprising change for existing adopters.
219
+ *
220
+ * This function is async because step 3 may dynamically import a module.
221
+ *
222
+ * @throws Error when `runnerName` is provided but not registered in the registry.
223
+ * @throws Error when `AI_SDLC_RUNNER_PLUGIN` points to an invalid module.
224
+ */
225
+ export async function resolveRunner(registry, opts = {}) {
226
+ const env = opts.env ?? process.env;
227
+ // 1. Programmatic injection (options.runner / test override) — always wins
228
+ if (opts.injectedRunner) {
229
+ return opts.injectedRunner;
230
+ }
231
+ // 2. Explicit --runner <name> flag
232
+ if (opts.runnerName) {
233
+ const named = registry.get(opts.runnerName);
234
+ if (!named) {
235
+ const available = registry.listAvailable().map((r) => r.name);
236
+ throw new Error(`--runner "${opts.runnerName}" is not registered. ` +
237
+ `Available runners: ${available.length > 0 ? available.join(', ') : '(none)'}.\n` +
238
+ `Tip: set AI_SDLC_RUNNER_PLUGIN=/path/to/runner.mjs to load a custom runner first.`);
239
+ }
240
+ return named;
241
+ }
242
+ // 3. AI_SDLC_RUNNER_PLUGIN env — dynamically load + register, then return
243
+ const pluginPath = env.AI_SDLC_RUNNER_PLUGIN;
244
+ if (pluginPath) {
245
+ const registeredName = await registry.loadFromPlugin(pluginPath);
246
+ const pluginRunner = registry.get(registeredName);
247
+ // loadFromPlugin throws on invalid module, so pluginRunner is guaranteed to exist here
248
+ return pluginRunner;
249
+ }
250
+ // 4. ClaudeCodeRunner default (always in registry after discoverFromEnv).
251
+ // Env-discovered runners are intentionally NOT auto-selected here — the mere presence
252
+ // of an ambient API-key env var must not silently override the default (AISDLC-529 code
253
+ // review). An adopter selects an env-discovered runner explicitly via `--runner <name>`.
254
+ return registry.get('claude-code') ?? new (await import('./claude-code.js')).ClaudeCodeRunner();
255
+ }
166
256
  //# sourceMappingURL=runner-registry.js.map