crewly 1.20.388 → 1.20.389
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.
- package/config/skills/agent/core/app-comments/execute.sh +3 -2
- package/config/skills/agent/core/app-comments/execute.test.sh +1 -1
- package/config/skills/team-leader/propose-tier-change/SKILL.md +16 -10
- package/config/skills/team-leader/propose-tier-change/execute.sh +4 -3
- package/dist/backend/backend/src/constants.d.ts +20 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +20 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/apps/apps.controller.js +2 -2
- package/dist/backend/backend/src/controllers/apps/apps.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.js +8 -0
- package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/owner-receipt/owner-receipt.controller.d.ts +4 -3
- package/dist/backend/backend/src/controllers/owner-receipt/owner-receipt.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/owner-receipt/owner-receipt.controller.js +7 -3
- package/dist/backend/backend/src/controllers/owner-receipt/owner-receipt.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/team/team-model-tiers.controller.d.ts +2 -2
- package/dist/backend/backend/src/controllers/team/team-model-tiers.controller.js +3 -3
- package/dist/backend/backend/src/controllers/team/team-model-tiers.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +30 -18
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/member-relaunch.d.ts +84 -0
- package/dist/backend/backend/src/services/agent/member-relaunch.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/member-relaunch.js +96 -0
- package/dist/backend/backend/src/services/agent/member-relaunch.js.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-builder.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-builder.service.js +2 -0
- package/dist/backend/backend/src/services/ai/prompt-builder.service.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/model-tiers.module.d.ts +3 -2
- package/dist/backend/backend/src/services/ai/prompt-modules/model-tiers.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/model-tiers.module.js +11 -4
- package/dist/backend/backend/src/services/ai/prompt-modules/model-tiers.module.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts +2 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/prompt-module.interface.js.map +1 -1
- package/dist/backend/backend/src/services/apps/app-comment-room.service.d.ts +10 -1
- package/dist/backend/backend/src/services/apps/app-comment-room.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/apps/app-comment-room.service.js +15 -1
- package/dist/backend/backend/src/services/apps/app-comment-room.service.js.map +1 -1
- package/dist/backend/backend/src/services/apps/apps.service.d.ts +3 -2
- package/dist/backend/backend/src/services/apps/apps.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/apps/apps.service.js +3 -2
- package/dist/backend/backend/src/services/apps/apps.service.js.map +1 -1
- package/dist/backend/backend/src/services/apps/apps.wiring.js +2 -2
- package/dist/backend/backend/src/services/apps/apps.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts +15 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js +36 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/sqlite/chat-db.d.ts +1 -1
- package/dist/backend/backend/src/services/chat-v2/sqlite/chat-db.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/sqlite/chat-db.js +4 -0
- package/dist/backend/backend/src/services/chat-v2/sqlite/chat-db.js.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision-card.d.ts +14 -5
- package/dist/backend/backend/src/services/decisions/decision-card.d.ts.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision-card.js +19 -3
- package/dist/backend/backend/src/services/decisions/decision-card.js.map +1 -1
- package/dist/backend/backend/src/services/drive/drive-agent.service.d.ts +6 -1
- package/dist/backend/backend/src/services/drive/drive-agent.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/drive/drive-agent.service.js +17 -0
- package/dist/backend/backend/src/services/drive/drive-agent.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +13 -0
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js +33 -2
- package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
- package/dist/backend/backend/src/services/model-tiers/model-tier.service.d.ts +107 -13
- package/dist/backend/backend/src/services/model-tiers/model-tier.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/model-tiers/model-tier.service.js +362 -37
- package/dist/backend/backend/src/services/model-tiers/model-tier.service.js.map +1 -1
- package/dist/backend/backend/src/services/model-tiers/model-tier.store.d.ts +7 -3
- package/dist/backend/backend/src/services/model-tiers/model-tier.store.d.ts.map +1 -1
- package/dist/backend/backend/src/services/model-tiers/model-tier.store.js.map +1 -1
- package/dist/backend/backend/src/services/model-tiers/model-tier.wiring.d.ts +33 -2
- package/dist/backend/backend/src/services/model-tiers/model-tier.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/model-tiers/model-tier.wiring.js +73 -2
- package/dist/backend/backend/src/services/model-tiers/model-tier.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.js +8 -37
- package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts +10 -1
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.js +18 -8
- package/dist/backend/backend/src/services/slack/slack-agent-post.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +7 -0
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +16 -0
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/stall/stall-watcher.service.d.ts +2 -0
- package/dist/backend/backend/src/services/stall/stall-watcher.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/stall/stall-watcher.service.js +6 -0
- package/dist/backend/backend/src/services/stall/stall-watcher.service.js.map +1 -1
- package/dist/backend/backend/src/services/stall/stall-watcher.wiring.d.ts +14 -0
- package/dist/backend/backend/src/services/stall/stall-watcher.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/stall/stall-watcher.wiring.js +18 -2
- package/dist/backend/backend/src/services/stall/stall-watcher.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-pulls-post-log.d.ts +28 -0
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-pulls-post-log.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-pulls-post-log.js +40 -2
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-pulls-post-log.js.map +1 -1
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-pulls-weekly.d.ts +40 -1
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-pulls-weekly.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-pulls-weekly.js +52 -4
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-pulls-weekly.js.map +1 -1
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-pulls-weekly.loader.d.ts +8 -0
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-pulls-weekly.loader.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-pulls-weekly.loader.js +17 -5
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-pulls-weekly.loader.js.map +1 -1
- package/dist/backend/backend/src/types/decision.types.d.ts +17 -2
- package/dist/backend/backend/src/types/decision.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/index.d.ts +9 -0
- package/dist/backend/backend/src/types/index.d.ts.map +1 -1
- package/dist/backend/backend/src/types/index.js.map +1 -1
- package/dist/backend/backend/src/utils/model-tier.utils.d.ts +23 -1
- package/dist/backend/backend/src/utils/model-tier.utils.d.ts.map +1 -1
- package/dist/backend/backend/src/utils/model-tier.utils.js +28 -0
- package/dist/backend/backend/src/utils/model-tier.utils.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +20 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +20 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-builder.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-builder.service.js +2 -0
- package/dist/cli/backend/src/services/ai/prompt-builder.service.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/model-tiers.module.d.ts +3 -2
- package/dist/cli/backend/src/services/ai/prompt-modules/model-tiers.module.d.ts.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/model-tiers.module.js +11 -4
- package/dist/cli/backend/src/services/ai/prompt-modules/model-tiers.module.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts +2 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.js.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts +15 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js +36 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/sqlite/chat-db.d.ts +1 -1
- package/dist/cli/backend/src/services/chat-v2/sqlite/chat-db.d.ts.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/sqlite/chat-db.js +4 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/chat-db.js.map +1 -1
- package/dist/cli/backend/src/services/decisions/decision-card.d.ts +14 -5
- package/dist/cli/backend/src/services/decisions/decision-card.d.ts.map +1 -1
- package/dist/cli/backend/src/services/decisions/decision-card.js +19 -3
- package/dist/cli/backend/src/services/decisions/decision-card.js.map +1 -1
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +13 -0
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js +33 -2
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +7 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +16 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/types/decision.types.d.ts +17 -2
- package/dist/cli/backend/src/types/decision.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/index.d.ts +9 -0
- package/dist/cli/backend/src/types/index.d.ts.map +1 -1
- package/dist/cli/backend/src/types/index.js.map +1 -1
- package/dist/cli/backend/src/utils/model-tier.utils.d.ts +23 -1
- package/dist/cli/backend/src/utils/model-tier.utils.d.ts.map +1 -1
- package/dist/cli/backend/src/utils/model-tier.utils.js +28 -0
- package/dist/cli/backend/src/utils/model-tier.utils.js.map +1 -1
- package/frontend/dist/assets/{index-778ed2d9.js → index-d60af958.js} +237 -237
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ModelTierService — model tiers and "Optimize usage" (crewly#1173
|
|
3
|
-
* specs/2026-10-08-model-tiers.md).
|
|
2
|
+
* ModelTierService — model tiers and "Optimize usage" (crewly#1173 phases 1
|
|
3
|
+
* and 2, specs/2026-10-08-model-tiers.md).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Two change modes per team (`Team.tierChangeMode`, owner-only):
|
|
6
|
+
* `approve` (default) — the lead proposes, the owner approves each change;
|
|
7
|
+
* `auto` — the lead's changes apply at once and the owner gets a notice with
|
|
8
|
+
* one-tap Undo.
|
|
6
9
|
*
|
|
7
10
|
* 1. Review: weekly while a team has "Optimize usage" on (and on demand), the
|
|
8
11
|
* team lead gets a compact usage report per member and is asked to
|
|
9
12
|
* propose tier changes and task routing rules.
|
|
10
|
-
* 2. Proposal: the lead adds changes with `propose-tier-change` and submits
|
|
11
|
-
* ONE owner card per review carries them all (Apply / Keep
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* 2. Proposal: the lead adds changes with `propose-tier-change` and submits.
|
|
14
|
+
* Approve mode: ONE owner card per review carries them all (Apply / Keep
|
|
15
|
+
* as is). Auto mode: they apply right away, and ONE notice (Undo / Keep)
|
|
16
|
+
* goes to the same place.
|
|
17
|
+
* 3. Apply: the tiers are set (a member's fixed model is cleared so the tier
|
|
18
|
+
* takes effect). An idle member whose model changed is restarted so the
|
|
19
|
+
* new model takes effect now; a busy one picks it up at its next start.
|
|
15
20
|
* 4. Quality guard: a member moved down is watched over its next settled work
|
|
16
|
-
* items; when it is sent back clearly more often than before,
|
|
17
|
-
*
|
|
21
|
+
* items; when it is sent back clearly more often than before, approve mode
|
|
22
|
+
* sends a revert card, auto mode moves it back at once and tells the owner
|
|
23
|
+
* (with "Keep the lower tier" to undo the revert).
|
|
18
24
|
*
|
|
19
25
|
* @module services/model-tiers/model-tier.service
|
|
20
26
|
*/
|
|
21
27
|
import { randomUUID } from 'crypto';
|
|
22
28
|
import { MODEL_TIER_CONSTANTS, RUNTIME_TYPES } from '../../constants.js';
|
|
23
29
|
import { effectiveMemberModelId } from '../../utils/member-default-model.utils.js';
|
|
24
|
-
import { isModelTier, parseTierModels, resolveTierModel, tierMapFor, tierRank } from '../../utils/model-tier.utils.js';
|
|
30
|
+
import { isModelTier, isTierChangeMode, leadChangesTiersItself, parseTierModels, resolveTierModel, tierChangeModeOf, tierMapFor, tierRank } from '../../utils/model-tier.utils.js';
|
|
25
31
|
import { getTeamLeads, isTeamLead } from '../../utils/team.utils.js';
|
|
26
32
|
import { resolveRate } from '../monitoring/model-pricing.js';
|
|
27
33
|
import { memberLedgerKeys } from '../tl-delegation/lead-share.js';
|
|
@@ -31,6 +37,8 @@ const C = MODEL_TIER_CONSTANTS;
|
|
|
31
37
|
const APPLY_KEY = 'a';
|
|
32
38
|
const KEEP_KEY = 'b';
|
|
33
39
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
40
|
+
/** What the lead is told about restarts after an apply. */
|
|
41
|
+
const RESTART_NOTE = 'Idle members restart now on their new model; a busy member picks it up at its next start.';
|
|
34
42
|
/** A refusal with an HTTP status. */
|
|
35
43
|
export class ModelTierError extends Error {
|
|
36
44
|
status;
|
|
@@ -54,6 +62,8 @@ export class ModelTierService {
|
|
|
54
62
|
deps;
|
|
55
63
|
timer = null;
|
|
56
64
|
ticking = false;
|
|
65
|
+
restarts = Promise.resolve();
|
|
66
|
+
submitting = new Set();
|
|
57
67
|
constructor(deps) {
|
|
58
68
|
this.deps = deps;
|
|
59
69
|
}
|
|
@@ -100,6 +110,7 @@ export class ModelTierService {
|
|
|
100
110
|
return {
|
|
101
111
|
teamId,
|
|
102
112
|
optimizeUsage: team.optimizeUsage === true,
|
|
113
|
+
tierChangeMode: tierChangeModeOf(team),
|
|
103
114
|
tierModels: team.tierModels ?? {},
|
|
104
115
|
routingRules: team.tierRoutingRules ?? [],
|
|
105
116
|
tierMaps,
|
|
@@ -139,6 +150,11 @@ export class ModelTierService {
|
|
|
139
150
|
throw new ModelTierError(400, 'optimizeUsage is true or false');
|
|
140
151
|
team.optimizeUsage = patch.optimizeUsage;
|
|
141
152
|
}
|
|
153
|
+
if (patch.tierChangeMode !== undefined) {
|
|
154
|
+
if (!isTierChangeMode(patch.tierChangeMode))
|
|
155
|
+
throw new ModelTierError(400, 'tierChangeMode is "approve" or "auto"');
|
|
156
|
+
team.tierChangeMode = patch.tierChangeMode;
|
|
157
|
+
}
|
|
142
158
|
if (patch.tierModels !== undefined) {
|
|
143
159
|
const parsed = parseTierModels(patch.tierModels);
|
|
144
160
|
if (!parsed.ok)
|
|
@@ -218,8 +234,13 @@ export class ModelTierService {
|
|
|
218
234
|
reviewMessage(team, report, reviewId, trigger) {
|
|
219
235
|
const skill = `bash ${this.deps.tlSkillsPath}/propose-tier-change/execute.sh`;
|
|
220
236
|
const rules = team.tierRoutingRules?.length ? `\nRouting rules in force: ${team.tierRoutingRules.map((r) => `"${r}"`).join('; ')}.` : '';
|
|
237
|
+
const auto = leadChangesTiersItself(team);
|
|
238
|
+
const who = auto ? 'you decide: your changes apply as soon as you submit, and the owner is told and can undo them' : 'the owner approves every change';
|
|
239
|
+
const send = auto
|
|
240
|
+
? `4. Send it: ${skill} --submit. The changes apply at once: idle members restart on their new model, busy ones switch at their next start, and the owner gets one notice with Undo. If nothing should change, still run --submit (nothing is posted).`
|
|
241
|
+
: `4. Send it: ${skill} --submit. One card goes to the owner; nothing changes until the owner says yes. If nothing should change, still run --submit (no card is posted).`;
|
|
221
242
|
return [
|
|
222
|
-
`[MODEL TIER REVIEW] ${trigger === 'weekly' ? 'Weekly' : 'Requested'} usage review for team ${team.name} (${reviewId}). "Optimize usage" is on;
|
|
243
|
+
`[MODEL TIER REVIEW] ${trigger === 'weekly' ? 'Weekly' : 'Requested'} usage review for team ${team.name} (${reviewId}). "Optimize usage" is on; ${who}.`,
|
|
223
244
|
'',
|
|
224
245
|
renderTierReport(report),
|
|
225
246
|
rules,
|
|
@@ -228,7 +249,7 @@ export class ModelTierService {
|
|
|
228
249
|
'1. For each member, decide whether its work needs its tier. strong = hard reasoning, design, reviewing others. mid = normal implementation and writing. weak = routine work: polling, checks, formatting, sorting, first-pass triage, summaries.',
|
|
229
250
|
`2. Propose a change for each member that should move: ${skill} --member "<name>" --tier weak --reason "<the work it does and why the tier fits>"`,
|
|
230
251
|
`3. Propose routing rules for incoming work: ${skill} --routing "polling / formatting / sorting -> <member>"`,
|
|
231
|
-
|
|
252
|
+
send,
|
|
232
253
|
'Do not lower a member whose work was often sent back, and do not lower yourself below mid.',
|
|
233
254
|
].join('\n');
|
|
234
255
|
}
|
|
@@ -328,6 +349,18 @@ export class ModelTierService {
|
|
|
328
349
|
return state.draft;
|
|
329
350
|
}
|
|
330
351
|
async submit(ctx) {
|
|
352
|
+
// The hourly auto-submit and the lead's own --submit can overlap; one wins.
|
|
353
|
+
if (this.submitting.has(ctx.team.id))
|
|
354
|
+
throw new ModelTierError(409, 'This review is being submitted already');
|
|
355
|
+
this.submitting.add(ctx.team.id);
|
|
356
|
+
try {
|
|
357
|
+
return await this.submitOnce(ctx);
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
this.submitting.delete(ctx.team.id);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async submitOnce(ctx) {
|
|
331
364
|
const state = await this.deps.store.get(ctx.team.id);
|
|
332
365
|
const draft = state.draft;
|
|
333
366
|
if (!draft || (draft.changes.length === 0 && draft.routing.length === 0)) {
|
|
@@ -337,10 +370,128 @@ export class ModelTierService {
|
|
|
337
370
|
await this.refreshOpenDecision(ctx.team.id, state);
|
|
338
371
|
if (state.openDecisionId)
|
|
339
372
|
throw new ModelTierError(409, `The last tier proposal (${state.openDecisionId}) still waits for the owner; add to it next review`);
|
|
373
|
+
if (leadChangesTiersItself(ctx.team))
|
|
374
|
+
return this.applyNow(ctx, draft);
|
|
340
375
|
const decision = await this.askOwner(ctx.team, ctx.lead, draft, 'review');
|
|
341
376
|
await this.deps.store.update(ctx.team.id, (s) => ({ ...s, draft: undefined, openDecisionId: decision.id }));
|
|
342
377
|
return { submitted: true, decisionId: decision.id, changes: draft.changes.length, routing: draft.routing.length, note: 'One card went to the owner. Nothing changes until the owner says yes; you will be told the answer.' };
|
|
343
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Auto mode: apply the lead's draft at once (the same apply path as the
|
|
381
|
+
* owner's Apply), then post ONE notice where the card would go, whose Undo
|
|
382
|
+
* puts back exactly these changes.
|
|
383
|
+
*
|
|
384
|
+
* @param ctx - The lead and its team
|
|
385
|
+
* @param draft - The submitted draft
|
|
386
|
+
* @returns What happened (for the skill output)
|
|
387
|
+
*/
|
|
388
|
+
async applyNow(ctx, draft) {
|
|
389
|
+
if (!this.deps.decisions())
|
|
390
|
+
throw new ModelTierError(503, 'Owner notices are not running on this machine, so the change cannot be applied (the owner must be able to undo it).');
|
|
391
|
+
const before = ctx.team.tierRoutingRules ?? [];
|
|
392
|
+
const subject = {
|
|
393
|
+
teamId: ctx.team.id,
|
|
394
|
+
teamName: ctx.team.name,
|
|
395
|
+
reviewId: draft.reviewId,
|
|
396
|
+
kind: 'review',
|
|
397
|
+
changes: draft.changes,
|
|
398
|
+
routing: draft.routing,
|
|
399
|
+
askerSession: sessionOf(ctx.lead),
|
|
400
|
+
askerName: ctx.lead.name,
|
|
401
|
+
};
|
|
402
|
+
// Priced before the apply: the estimate compares each member's current model with its new one.
|
|
403
|
+
const saving = this.estimateSaving(ctx.team, draft.changes);
|
|
404
|
+
const applied = await this.apply(subject, draft.reviewId);
|
|
405
|
+
await this.deps.store.update(ctx.team.id, (s) => ({ ...s, draft: undefined }));
|
|
406
|
+
const team = await this.team(ctx.team.id);
|
|
407
|
+
const undo = {
|
|
408
|
+
...subject,
|
|
409
|
+
kind: 'undo',
|
|
410
|
+
notice: 'applied',
|
|
411
|
+
undoOf: draft.reviewId,
|
|
412
|
+
changes: applied.map((c) => reverseEntry(c, team, 'the owner undid it')),
|
|
413
|
+
routing: before.filter((r) => !(team.tierRoutingRules ?? []).includes(r)),
|
|
414
|
+
removeRouting: draft.routing.filter((r) => !before.includes(r)),
|
|
415
|
+
};
|
|
416
|
+
let noticeId = null;
|
|
417
|
+
try {
|
|
418
|
+
noticeId = (await this.postNotice(team, ctx.lead, undo, applied, draft.routing, saving)).id;
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
this.deps.logger?.warn('Tier changes applied but the owner notice could not be posted', { teamId: team.id, error: err instanceof Error ? err.message : String(err) });
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
submitted: true,
|
|
425
|
+
applied: true,
|
|
426
|
+
changes: applied.length,
|
|
427
|
+
routing: draft.routing.length,
|
|
428
|
+
...(noticeId ? { noticeId } : {}),
|
|
429
|
+
note: `Applied now. ${RESTART_NOTE} ${noticeId ? 'The owner got one notice and can undo it.' : 'The owner notice could not be posted; tell the owner what you changed.'}`,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Post an auto-mode notice: the change already happened; its one action
|
|
434
|
+
* (`APPLY_KEY`) applies `subject` — the undo. Default and plain "yes" = keep.
|
|
435
|
+
*
|
|
436
|
+
* @param team - Team
|
|
437
|
+
* @param asker - The lead (its bot posts it, it gets the outcome)
|
|
438
|
+
* @param subject - The undo the action applies
|
|
439
|
+
* @param done - What happened (the lead's changes, or the guard's move back)
|
|
440
|
+
* @param routing - Routing rules that went in (applied notices)
|
|
441
|
+
* @param saving - Weekly saving estimate of the lead's changes, or null
|
|
442
|
+
* @returns The notice decision
|
|
443
|
+
*/
|
|
444
|
+
async postNotice(team, asker, subject, done, routing, saving) {
|
|
445
|
+
const decisions = this.deps.decisions();
|
|
446
|
+
if (!decisions)
|
|
447
|
+
throw new ModelTierError(503, 'Owner notices are not running on this machine.');
|
|
448
|
+
const reverted = subject.notice === 'reverted';
|
|
449
|
+
const body = [];
|
|
450
|
+
let question;
|
|
451
|
+
let options;
|
|
452
|
+
if (reverted) {
|
|
453
|
+
const c = done[0];
|
|
454
|
+
const m = subject.changes[0];
|
|
455
|
+
question = `${c.memberName}'s work was sent back more often on the ${c.from} tier, so it was moved back to ${tierLabel(c.clearTier ? null : c.to, c.toModel)}.`;
|
|
456
|
+
body.push(`${c.reason[0].toUpperCase()}${c.reason.slice(1)}.`);
|
|
457
|
+
options = [
|
|
458
|
+
{ key: APPLY_KEY, label: 'Keep the lower tier', detail: `moves ${m.memberName} to ${m.to} again` },
|
|
459
|
+
{ key: KEEP_KEY, label: 'OK', detail: 'leave it moved back' },
|
|
460
|
+
];
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
const summary = done.map((c) => `${c.memberName} → ${targetLabel(c)}`).join(', ');
|
|
464
|
+
question = summary ? `${asker.name} changed model tiers for ${team.name}: ${summary}.` : `${asker.name} set new work routing for ${team.name}.`;
|
|
465
|
+
for (const c of done) {
|
|
466
|
+
const fixed = c.clearsModelId ? ` (its fixed model \`${c.clearsModelId}\` was cleared)` : '';
|
|
467
|
+
body.push(`• *${c.memberName}*: ${tierLabel(c.from, c.fromModel)} → ${tierLabel(c.clearTier ? null : c.to, c.toModel)}${fixed} — ${c.reason}`);
|
|
468
|
+
}
|
|
469
|
+
if (routing.length)
|
|
470
|
+
body.push(`New work routing: ${routing.map((r) => `"${r}"`).join('; ')}`);
|
|
471
|
+
if (saving !== null)
|
|
472
|
+
body.push(`Estimated change: ${saving >= 0 ? 'saves' : 'adds'} about $${Math.abs(saving).toFixed(2)} a week at API list price (from the last ${C.REPORT_WINDOW_DAYS} days).`);
|
|
473
|
+
options = [
|
|
474
|
+
{ key: APPLY_KEY, label: 'Undo', detail: 'puts these back as they were' },
|
|
475
|
+
{ key: KEEP_KEY, label: 'Keep', detail: 'nothing more to do' },
|
|
476
|
+
];
|
|
477
|
+
}
|
|
478
|
+
if (done.length)
|
|
479
|
+
body.push('Idle members switch to the new model now; busy ones at their next start.');
|
|
480
|
+
const channel = await this.deps.teamChannelOf?.(team.id).catch(() => null);
|
|
481
|
+
return decisions.askPrebuilt({
|
|
482
|
+
kind: C.DECISION_KIND,
|
|
483
|
+
asker: sessionOf(asker),
|
|
484
|
+
question,
|
|
485
|
+
title: reverted ? `Model tiers — quality check, ${team.name}` : `Model tiers — ${team.name}`,
|
|
486
|
+
body,
|
|
487
|
+
options,
|
|
488
|
+
defaultKey: KEEP_KEY,
|
|
489
|
+
yesKey: KEEP_KEY,
|
|
490
|
+
deadline: new Date(this.now().getTime() + C.NOTICE_DEADLINE_MS),
|
|
491
|
+
tierChange: subject,
|
|
492
|
+
...(channel ? { place: { slackChannelId: channel } } : {}),
|
|
493
|
+
});
|
|
494
|
+
}
|
|
344
495
|
/** Post the owner card for a draft (review) or a revert. */
|
|
345
496
|
async askOwner(team, asker, draft, kind, extraBody = []) {
|
|
346
497
|
const decisions = this.deps.decisions();
|
|
@@ -370,9 +521,9 @@ export class ModelTierService {
|
|
|
370
521
|
const saving = kind === 'review' ? this.estimateSaving(team, draft.changes) : null;
|
|
371
522
|
if (saving !== null)
|
|
372
523
|
body.push(`Estimated change: ${saving >= 0 ? 'saves' : 'adds'} about $${Math.abs(saving).toFixed(2)} a week at API list price (from the last ${C.REPORT_WINDOW_DAYS} days).`);
|
|
373
|
-
body.push('
|
|
524
|
+
body.push('On Apply, idle members switch to the new model right away; busy ones at their next start.');
|
|
374
525
|
const options = [
|
|
375
|
-
{ key: APPLY_KEY, label: kind === 'revert' ? 'Move back' : 'Apply', detail: '
|
|
526
|
+
{ key: APPLY_KEY, label: kind === 'revert' ? 'Move back' : 'Apply', detail: 'idle members switch now, busy ones at their next start' },
|
|
376
527
|
{ key: KEEP_KEY, label: 'Keep as is', detail: 'nothing changes' },
|
|
377
528
|
];
|
|
378
529
|
const channel = await this.deps.teamChannelOf?.(team.id).catch(() => null);
|
|
@@ -460,6 +611,8 @@ export class ModelTierService {
|
|
|
460
611
|
if (!s)
|
|
461
612
|
return null;
|
|
462
613
|
await this.deps.store.update(s.teamId, (st) => (st.openDecisionId === decision.id ? { ...st, openDecisionId: undefined } : st));
|
|
614
|
+
if (s.notice)
|
|
615
|
+
return this.onNoticeSettled(decision, s);
|
|
463
616
|
if (decision.status !== 'resolved' || decision.chosenKey !== APPLY_KEY) {
|
|
464
617
|
if (s.kind === 'revert')
|
|
465
618
|
return `[MODEL TIERS] The owner kept the lower tier (${decision.id}). Nothing changed.`;
|
|
@@ -469,7 +622,7 @@ export class ModelTierService {
|
|
|
469
622
|
const applied = await this.apply(s, decision.id);
|
|
470
623
|
const lines = applied.map((c) => `${c.memberName} → ${tierLabel(c.clearTier ? null : c.to, c.toModel)}`);
|
|
471
624
|
const routing = s.routing.length ? ` Routing rules now in force: ${s.routing.map((r) => `"${r}"`).join('; ')}. Route new work by them.` : '';
|
|
472
|
-
return `[MODEL TIERS] The owner approved ${decision.id}: ${lines.join(', ') || 'routing rules only'}.
|
|
625
|
+
return `[MODEL TIERS] The owner approved ${decision.id}: ${lines.join(', ') || 'routing rules only'}. ${RESTART_NOTE}${routing}`;
|
|
473
626
|
}
|
|
474
627
|
catch (err) {
|
|
475
628
|
this.deps.logger?.warn('Applying tier changes failed', { decisionId: decision.id, error: String(err) });
|
|
@@ -477,10 +630,42 @@ export class ModelTierService {
|
|
|
477
630
|
}
|
|
478
631
|
}
|
|
479
632
|
/**
|
|
480
|
-
*
|
|
633
|
+
* An auto-mode notice settled: its one action undoes what the notice is
|
|
634
|
+
* about; anything else (Keep, the deadline, skip) leaves it as it is.
|
|
635
|
+
*
|
|
636
|
+
* @param decision - The settled notice
|
|
637
|
+
* @param s - Its subject (the undo)
|
|
638
|
+
* @returns Note for the lead, or null
|
|
639
|
+
*/
|
|
640
|
+
async onNoticeSettled(decision, s) {
|
|
641
|
+
if (decision.status !== 'resolved' || decision.chosenKey !== APPLY_KEY)
|
|
642
|
+
return null;
|
|
643
|
+
try {
|
|
644
|
+
const applied = await this.apply(s, decision.id);
|
|
645
|
+
const lines = applied.map((c) => `${c.memberName} → ${tierLabel(c.clearTier ? null : c.to, c.toModel)}`);
|
|
646
|
+
const skipped = s.changes.length - applied.length;
|
|
647
|
+
const skippedNote = skipped > 0 ? ` ${skipped} member(s) were left as they are: their tier changed again since.` : '';
|
|
648
|
+
if (s.notice === 'reverted') {
|
|
649
|
+
return `[MODEL TIERS] The owner chose to keep the lower tier after the quality check: ${lines.join(', ') || 'nothing to change'}. It is not watched again.${skippedNote} ${RESTART_NOTE}`;
|
|
650
|
+
}
|
|
651
|
+
const routing = s.removeRouting?.length ? ` Routing rules removed: ${s.removeRouting.map((r) => `"${r}"`).join('; ')}.` : '';
|
|
652
|
+
return `[MODEL TIERS] The owner undid your tier changes: ${lines.join(', ') || 'routing rules only'}.${routing}${skippedNote} Do not make the same change again this week. ${RESTART_NOTE}`;
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
this.deps.logger?.warn('Undoing tier changes failed', { decisionId: decision.id, error: String(err) });
|
|
656
|
+
return `[MODEL TIERS] The owner tapped Undo, but undoing failed (${err instanceof Error ? err.message : String(err)}). Tell the owner.`;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Apply a subject to the team (owner's Apply, the lead's change in auto
|
|
661
|
+
* mode, an undo or a revert), record it for the guard, and restart idle
|
|
662
|
+
* members whose model changed.
|
|
663
|
+
*
|
|
664
|
+
* An `undo` changes only members still on the tier it expects (`from`): a
|
|
665
|
+
* member whose tier changed again since is left alone.
|
|
481
666
|
*
|
|
482
667
|
* @param s - Subject
|
|
483
|
-
* @param decisionId - The approving decision
|
|
668
|
+
* @param decisionId - The approving decision, or the batch id of an auto apply
|
|
484
669
|
* @returns The applied entries
|
|
485
670
|
*/
|
|
486
671
|
async apply(s, decisionId) {
|
|
@@ -488,27 +673,57 @@ export class ModelTierService {
|
|
|
488
673
|
const items = await this.deps.workItems().catch(() => []);
|
|
489
674
|
const now = this.now();
|
|
490
675
|
const applied = [];
|
|
676
|
+
const modelChanged = [];
|
|
677
|
+
// An undo puts back only members whose latest change is the one undone:
|
|
678
|
+
// a later change (by the owner's card or another batch) wins.
|
|
679
|
+
const latest = new Map();
|
|
680
|
+
if (s.kind === 'undo')
|
|
681
|
+
for (const a of (await this.deps.store.get(s.teamId)).applied)
|
|
682
|
+
latest.set(a.memberId, a.decisionId);
|
|
491
683
|
for (const c of s.changes) {
|
|
492
684
|
const m = team.members.find((x) => x.id === c.memberId);
|
|
493
|
-
if (!m)
|
|
685
|
+
if (!m || m.role === 'orchestrator')
|
|
494
686
|
continue;
|
|
687
|
+
if (s.kind === 'undo' && ((m.tier ?? null) !== c.from || (s.undoOf && latest.get(m.id) !== s.undoOf)))
|
|
688
|
+
continue;
|
|
689
|
+
const runtime = m.runtimeType ?? RUNTIME_TYPES.CLAUDE_CODE;
|
|
690
|
+
const modelBefore = effectiveMemberModelId(team, { ...m, runtimeType: runtime });
|
|
691
|
+
// What the member had right now, not when the change was proposed: the
|
|
692
|
+
// record (and the undo built from it) must put back this state.
|
|
693
|
+
const tierBefore = m.tier ?? null;
|
|
694
|
+
const fixedBefore = m.modelId;
|
|
495
695
|
if (c.clearTier)
|
|
496
696
|
delete m.tier;
|
|
497
697
|
else
|
|
498
698
|
m.tier = c.to;
|
|
499
699
|
if (c.clearsModelId && m.modelId === c.clearsModelId)
|
|
500
700
|
delete m.modelId;
|
|
501
|
-
|
|
701
|
+
// Never overwrite a fixed model someone set after the change being reverted.
|
|
702
|
+
if (c.restoreModelId && !m.modelId)
|
|
502
703
|
m.modelId = c.restoreModelId;
|
|
503
|
-
|
|
704
|
+
if (effectiveMemberModelId(team, { ...m, runtimeType: runtime }) !== modelBefore && sessionOf(m))
|
|
705
|
+
modelChanged.push(sessionOf(m));
|
|
706
|
+
const actual = { ...c, from: tierBefore, fromModel: modelBefore };
|
|
707
|
+
delete actual.clearsModelId;
|
|
708
|
+
if (fixedBefore && !m.modelId)
|
|
709
|
+
actual.clearsModelId = fixedBefore;
|
|
710
|
+
const lowered = !c.clearTier && tierRank(c.to) < tierRank(effectiveFromTier(actual, runtime, team.tierModels));
|
|
504
711
|
applied.push({
|
|
505
|
-
...
|
|
712
|
+
...actual,
|
|
506
713
|
appliedAt: now.toISOString(),
|
|
507
714
|
decisionId,
|
|
508
715
|
baseline: baselineBefore(items, memberLedgerKeys(m), now.getTime()),
|
|
509
716
|
guard: lowered && s.kind === 'review' ? 'watching' : 'not_watched',
|
|
510
717
|
});
|
|
511
718
|
}
|
|
719
|
+
// Removal first, so an undo can take its rules out and put back the ones they pushed out.
|
|
720
|
+
if (s.removeRouting?.length) {
|
|
721
|
+
const rules = (team.tierRoutingRules ?? []).filter((r) => !s.removeRouting.includes(r));
|
|
722
|
+
if (rules.length)
|
|
723
|
+
team.tierRoutingRules = rules;
|
|
724
|
+
else
|
|
725
|
+
delete team.tierRoutingRules;
|
|
726
|
+
}
|
|
512
727
|
if (s.routing.length) {
|
|
513
728
|
const rules = [...(team.tierRoutingRules ?? [])];
|
|
514
729
|
for (const r of s.routing)
|
|
@@ -518,10 +733,44 @@ export class ModelTierService {
|
|
|
518
733
|
}
|
|
519
734
|
team.updatedAt = now.toISOString();
|
|
520
735
|
await this.deps.saveTeam(team);
|
|
521
|
-
|
|
522
|
-
this.deps.
|
|
736
|
+
const undone = new Set(s.kind === 'undo' && s.undoOf ? applied.map((a) => a.memberId) : []);
|
|
737
|
+
await this.deps.store.update(s.teamId, (st) => ({
|
|
738
|
+
...st,
|
|
739
|
+
applied: [
|
|
740
|
+
...st.applied.map((a) => (a.decisionId === s.undoOf && undone.has(a.memberId) && a.guard !== 'auto_reverted' ? { ...a, guard: 'undone', guardCheckedAt: now.toISOString() } : a)),
|
|
741
|
+
...applied,
|
|
742
|
+
],
|
|
743
|
+
}));
|
|
744
|
+
this.deps.logger?.info('Model tier changes applied', { teamId: s.teamId, decisionId, kind: s.kind, changes: applied.map((a) => `${a.memberName}:${a.to}`) });
|
|
745
|
+
this.restartIdle(modelChanged);
|
|
523
746
|
return applied;
|
|
524
747
|
}
|
|
748
|
+
/**
|
|
749
|
+
* Restart, one after another in the background, each member whose model
|
|
750
|
+
* changed and that is idle; busy or stopped members pick the model up at
|
|
751
|
+
* their next start.
|
|
752
|
+
*
|
|
753
|
+
* @param sessions - Members whose effective model changed
|
|
754
|
+
*/
|
|
755
|
+
restartIdle(sessions) {
|
|
756
|
+
const restart = this.deps.restartIfIdle;
|
|
757
|
+
if (!restart || !sessions.length)
|
|
758
|
+
return;
|
|
759
|
+
this.restarts = this.restarts.then(async () => {
|
|
760
|
+
for (const session of new Set(sessions)) {
|
|
761
|
+
const outcome = await restart(session).catch(() => 'failed');
|
|
762
|
+
this.deps.logger?.info('Model tier change: restart of an idle member', { session, outcome });
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Resolves once the restarts scheduled so far are done (tests, shutdown).
|
|
768
|
+
*
|
|
769
|
+
* @returns Promise
|
|
770
|
+
*/
|
|
771
|
+
restartsSettled() {
|
|
772
|
+
return this.restarts;
|
|
773
|
+
}
|
|
525
774
|
// -------------------------------------------------------------------- tick
|
|
526
775
|
/**
|
|
527
776
|
* Hourly: start due weekly reviews, send drafts the lead never submitted,
|
|
@@ -579,6 +828,57 @@ export class ModelTierService {
|
|
|
579
828
|
}
|
|
580
829
|
return out;
|
|
581
830
|
}
|
|
831
|
+
/**
|
|
832
|
+
* Auto mode: move a member whose work got worse back at once (the revert
|
|
833
|
+
* the approve-mode card would apply), tell the lead, and post the owner a
|
|
834
|
+
* notice whose one action — "Keep the lower tier" — undoes the move back.
|
|
835
|
+
*
|
|
836
|
+
* @param team - Team
|
|
837
|
+
* @param lead - Its lead (posts the notice)
|
|
838
|
+
* @param a - The watched change
|
|
839
|
+
* @param back - The reverse entry
|
|
840
|
+
* @returns The notice id, or null when it could not be posted / nothing moved
|
|
841
|
+
*/
|
|
842
|
+
async autoRevert(team, lead, a, back) {
|
|
843
|
+
const batch = `TG-${randomUUID().slice(0, 8)}`;
|
|
844
|
+
const subject = {
|
|
845
|
+
teamId: team.id,
|
|
846
|
+
teamName: team.name,
|
|
847
|
+
reviewId: batch,
|
|
848
|
+
kind: 'revert',
|
|
849
|
+
changes: [back],
|
|
850
|
+
routing: [],
|
|
851
|
+
askerSession: sessionOf(lead),
|
|
852
|
+
askerName: lead.name,
|
|
853
|
+
};
|
|
854
|
+
const done = await this.apply(subject, batch);
|
|
855
|
+
if (!done.length)
|
|
856
|
+
return null;
|
|
857
|
+
const forward = {
|
|
858
|
+
memberId: a.memberId,
|
|
859
|
+
memberName: a.memberName,
|
|
860
|
+
session: a.session,
|
|
861
|
+
from: back.clearTier ? null : back.to,
|
|
862
|
+
to: a.to,
|
|
863
|
+
fromModel: back.toModel,
|
|
864
|
+
toModel: a.toModel,
|
|
865
|
+
...(a.clearsModelId ? { clearsModelId: a.clearsModelId } : {}),
|
|
866
|
+
reason: a.reason,
|
|
867
|
+
};
|
|
868
|
+
const undo = { ...subject, kind: 'undo', notice: 'reverted', undoOf: batch, changes: [forward] };
|
|
869
|
+
let id = null;
|
|
870
|
+
try {
|
|
871
|
+
id = (await this.postNotice(await this.team(team.id), lead, undo, done, [], null)).id;
|
|
872
|
+
}
|
|
873
|
+
catch (err) {
|
|
874
|
+
this.deps.logger?.warn('Member moved back, but the owner notice could not be posted', { teamId: team.id, member: a.memberName, error: String(err) });
|
|
875
|
+
}
|
|
876
|
+
const to = tierLabel(back.clearTier ? null : back.to, back.toModel);
|
|
877
|
+
await this.deps
|
|
878
|
+
.deliverToAgent(sessionOf(lead), `[MODEL TIERS] Quality check: ${a.memberName}'s work was sent back more often on ${a.to} (${back.reason}), so it was moved back to ${to}. The owner was told and can keep the lower tier. Do not lower ${a.memberName} again this month unless its work changes. ${RESTART_NOTE}`)
|
|
879
|
+
.catch(() => false);
|
|
880
|
+
return id;
|
|
881
|
+
}
|
|
582
882
|
/**
|
|
583
883
|
* Quality guard for one team: judge every watched (lowered) member and ask
|
|
584
884
|
* the owner to move back the ones whose work got worse.
|
|
@@ -616,21 +916,21 @@ export class ModelTierService {
|
|
|
616
916
|
const lead = getTeamLeads(team)[0];
|
|
617
917
|
if (!lead)
|
|
618
918
|
continue;
|
|
619
|
-
const back = {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
}
|
|
919
|
+
const back = reverseEntry(a, team, `since moving to ${a.to}: ${describeStats(after)}; before: ${describeStats(a.baseline)}`);
|
|
920
|
+
if (leadChangesTiersItself(team)) {
|
|
921
|
+
try {
|
|
922
|
+
const id = await this.autoRevert(team, lead, a, back);
|
|
923
|
+
updates.set(key, { guard: 'auto_reverted', guardDecisionId: id ?? undefined, guardCheckedAt: now.toISOString(), guardAfter: after });
|
|
924
|
+
reverts.push(id ?? `reverted:${m.name}`);
|
|
925
|
+
}
|
|
926
|
+
catch (err) {
|
|
927
|
+
this.deps.logger?.warn('Quality guard could not move a member back', { teamId: team.id, member: m.name, error: String(err) });
|
|
928
|
+
}
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
631
931
|
try {
|
|
632
932
|
const d = await this.askOwner(team, lead, { reviewId: `guard-${a.decisionId}`, changes: [back], routing: [] }, 'revert', [
|
|
633
|
-
`${m.name} was moved to ${a.to}
|
|
933
|
+
`${m.name} was moved to ${a.to}. Since then ${describeStats(after)}; before the change ${describeStats(a.baseline)}.`,
|
|
634
934
|
]);
|
|
635
935
|
updates.set(key, { guard: 'revert_proposed', guardDecisionId: d.id, guardCheckedAt: now.toISOString(), guardAfter: after });
|
|
636
936
|
reverts.push(d.id);
|
|
@@ -651,6 +951,31 @@ export class ModelTierService {
|
|
|
651
951
|
return reverts;
|
|
652
952
|
}
|
|
653
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* The entry that puts a change back: from its target to the tier (and fixed
|
|
956
|
+
* model) the member had before it.
|
|
957
|
+
*
|
|
958
|
+
* @param c - The change to reverse
|
|
959
|
+
* @param team - Team (tier map)
|
|
960
|
+
* @param reason - Why it is put back
|
|
961
|
+
* @returns The reverse entry
|
|
962
|
+
*/
|
|
963
|
+
export function reverseEntry(c, team, reason) {
|
|
964
|
+
const m = team.members.find((x) => x.id === c.memberId);
|
|
965
|
+
const runtime = m?.runtimeType ?? RUNTIME_TYPES.CLAUDE_CODE;
|
|
966
|
+
return {
|
|
967
|
+
memberId: c.memberId,
|
|
968
|
+
memberName: c.memberName,
|
|
969
|
+
session: c.session,
|
|
970
|
+
from: c.to,
|
|
971
|
+
to: c.from ?? effectiveFromTier(c, runtime, team.tierModels),
|
|
972
|
+
...(c.from === null ? { clearTier: true } : {}),
|
|
973
|
+
fromModel: c.toModel,
|
|
974
|
+
toModel: c.fromModel,
|
|
975
|
+
...(c.clearsModelId ? { restoreModelId: c.clearsModelId } : {}),
|
|
976
|
+
reason,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
654
979
|
/** "weak" / "no tier" for the target of a change. */
|
|
655
980
|
function targetLabel(c) {
|
|
656
981
|
return c.clearTier ? 'no tier' : c.to;
|