axstack 0.20.29 → 0.20.31

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.
@@ -204,15 +204,18 @@ This section applies to peer and authored PR modes.
204
204
  - **Authored:** exactly one eligible independent reviewer from this complete
205
205
  mapping:
206
206
 
207
- | Preset | Actual author provider/model | Reviewer role (configured model/effort) |
207
+ | Preset | Actual author provider/class | Reviewer role (configured class/effort) |
208
208
  | --- | --- | --- |
209
- | `mixed` | Codex / Sol (`codex/gpt-6-sol`) | `axstack-reviewer-secondary` (`claude/claude-opus-5-5` medium) |
210
- | `mixed` | Claude / Opus (`claude/claude-opus-5-5`) | `axstack-reviewer-primary` (`codex/gpt-6-sol` high) |
211
- | `codex-only` | Codex / Sol (`codex/gpt-6-sol`) | `axstack-reviewer-secondary` (`codex/gpt-6-luna` xhigh) |
212
- | `claude-only` | Claude / Opus (`claude/claude-opus-5-5`) | `axstack-reviewer-secondary` (`claude/claude-sonnet-5-5` high) |
209
+ | `mixed` | `codex/sol` | `axstack-reviewer-secondary` (`claude/opus` medium) |
210
+ | `mixed` | `claude/opus` | `axstack-reviewer-primary` (`codex/sol` high) |
211
+ | `codex-only` | `codex/sol` | `axstack-reviewer-secondary` (`codex/luna` xhigh) |
212
+ | `claude-only` | `claude/opus` | `axstack-reviewer-secondary` (`claude/sonnet` high) |
213
213
 
214
- Provenance is matched on provider/model ID; record effort, but never use
215
- effort to create a mapping. Any other author provenance for the
214
+ The diligence receipt is separate and does not count as a reviewer receipt.
215
+
216
+ From the recorded exact model ID, derive its class and match provenance
217
+ on provider/class; record effort, but never use effort to create a mapping.
218
+ An ID with no class is `INCOMPLETE`. Any other author provenance for the
216
219
  selected preset is unsupported and `INCOMPLETE`, including its secondary
217
220
  reviewer model, Astra, Luna, or Fable. Report the exact provenance gap and
218
221
  ask the user. Never derive a reverse pairing from slot position. The
@@ -231,9 +234,19 @@ This section applies to peer and authored PR modes.
231
234
  effort and spawn no redundant final reviewer. If a required reviewer is
232
235
  unavailable, report that exact model gap, mark review `INCOMPLETE`, and ask
233
236
  the user; do not lower effort or choose any automatic fallback.
237
+ The only within-class exception is explicit model rejection before the first
238
+ turn: Codex may use
239
+ `--retry-of` with the next eligible ID in the same class, provider, and
240
+ effort; fence and record the rejected attempt. Timeout, quota, and auth
241
+ failures hold; Claude rejection holds. Never cross class or provider.
234
242
 
235
243
  Continue only when session receipts prove the required models, non-author
236
244
  independence, actual author provenance where applicable, and exact brief.
245
+ In peer and authored PR review rounds, dispatch `axstack-diligence`
246
+ independently alongside the configured reviewer(s) on the same exact revision
247
+ and base. Follow [Diligence](../axstack/references/diligence.md). A diligence
248
+ `FINDINGS` receipt returns validated findings to the same author in the same
249
+ round; it is not an extra `REQUEST_CHANGES` round.
237
250
  3. **Inspect all six angles.** In peer mode each reviewer covers every angle;
238
251
  in authored mode the one reviewer covers all six angles:
239
252
  1. Security and trust boundaries.
@@ -321,12 +334,14 @@ These verdicts apply only to PR modes. Codebase findings use coverage status.
321
334
  covering the whole brief, all six angles, and applicable acceptance.
322
335
  - **Complete verdict:** validated blocking defects permit `REQUEST_CHANGES`;
323
336
  complete evidence with no blocker permits `APPROVE`.
337
+ - **Diligence:** a current `PASS` is required with reviewer approval for
338
+ merge-ready; `FINDINGS` return to the author in that round.
324
339
  - **Incomplete or stale:** use `INCOMPLETE`; never fabricate `APPROVE` or
325
340
  `REQUEST_CHANGES`.
326
341
 
327
342
  The owner verifies and synthesizes the mode-required evidence without voting.
328
- A peer receipt count of one is incomplete; an authored receipt count other
329
- than one is not the selected mode. Passing tests or reviewer unanimity grants
343
+ A peer reviewer receipt count of one is incomplete; an authored reviewer
344
+ receipt count other than one is not the selected mode. Passing tests or reviewer unanimity grants
330
345
  no merge authority.
331
346
 
332
347
  ## Template: candidate review brief
@@ -397,10 +412,11 @@ Mode-required exact-revision completeness gates external approval,
397
412
  [merge-ready declarations](#authored-mode-own-pr), and authorized submission. It never gates returning
398
413
  evidence, limitations, validated risk, or an internal `INCOMPLETE` report.
399
414
 
400
- - Peer mode requires both current reviews and no unresolved material finding
401
- beyond the validated defects reported by `REQUEST_CHANGES`.
402
- - Authored mode requires its one current eligible configured review and applicable
403
- scope identity to remain valid.
415
+ - Peer mode requires both current reviews, a separate current diligence receipt,
416
+ and no unresolved material finding beyond the validated defects reported by
417
+ `REQUEST_CHANGES`.
418
+ - Authored mode requires its one current eligible configured reviewer receipt and
419
+ a separate current diligence receipt; applicable scope identity must remain valid.
404
420
  - A missing, mismatched, stale, or materially changed input blocks approval and
405
421
  merge-ready declarations while readonly investigation continues.
406
422
 
@@ -5,6 +5,9 @@ description: When agreed work needs an approved baseline, use axstack-spec to wr
5
5
 
6
6
  # Specification baseline
7
7
 
8
+ For authorized delivery runs, follow [Autopilot](../axstack/references/autopilot.md)
9
+ for phase continuation and holds.
10
+
8
11
  On driver entry, sweep under [Workspace hygiene](../axstack/references/workspace-hygiene.md); dispatched workers do not sweep.
9
12
  For every dispatch brief, name its private `<run dir>/evidence/<dispatch>/` folder.
10
13
 
@@ -54,6 +57,9 @@ and the lifecycle's [audit skill](../axstack-audit/SKILL.md) hook.
54
57
  session to return plain AGREE. Present one
55
58
  reviewable, identified revision for this checkpoint. Its user approval
56
59
  creates the execution baseline.
60
+ Before user approval, dispatch `axstack-diligence` under
61
+ [Diligence](../axstack/references/diligence.md) to check the draft against
62
+ the Align decisions for anything dropped, added, or softened.
57
63
  5. **Snapshot the baseline.** Record the approved revision identity and a
58
64
  concise repository Markdown counterpart. In Linear mode, the native
59
65
  document remains authoritative. In GitHub mode, the approved issue body is
@@ -72,7 +78,8 @@ Material change: <none | description + affected PRs/tasks + hold state>
72
78
  The snapshot is ready for ticketing when its authoritative revision,
73
79
  counterpart, and preserved ref resolve to the approved content. Return that
74
80
  exact identity; routine execution of the settled plan needs no repeat adviser
75
- consultation or spec approval.
81
+ consultation or spec approval. In an eligible delivery run with no hold,
82
+ continue to Tickets in the same driver chat.
76
83
 
77
84
  ## Material revisions
78
85
 
@@ -5,12 +5,15 @@ description: When an approved capability needs executable tasks, use axstack-tic
5
5
 
6
6
  # Tickets
7
7
 
8
+ For authorized delivery runs, follow [Autopilot](../axstack/references/autopilot.md)
9
+ for phase continuation and holds.
10
+
8
11
  On driver entry, sweep under [Workspace hygiene](../axstack/references/workspace-hygiene.md); dispatched workers do not sweep.
9
12
  For every dispatch brief, name its private `<run dir>/evidence/<dispatch>/` folder.
10
13
 
11
14
  Produce an executable capability map tied to the exact approved spec revision.
12
15
  Keep user-visible capabilities in the selected store, keep implementation detail
13
- in the repository, reconcile lifecycle state, and stop before implementation.
16
+ in the repository, reconcile lifecycle state, and return a map for continuation.
14
17
 
15
18
  Before mapping, load [Standing contracts](../axstack/references/contracts.md).
16
19
  Follow its required edge to [Shared lifecycle](../axstack/references/lifecycle.md),
@@ -54,6 +57,9 @@ an actual checker dispatch, not for ordinary mapping or state reconciliation.
54
57
  rationale with the task; actual measurement and exception evidence follow in
55
58
  the implement receipt. Mapping time requires no actual SHAs or line counts.
56
59
  Every capability ends with the fields below and an explicit dependency list.
60
+ Before accepting the map, dispatch `axstack-diligence` under
61
+ [Diligence](../axstack/references/diligence.md) to confirm every spec
62
+ acceptance item maps to a capability's acceptance.
57
63
  These routine mapping and split choices are autonomous driver decisions
58
64
  within the approved spec; size alone never requires user approval.
59
65
 
@@ -97,5 +103,5 @@ Recommendation: <move to In Review | keep open | close | other> (driver verifies
97
103
 
98
104
  5. **Return the mapping.** Report the pinned spec revision, selected store, map
99
105
  references, mutations performed by the driver, recorded gaps, and unresolved
100
- decisions. Stop with a map ready for lifecycle continuation; implementation
101
- has not started.
106
+ decisions. With a complete map and no hold, an eligible delivery run
107
+ continues to Implement in the same driver chat.
@@ -5,6 +5,9 @@ description: When babysitting an existing PR, use axstack-watch to monitor or ma
5
5
 
6
6
  # Watch
7
7
 
8
+ For authorized delivery runs, follow [Autopilot](../axstack/references/autopilot.md)
9
+ for phase continuation and holds.
10
+
8
11
  On driver entry, sweep under [Workspace hygiene](../axstack/references/workspace-hygiene.md); dispatched workers do not sweep.
9
12
  For every dispatch brief, name its private `<run dir>/evidence/<dispatch>/` folder.
10
13
 
@@ -49,7 +52,8 @@ authority is unverified, record the hold and continue read-only.
49
52
 
50
53
  Choose one mode from the user's authority and record it before dispatch:
51
54
 
52
- - **Chat-run watch:** the initiating chat remains the only driver and record
55
+ - **Chat-run watch:** authorized maintain mode is the default for run-created
56
+ PRs. The initiating chat remains the only driver and record
53
57
  writer for every PR raised in its Run, including later verified publications
54
58
  and explicitly adopted members. Follow [Chat-run watch runtime](references/watch-runtime.md#chat-run-watch)
55
59
  for its scheduled driver wake and Orca fallback. This mode has no replacement `axstack-owner` or
@@ -121,11 +125,16 @@ A handled wake has an acknowledged event ID, an observation or action bound to
121
125
  the current revision, and a recorded hold or next owner where work remains.
122
126
 
123
127
  Under a recorded `Notification policy`, the owner may use the optional
124
- [axstack-relay](../axstack-relay/SKILL.md) only for a serious risk immediately
125
- or a genuine blocked operation needing user intervention after bounded safe
126
- recovery. Questions, spec approvals, progress, CI pending, merge-ready, merged,
127
- and completion stay in Orca. The standalone monitor never sends; the chat-run
128
- observer reports only internally. Deduplicate authorized notifications;
128
+ [axstack-relay](../axstack-relay/SKILL.md) only for a serious risk immediately,
129
+ a genuine blocked operation needing user intervention after bounded safe
130
+ recovery, or decision holds and capped milestones named by the recorded policy.
131
+ Routine questions stay in Orca. Progress, CI pending, and completion always stay
132
+ in Orca.
133
+ Only the bounded categories—user-decision holds (including spec approval),
134
+ serious-risk holds, and at most two merge-ready/merged milestones per run—may
135
+ be relayed under the recorded Notification policy.
136
+ The standalone monitor never sends; the chat-run observer reports only
137
+ internally. Deduplicate authorized notifications;
129
138
  absent policy or failed relay uses the current Orca conversation and leaves
130
139
  the existing hold open.
131
140
 
@@ -135,6 +144,7 @@ The owner checks current required checks, all feedback, approvals, mergeability,
135
144
  and exact-revision receipts before any merge-ready statement. API errors leave
136
145
  readiness `UNKNOWN`; review approval alone is not merge-ready. Merge-ready is an
137
146
  observed state distinct from merged, and the human merges by default.
147
+ A current diligence `PASS` at the exact head is required before any merge-ready statement.
138
148
  Under authorized own-PR maintenance, keep repairing and rebasing onto the base
139
149
  when it moves, then re-run checks, until the head is rebased on the current base,
140
150
  every review comment and thread is addressed, at least one human team member's
@@ -147,10 +157,14 @@ human approval remain allowed.
147
157
 
148
158
  ## 6. End and preserve continuity
149
159
 
150
- End a chat-run watch after all members merged or closed, user cancellation, or
151
- the recorded wake expires. Stop the chosen wake and verify its stop receipt;
152
- a failed or uncertain harness wake stop is a hold.
153
- the Orca fallback also needs own-automation disable/readback and driver-owned automation
160
+ End a chat-run watch after all members merged or closed and the run's release
161
+ step is settled or not applicable, user cancellation, or the recorded wake
162
+ expires. Without an Autopilot or Release record, the release step is not
163
+ applicable to this watch. A required PR closed without merging records a
164
+ decision hold and the wake remains active while unexpired until the user
165
+ resolves scope, cancels, or the wake expires. Stop the chosen wake and verify
166
+ its stop receipt; a failed or uncertain harness wake stop is a hold.
167
+ The Orca fallback also needs own-automation disable/readback and driver-owned automation
154
168
  removal and workspace cleanup under
155
169
  [Watch runtime](references/watch-runtime.md#chat-run-watch).
156
170
 
@@ -182,6 +196,6 @@ Resume: <known commands or verified refs needed to reconcile from this revision>
182
196
 
183
197
  The watch ends only when registrations are stopped, receipts are recorded, and
184
198
  the PR is either merged or represented by this resumable state.
185
- When every required PR is merged, follow the lifecycle
186
- [Close-out](../axstack/references/lifecycle.md#close-out) before reporting the
187
- run as done.
199
+ When every required PR is merged and the run's Release step is settled or not
200
+ applicable, follow the lifecycle [Close-out](../axstack/references/lifecycle.md#close-out)
201
+ before reporting the run as done.
@@ -90,8 +90,10 @@ busy, missing, fenced, protected, or permission-held driver is never interrupted
90
90
  or replaced.
91
91
 
92
92
  The driver records one Notification policy: `axstack-relay` Telegram home only
93
- for a user-decision hold, first merge-ready and fully merged milestones (at most
94
- two), or serious-risk hold. Quiet ticks never notify.
93
+ for a user-decision hold (including spec and npm approval), merge-ready or
94
+ merged milestones (at most two across implementation and release), or a
95
+ serious-risk hold.
96
+ Quiet ticks never notify.
95
97
 
96
98
  The driver alone routes repair. Re-read remote head/base and native ownership.
97
99
  Independent PRs may repair in parallel in separate Orca child worktrees within
@@ -116,8 +118,13 @@ re-read all feedback and approvals at the current head before readiness.
116
118
  Re-reading approvals checks current state, not re-requesting review from a
117
119
  human who already approved.
118
120
 
119
- Stop the chosen wake only when every watched PR is merged or closed, the user cancels,
120
- or it expires. The driver stops a harness-native wake and verifies its stop receipt;
121
+ Stop the chosen wake only when every watched PR is merged or closed and the
122
+ run's release step is settled or not applicable, the user cancels, or it
123
+ expires. Without an Autopilot or Release record, the release step is not
124
+ applicable to this watch. A required PR closed without merging records a
125
+ decision hold and the wake remains active while unexpired until the user
126
+ resolves scope, cancels, or the wake expires; the run is not release-eligible.
127
+ The driver stops a harness-native wake and verifies its stop receipt;
121
128
  a failed or uncertain stop is a hold. Re-read membership and confirm no ambiguous
122
129
  publication or unsettled pass; cancellation
123
130
  prevents new work but does not prove running workers exited. The observer may
package/src/installer.js CHANGED
@@ -635,8 +635,16 @@ export async function installBundle({
635
635
  const newIds = new Set(bundle.bundleRoles.map((role) => role.id));
636
636
  const addedRoleIds = bundle.bundleRoles.filter((role) => !oldIds.has(role.id)).map((role) => role.id);
637
637
  const removedRoleIds = old.roles.filter((role) => role?.id && !newIds.has(role.id)).map((role) => role.id);
638
+ const oldById = new Map(old.roles.filter((role) => role?.id).map((role) => [role.id, role]));
639
+ const changedRoleModels = bundle.bundleRoles.flatMap((role) => {
640
+ const previous = oldById.get(role.id);
641
+ if (!previous) return [];
642
+ const fields = ['modelClass', 'model'].filter((field) => previous[field] !== role[field]);
643
+ return fields.length ? [`${role.id} (${fields.join(', ')})`] : [];
644
+ });
638
645
  summary.addedRoleIds = addedRoleIds;
639
646
  summary.removedRoleIds = removedRoleIds;
647
+ summary.changedRoleModels = changedRoleModels;
640
648
  }
641
649
  } catch { /* No reliable role-ID diff for malformed prior bytes. */ }
642
650
  }
package/src/roles.js CHANGED
@@ -4,19 +4,33 @@ const PROVIDER_BOUNDS = Object.freeze({
4
4
  'claude-only': new Set(['claude']),
5
5
  });
6
6
 
7
+ const CLASS_PROVIDERS = Object.freeze({
8
+ fable: 'claude', opus: 'claude', sonnet: 'claude', haiku: 'claude',
9
+ astra: 'codex', sol: 'codex', luna: 'codex',
10
+ });
11
+
12
+ export function deriveModelClass(model) {
13
+ if (typeof model !== 'string') return null;
14
+ return model.match(/^gpt-(\d+(?:\.\d+)*)-(astra|sol|luna)$/)?.[2]
15
+ ?? model.match(/^claude-(fable|opus|sonnet|haiku)-/)?.[1]
16
+ ?? null;
17
+ }
18
+
7
19
  const AUTHORED_ROUTES = Object.freeze({
8
20
  mixed: {
9
- 'codex/gpt-6-sol': ['axstack-reviewer-secondary', 'claude/claude-opus-5-5', 'medium'],
10
- 'claude/claude-opus-5-5': ['axstack-reviewer-primary', 'codex/gpt-6-sol', 'high'],
21
+ 'codex/sol': ['axstack-reviewer-secondary', 'claude/opus', 'medium'],
22
+ 'claude/opus': ['axstack-reviewer-primary', 'codex/sol', 'high'],
11
23
  },
12
24
  'codex-only': {
13
- 'codex/gpt-6-sol': ['axstack-reviewer-secondary', 'codex/gpt-6-luna', 'xhigh'],
25
+ 'codex/sol': ['axstack-reviewer-secondary', 'codex/luna', 'xhigh'],
14
26
  },
15
27
  'claude-only': {
16
- 'claude/claude-opus-5-5': ['axstack-reviewer-secondary', 'claude/claude-sonnet-5-5', 'high'],
28
+ 'claude/opus': ['axstack-reviewer-secondary', 'claude/sonnet', 'high'],
17
29
  },
18
30
  });
19
31
 
32
+ const roleClass = (role) => role.modelClass ?? deriveModelClass(role.model);
33
+
20
34
  export function assertBundleRoles(roles) {
21
35
  if (!Array.isArray(roles) || roles.length === 0) {
22
36
  throw new Error('invalid bundle roles: expected a non-empty roles array');
@@ -36,12 +50,22 @@ export function assertBundleRoles(roles) {
36
50
  }
37
51
  if (ids.has(role.id)) throw new Error(`invalid bundle roles: duplicate role ID ${role.id}`);
38
52
  ids.add(role.id);
39
- if (!Object.hasOwn(role, 'model')) {
53
+ const hasClass = Object.hasOwn(role, 'modelClass');
54
+ if (hasClass && (typeof role.modelClass !== 'string' || !Object.hasOwn(CLASS_PROVIDERS, role.modelClass))) {
55
+ throw new Error(`invalid bundle roles: ${role.id} has an unsupported modelClass`);
56
+ }
57
+ if (hasClass && role.provider !== CLASS_PROVIDERS[role.modelClass]) {
58
+ throw new Error(`invalid bundle roles: ${role.id} model class does not match provider`);
59
+ }
60
+ if (!hasClass && !Object.hasOwn(role, 'model')) {
40
61
  throw new Error('invalid bundle roles: model must be a non-empty string or explicit null');
41
62
  }
42
- if (role.model !== null && (typeof role.model !== 'string' || role.model.trim() === '')) {
63
+ if (Object.hasOwn(role, 'model') && role.model !== null && (typeof role.model !== 'string' || role.model.trim() === '')) {
43
64
  throw new Error('invalid bundle roles: model must be a non-empty string or explicit null');
44
65
  }
66
+ if (hasClass && typeof role.model === 'string' && deriveModelClass(role.model) !== role.modelClass) {
67
+ throw new Error(`invalid bundle roles: ${role.id} model pin does not match model class`);
68
+ }
45
69
  for (const field of ['icon', 'color', 'modeId', 'thinkingOptionId', 'notes']) {
46
70
  if (role[field] !== undefined && typeof role[field] !== 'string') {
47
71
  throw new Error(`invalid bundle roles: ${field} must be a string when present`);
@@ -69,13 +93,13 @@ export function assessRoleReadiness(roles, preset) {
69
93
  (preset === 'mixed' && role.id === 'axstack-arena-candidate-grok' && role.provider === 'grok') ||
70
94
  (preset === 'mixed' && role.id === 'axstack-arena-candidate-antigravity' && role.provider === 'antigravity') ||
71
95
  (preset === 'codex-only' && ['axstack-advisor-opus', 'axstack-escalation-fable', 'axstack-arena-judge-opus', 'axstack-research-web-google', 'axstack-research-x', 'axstack-arena-candidate-grok', 'axstack-arena-candidate-antigravity'].includes(role.id)) ||
72
- (preset === 'claude-only' && ['axstack-advisor-astra', 'axstack-arena-judge-astra', 'axstack-research-web-google', 'axstack-research-x', 'axstack-arena-candidate-grok', 'axstack-arena-candidate-antigravity'].includes(role.id))
96
+ (preset === 'claude-only' && ['axstack-advisor-astra', 'axstack-arena-judge-astra', 'axstack-research-web-google', 'axstack-research-x', 'axstack-arena-candidate-grok', 'axstack-arena-candidate-antigravity', 'axstack-auditor-sol', 'axstack-research-code-sol', 'axstack-explore-execution-sol'].includes(role.id))
73
97
  );
74
98
  for (const role of roles) {
75
99
  if (!bounds.has(role.provider)) {
76
100
  gaps.push(`${role.id} provider ${JSON.stringify(role.provider)} is outside ${preset} bounds (${[...bounds].join('|')})`);
77
101
  }
78
- if (!isIntentionalAbsence(role) && (typeof role.model !== 'string' || role.model.trim() === '')) {
102
+ if (!isIntentionalAbsence(role) && !Object.hasOwn(role, 'modelClass') && (typeof role.model !== 'string' || role.model.trim() === '')) {
79
103
  gaps.push(`${role.id} requires a configured model`);
80
104
  }
81
105
  }
@@ -86,7 +110,11 @@ export function assessRoleReadiness(roles, preset) {
86
110
  const primary = byId.get('axstack-reviewer-primary');
87
111
  const secondary = byId.get('axstack-reviewer-secondary');
88
112
  if (primary && secondary) {
89
- if (primary.model === secondary.model) gaps.push('reviewer pair must use two distinct models');
113
+ const bothResolved = typeof primary.model === 'string' && typeof secondary.model === 'string';
114
+ const sameReviewer = bothResolved
115
+ ? primary.model === secondary.model
116
+ : roleClass(primary) !== null && roleClass(primary) === roleClass(secondary);
117
+ if (sameReviewer) gaps.push('reviewer pair must use two distinct models');
90
118
  if (preset === 'mixed' && primary.provider === secondary.provider) {
91
119
  gaps.push('mixed reviewer pair must use different providers');
92
120
  }
@@ -94,14 +122,14 @@ export function assessRoleReadiness(roles, preset) {
94
122
 
95
123
  const author = byId.get('axstack-author');
96
124
  if (author) {
97
- const authorRoute = `${author.provider}/${author.model}`;
125
+ const authorRoute = `${author.provider}/${roleClass(author) ?? author.model}`;
98
126
  const route = AUTHORED_ROUTES[preset]?.[authorRoute];
99
127
  if (!route) {
100
128
  gaps.push(`authored routing gap: unsupported axstack-author route ${authorRoute}`);
101
129
  } else {
102
130
  const [reviewerId, reviewerRoute, effort] = route;
103
131
  const reviewer = byId.get(reviewerId);
104
- if (!reviewer || `${reviewer.provider}/${reviewer.model}` !== reviewerRoute || reviewer.thinkingOptionId !== effort) {
132
+ if (!reviewer || `${reviewer.provider}/${roleClass(reviewer)}` !== reviewerRoute || reviewer.thinkingOptionId !== effort) {
105
133
  gaps.push(`authored routing gap: ${reviewerId} must be ${reviewerRoute}/${effort} for author ${authorRoute}`);
106
134
  }
107
135
  }