@pitcher/cli-scripts 3.2.0 → 3.3.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.
- package/dist/src/commands/dsrLanding.js +1 -1
- package/dist/src/commands/roleplay.d.ts +154 -3
- package/dist/src/commands/roleplay.js +276 -23
- package/dist/src/core/dsr-engagement.d.ts +152 -0
- package/dist/src/core/dsr-engagement.js +301 -0
- package/dist/src/core/envelope.d.ts +1 -0
- package/dist/src/core/envelope.js +6 -1
- package/dist/src/core/errors.d.ts +3 -1
- package/dist/src/core/errors.js +2 -2
- package/dist/src/core/http.js +7 -5
- package/dist/src/core/ld.js +2 -1
- package/dist/src/core/me.d.ts +3 -0
- package/dist/src/core/me.js +10 -0
- package/dist/src/core/roleplay-stats.d.ts +48 -7
- package/dist/src/core/roleplay-stats.js +68 -11
- package/dist/src/mcp/apps/confirm/define.d.ts +66 -0
- package/dist/src/mcp/apps/confirm/define.js +208 -0
- package/dist/src/mcp/apps/confirm/hash.d.ts +4 -0
- package/dist/src/mcp/apps/confirm/hash.js +23 -0
- package/dist/src/mcp/apps/confirm/types.d.ts +96 -0
- package/dist/src/mcp/apps/confirm/types.js +15 -0
- package/dist/src/mcp/apps/contentPicker.d.ts +104 -0
- package/dist/src/mcp/apps/contentPicker.js +268 -0
- package/dist/src/mcp/apps/dsrEngagement.d.ts +194 -0
- package/dist/src/mcp/apps/dsrEngagement.js +737 -0
- package/dist/src/mcp/apps/registry.d.ts +14 -0
- package/dist/src/mcp/apps/registry.js +25 -0
- package/dist/src/mcp/apps/roleplayManager.d.ts +228 -0
- package/dist/src/mcp/apps/roleplayManager.js +606 -0
- package/dist/src/mcp/apps/roleplayReps.d.ts +79 -0
- package/dist/src/mcp/apps/roleplayReps.js +328 -0
- package/dist/src/mcp/apps/roleplayWrites.d.ts +6 -0
- package/dist/src/mcp/apps/roleplayWrites.js +509 -0
- package/dist/src/mcp/apps/text.d.ts +41 -0
- package/dist/src/mcp/apps/text.js +58 -0
- package/dist/src/mcp/apps/types.d.ts +23 -0
- package/dist/src/mcp/apps/types.js +12 -0
- package/dist/src/mcp/apps/writeConfirm.d.ts +3 -0
- package/dist/src/mcp/apps/writeConfirm.js +8 -0
- package/dist/src/mcp/groups.d.ts +42 -0
- package/dist/src/mcp/groups.js +89 -0
- package/dist/src/mcp/hosted.d.ts +36 -0
- package/dist/src/mcp/hosted.js +47 -1
- package/dist/src/mcp/index.d.ts +9 -3
- package/dist/src/mcp/index.js +9 -2
- package/dist/src/mcp/prompts/registry.js +8 -7
- package/dist/src/mcp/prompts/types.d.ts +3 -0
- package/dist/src/mcp/registry.d.ts +4 -0
- package/dist/src/mcp/registry.js +29 -24
- package/dist/src/mcp/renderPolicy.d.ts +1 -0
- package/dist/src/mcp/renderPolicy.js +8 -2
- package/dist/src/mcp/resources/registry.js +2 -2
- package/dist/src/mcp/resources/types.d.ts +3 -0
- package/dist/src/mcp/result.d.ts +2 -1
- package/dist/src/mcp/result.js +8 -6
- package/dist/src/mcp/server.d.ts +62 -0
- package/dist/src/mcp/server.js +161 -10
- package/dist/src/mcp/tools/roleplay.js +34 -5
- package/dist/src/mcp/tools/types.d.ts +35 -0
- package/package.json +2 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type Envelope } from '../../core/envelope.js';
|
|
2
|
+
import { type LatestSkill, type RepStatus } from '../../core/roleplay-stats.js';
|
|
3
|
+
import type { ToolDescriptor } from '../tools/types.js';
|
|
4
|
+
import { type AssignmentModel, type Deps, type ScopedResult, type StatusCounts, type ViewOpts } from './roleplayManager.js';
|
|
5
|
+
/**
|
|
6
|
+
* PIT-10116: the Reps tab of the roleplay manager view. Each rep once, across all of their scoped assignments,
|
|
7
|
+
* aggregated from the SAME per-(assignment, rep) rows the Assignment panel shows (`deriveAssignment`), so the two
|
|
8
|
+
* never disagree. Read-only; no rule here decides a status, a session count or a score.
|
|
9
|
+
*/
|
|
10
|
+
export declare const REPS_LIST_PAGE_SIZE = 25;
|
|
11
|
+
export declare const REP_ASSIGNMENTS_MAX = 200;
|
|
12
|
+
/** fitRepAssignments dropped skills to fit: the Skills list is not the whole latest evaluation. */
|
|
13
|
+
export declare const SKILLS_OMITTED = "Some skills were left out to fit";
|
|
14
|
+
/** One rep across all of their scoped assignments. */
|
|
15
|
+
export interface RepSummaryRow {
|
|
16
|
+
userId: string;
|
|
17
|
+
name: string | null;
|
|
18
|
+
status: RepStatus;
|
|
19
|
+
assignments: number;
|
|
20
|
+
passed: number;
|
|
21
|
+
sessions: number;
|
|
22
|
+
bestScore: number | null;
|
|
23
|
+
lastSessionAt: string | null;
|
|
24
|
+
recentOnly: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface RepsPageData extends ScopedResult<'reps'> {
|
|
27
|
+
generatedAt: string;
|
|
28
|
+
status: RepStatus | null;
|
|
29
|
+
page: number;
|
|
30
|
+
pageSize: number;
|
|
31
|
+
totalReps: number;
|
|
32
|
+
filteredReps: number;
|
|
33
|
+
statusCounts: StatusCounts;
|
|
34
|
+
hasMore: boolean;
|
|
35
|
+
reps: RepSummaryRow[];
|
|
36
|
+
canViewInstance: boolean | null;
|
|
37
|
+
}
|
|
38
|
+
export interface RepAssignmentRow {
|
|
39
|
+
assignmentId: string;
|
|
40
|
+
scenarioName: string | null;
|
|
41
|
+
type: string;
|
|
42
|
+
passingScore: number;
|
|
43
|
+
dueDate: string | null;
|
|
44
|
+
active: boolean;
|
|
45
|
+
status: RepStatus;
|
|
46
|
+
sessions: number;
|
|
47
|
+
bestScore: number | null;
|
|
48
|
+
avgScore: number | null;
|
|
49
|
+
lastSessionAt: string | null;
|
|
50
|
+
recentOnly: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface RepAssignmentsData extends ScopedResult<'rep-assignments'> {
|
|
53
|
+
rep: RepSummaryRow;
|
|
54
|
+
status: RepStatus | null;
|
|
55
|
+
assignments: RepAssignmentRow[];
|
|
56
|
+
totalAssignments: number;
|
|
57
|
+
latestSkills: {
|
|
58
|
+
sessionId: string;
|
|
59
|
+
timestamp: string | null;
|
|
60
|
+
skills: LatestSkill[];
|
|
61
|
+
} | null;
|
|
62
|
+
canViewInstance: boolean | null;
|
|
63
|
+
truncated?: string;
|
|
64
|
+
}
|
|
65
|
+
/** Pair rows (`deriveAssignment` per scoped assignment, in D18 order) to one row per user id, most urgent first. */
|
|
66
|
+
export declare function aggregateReps(models: AssignmentModel[]): RepSummaryRow[];
|
|
67
|
+
/** Rows are never dropped (paging continues after them): names are clipped instead, to 200 then 60 characters. */
|
|
68
|
+
export declare function fitRepsPage(data: RepsPageData, max?: number): RepsPageData;
|
|
69
|
+
/** Names (rep, scenarios, skills) to 200 then 60 characters; then skills from the end; then rows, stated. */
|
|
70
|
+
export declare function fitRepAssignments(data: RepAssignmentsData, max?: number): RepAssignmentsData;
|
|
71
|
+
export declare function runRoleplayReps(opts: ViewOpts, deps?: Deps): Promise<Envelope<RepsPageData>>;
|
|
72
|
+
export declare function runRoleplayRepAssignments(opts: ViewOpts, deps?: Deps): Promise<Envelope<RepAssignmentsData>>;
|
|
73
|
+
/** "62", "none", or the whole token "invalid" outside 0..9999. */
|
|
74
|
+
export declare const bestToken: (x: number | null) => string;
|
|
75
|
+
/** A list line, at most 137 B for user ids up to 20 bytes (spec 7.3 item 1.3). */
|
|
76
|
+
export declare const repLine: (r: RepSummaryRow) => string;
|
|
77
|
+
export declare function repsText(raw: unknown): string;
|
|
78
|
+
export declare function repAssignmentsText(raw: unknown): string;
|
|
79
|
+
export declare const roleplayRepsTools: ToolDescriptor[];
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { compareReps, finiteOrNull, loadTeamModel, STATUS_RANK } from '../../commands/roleplay.js';
|
|
3
|
+
import { ok, fail } from '../../core/envelope.js';
|
|
4
|
+
import { createCliError } from '../../core/errors.js';
|
|
5
|
+
import { clientFor, toCliError } from '../../core/http.js';
|
|
6
|
+
import { assignmentSessions, latestSkills } from '../../core/roleplay-stats.js';
|
|
7
|
+
import { cappedSentence, checkCommon, clipChars, deriveAssignment, emptyCounts, fmtNum, fmtUtc, inRange, instanceId, name60, NAME_CHARS, ROLEPLAY_VIEW_MAX_BYTES, roleplayFitText, scoped, scopeEnum, STATUS_TEXT, statusEnum, } from './roleplayManager.js';
|
|
8
|
+
import { byteLen, clipBytes, jsonBytes } from './text.js';
|
|
9
|
+
/**
|
|
10
|
+
* PIT-10116: the Reps tab of the roleplay manager view. Each rep once, across all of their scoped assignments,
|
|
11
|
+
* aggregated from the SAME per-(assignment, rep) rows the Assignment panel shows (`deriveAssignment`), so the two
|
|
12
|
+
* never disagree. Read-only; no rule here decides a status, a session count or a score.
|
|
13
|
+
*/
|
|
14
|
+
export const REPS_LIST_PAGE_SIZE = 25;
|
|
15
|
+
export const REP_ASSIGNMENTS_MAX = 200;
|
|
16
|
+
/** fitRepAssignments dropped skills to fit: the Skills list is not the whole latest evaluation. */
|
|
17
|
+
export const SKILLS_OMITTED = 'Some skills were left out to fit';
|
|
18
|
+
// ── aggregation ──────────────────────────────────────────────────────────────
|
|
19
|
+
const timeOf = (iso) => {
|
|
20
|
+
if (!iso)
|
|
21
|
+
return null;
|
|
22
|
+
const t = Date.parse(iso);
|
|
23
|
+
return Number.isNaN(t) ? null : t;
|
|
24
|
+
};
|
|
25
|
+
/** The later of two timestamps by instant; an unparseable value never wins over a parseable one. */
|
|
26
|
+
function laterOf(a, b) {
|
|
27
|
+
const ta = timeOf(a);
|
|
28
|
+
const tb = timeOf(b);
|
|
29
|
+
if (tb === null)
|
|
30
|
+
return a ?? b;
|
|
31
|
+
if (ta === null)
|
|
32
|
+
return b;
|
|
33
|
+
return tb > ta ? b : a;
|
|
34
|
+
}
|
|
35
|
+
/** Pair rows (`deriveAssignment` per scoped assignment, in D18 order) to one row per user id, most urgent first. */
|
|
36
|
+
export function aggregateReps(models) {
|
|
37
|
+
const byId = new Map();
|
|
38
|
+
for (const m of models) {
|
|
39
|
+
for (const r of m.reps) {
|
|
40
|
+
const cur = byId.get(r.userId);
|
|
41
|
+
if (!cur) {
|
|
42
|
+
byId.set(r.userId, {
|
|
43
|
+
userId: r.userId,
|
|
44
|
+
name: r.name,
|
|
45
|
+
status: r.status,
|
|
46
|
+
assignments: 1,
|
|
47
|
+
passed: r.status === 'passed' ? 1 : 0,
|
|
48
|
+
sessions: r.sessions,
|
|
49
|
+
bestScore: finiteOrNull(r.bestScore),
|
|
50
|
+
lastSessionAt: r.lastSessionAt,
|
|
51
|
+
recentOnly: r.recentOnly,
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
cur.name ?? (cur.name = r.name);
|
|
56
|
+
if (STATUS_RANK[r.status] < STATUS_RANK[cur.status])
|
|
57
|
+
cur.status = r.status;
|
|
58
|
+
cur.assignments += 1;
|
|
59
|
+
if (r.status === 'passed')
|
|
60
|
+
cur.passed += 1;
|
|
61
|
+
cur.sessions += r.sessions;
|
|
62
|
+
const best = finiteOrNull(r.bestScore);
|
|
63
|
+
if (best !== null && (cur.bestScore === null || best > cur.bestScore))
|
|
64
|
+
cur.bestScore = best;
|
|
65
|
+
cur.lastSessionAt = laterOf(cur.lastSessionAt, r.lastSessionAt);
|
|
66
|
+
cur.recentOnly || (cur.recentOnly = r.recentOnly);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return Array.from(byId.values()).sort((a, b) => compareReps({ status: a.status, name: a.name, user_id: a.userId }, { status: b.status, name: b.name, user_id: b.userId }));
|
|
70
|
+
}
|
|
71
|
+
const canViewInstanceOf = (scope, model) => scope === 'coach' ? null : model.scope === 'instance';
|
|
72
|
+
// ── fitting ──────────────────────────────────────────────────────────────────
|
|
73
|
+
/** Rows are never dropped (paging continues after them): names are clipped instead, to 200 then 60 characters. */
|
|
74
|
+
export function fitRepsPage(data, max = ROLEPLAY_VIEW_MAX_BYTES) {
|
|
75
|
+
let out = data;
|
|
76
|
+
for (const n of [NAME_CHARS, 60]) {
|
|
77
|
+
if (jsonBytes(out) <= max)
|
|
78
|
+
return out;
|
|
79
|
+
out = { ...out, reps: out.reps.map((r) => ({ ...r, name: r.name === null ? null : clipChars(r.name, n) })) };
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/** Names (rep, scenarios, skills) to 200 then 60 characters; then skills from the end; then rows, stated. */
|
|
84
|
+
export function fitRepAssignments(data, max = ROLEPLAY_VIEW_MAX_BYTES) {
|
|
85
|
+
let out = data;
|
|
86
|
+
for (const n of [NAME_CHARS, 60]) {
|
|
87
|
+
if (jsonBytes(out) <= max)
|
|
88
|
+
return out;
|
|
89
|
+
out = {
|
|
90
|
+
...out,
|
|
91
|
+
rep: { ...out.rep, name: out.rep.name === null ? null : clipChars(out.rep.name, n) },
|
|
92
|
+
assignments: out.assignments.map((a) => ({ ...a, scenarioName: a.scenarioName === null ? null : clipChars(a.scenarioName, n) })),
|
|
93
|
+
latestSkills: out.latestSkills && {
|
|
94
|
+
...out.latestSkills,
|
|
95
|
+
skills: out.latestSkills.skills.map((k) => ({ ...k, title: clipChars(k.title, n), skillId: clipChars(k.skillId, n) })),
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Dropped skills are stated, as fitScorecard does: the remaining ones are not the whole latest evaluation.
|
|
100
|
+
const skillsBefore = out.latestSkills?.skills.length ?? 0;
|
|
101
|
+
const skillsNote = (o) => ((o.latestSkills?.skills.length ?? 0) < skillsBefore ? SKILLS_OMITTED : null);
|
|
102
|
+
while (out.latestSkills?.skills.length && jsonBytes({ ...out, truncated: SKILLS_OMITTED }) > max) {
|
|
103
|
+
out = { ...out, latestSkills: { ...out.latestSkills, skills: out.latestSkills.skills.slice(0, -1) } };
|
|
104
|
+
}
|
|
105
|
+
const noted = (rows) => {
|
|
106
|
+
const { truncated: _old, ...rest } = out;
|
|
107
|
+
const notes = [rows.length < data.totalAssignments ? `Showing ${rows.length} of ${data.totalAssignments} assignments` : null, skillsNote(out)];
|
|
108
|
+
const truncated = notes.filter(Boolean).join('; ');
|
|
109
|
+
return truncated ? { ...rest, assignments: rows, truncated } : { ...rest, assignments: rows };
|
|
110
|
+
};
|
|
111
|
+
let rows = out.assignments;
|
|
112
|
+
while (rows.length && jsonBytes(noted(rows)) > max)
|
|
113
|
+
rows = rows.slice(0, -1);
|
|
114
|
+
return noted(rows);
|
|
115
|
+
}
|
|
116
|
+
// ── runners ──────────────────────────────────────────────────────────────────
|
|
117
|
+
export async function runRoleplayReps(opts, deps = {}) {
|
|
118
|
+
try {
|
|
119
|
+
const { instanceId: iid, scope, status } = checkCommon(opts);
|
|
120
|
+
const page = opts.page ?? 1;
|
|
121
|
+
if (!Number.isInteger(page) || page < 1)
|
|
122
|
+
throw createCliError('VALIDATION', 'page must be an integer from 1');
|
|
123
|
+
const model = await loadTeamModel(clientFor(opts, deps), iid, scope);
|
|
124
|
+
const now = new Date();
|
|
125
|
+
const all = aggregateReps(model.assignments.map((a) => deriveAssignment(model, a, now)));
|
|
126
|
+
const statusCounts = emptyCounts();
|
|
127
|
+
for (const r of all)
|
|
128
|
+
statusCounts[r.status] += 1;
|
|
129
|
+
const filtered = status ? all.filter((r) => r.status === status) : all;
|
|
130
|
+
const start = (page - 1) * REPS_LIST_PAGE_SIZE;
|
|
131
|
+
const reps = filtered.slice(start, start + REPS_LIST_PAGE_SIZE);
|
|
132
|
+
return ok(fitRepsPage({
|
|
133
|
+
...scoped('reps', iid, model),
|
|
134
|
+
generatedAt: now.toISOString(),
|
|
135
|
+
status,
|
|
136
|
+
page,
|
|
137
|
+
pageSize: REPS_LIST_PAGE_SIZE,
|
|
138
|
+
totalReps: all.length,
|
|
139
|
+
filteredReps: filtered.length,
|
|
140
|
+
statusCounts,
|
|
141
|
+
hasMore: start + reps.length < filtered.length,
|
|
142
|
+
reps,
|
|
143
|
+
canViewInstance: canViewInstanceOf(scope, model),
|
|
144
|
+
}), model.warnings);
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
return fail(toCliError(e));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export async function runRoleplayRepAssignments(opts, deps = {}) {
|
|
151
|
+
try {
|
|
152
|
+
const { instanceId: iid, scope, status } = checkCommon(opts);
|
|
153
|
+
if (!opts.userId)
|
|
154
|
+
throw createCliError('VALIDATION', 'userId is required');
|
|
155
|
+
const userId = opts.userId;
|
|
156
|
+
const model = await loadTeamModel(clientFor(opts, deps), iid, scope);
|
|
157
|
+
const now = new Date();
|
|
158
|
+
const mine = model.assignments.filter((a) => (a.resolvedUserIds ?? []).includes(userId));
|
|
159
|
+
if (mine.length === 0) {
|
|
160
|
+
throw createCliError('NOT_FOUND', `No rep ${userId} on roleplay assignments in this ${model.scope === 'instance' ? 'instance' : 'coaching scope'}`);
|
|
161
|
+
}
|
|
162
|
+
const models = mine.map((a) => deriveAssignment(model, a, now));
|
|
163
|
+
// Only this rep's pairs: the models also hold every other rep on these assignments.
|
|
164
|
+
const rep = aggregateReps(models.map((m) => ({ ...m, reps: m.reps.filter((x) => x.userId === userId) })))[0];
|
|
165
|
+
const rows = models
|
|
166
|
+
.map((m, pos) => ({ m, pos, r: m.reps.find((x) => x.userId === userId) }))
|
|
167
|
+
.sort((x, y) => STATUS_RANK[x.r.status] - STATUS_RANK[y.r.status] || x.pos - y.pos)
|
|
168
|
+
.map(({ m, r }) => ({
|
|
169
|
+
assignmentId: m.assignment.id,
|
|
170
|
+
scenarioName: m.assignment.scenarioName ?? null,
|
|
171
|
+
type: m.assignment.type,
|
|
172
|
+
passingScore: m.assignment.passingScore,
|
|
173
|
+
dueDate: m.assignment.dueDate ?? null,
|
|
174
|
+
active: m.assignment.active !== false,
|
|
175
|
+
status: r.status,
|
|
176
|
+
sessions: r.sessions,
|
|
177
|
+
bestScore: r.bestScore,
|
|
178
|
+
avgScore: r.avgScore,
|
|
179
|
+
lastSessionAt: r.lastSessionAt,
|
|
180
|
+
recentOnly: r.recentOnly,
|
|
181
|
+
}));
|
|
182
|
+
const shown = status ? rows.filter((r) => r.status === status) : rows;
|
|
183
|
+
// T12: the newest evaluated session among the rep's sessions on THEIR scoped assignments only.
|
|
184
|
+
const own = mine.flatMap((a) => assignmentSessions(model.sessions, a)).filter((s) => s.userId === userId);
|
|
185
|
+
const latest = latestSkills(own);
|
|
186
|
+
const capped = shown.slice(0, REP_ASSIGNMENTS_MAX);
|
|
187
|
+
return ok(fitRepAssignments({
|
|
188
|
+
...scoped('rep-assignments', iid, model),
|
|
189
|
+
rep,
|
|
190
|
+
status,
|
|
191
|
+
assignments: capped,
|
|
192
|
+
totalAssignments: shown.length,
|
|
193
|
+
latestSkills: latest
|
|
194
|
+
? { sessionId: String(latest.session.id), timestamp: latest.session.timestamp ?? null, skills: latest.skills }
|
|
195
|
+
: null,
|
|
196
|
+
canViewInstance: canViewInstanceOf(scope, model),
|
|
197
|
+
...(capped.length < shown.length ? { truncated: `Showing ${capped.length} of ${shown.length} assignments` } : {}),
|
|
198
|
+
}), model.warnings);
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
return fail(toCliError(e));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// ── text (spec 7.3) ──────────────────────────────────────────────────────────
|
|
205
|
+
/** "62", "none", or the whole token "invalid" outside 0..9999. */
|
|
206
|
+
export const bestToken = (x) => (x === null ? 'none' : inRange(x) ? fmtNum(x) : 'invalid');
|
|
207
|
+
const dateToken = (ts) => (ts && timeOf(ts) !== null ? fmtUtc(ts).slice(0, 10) : 'none');
|
|
208
|
+
const scopeText = (d) => d.scope === 'coach' ? 'coach scope; counts cover assignments you coach only' : 'instance scope';
|
|
209
|
+
/** A list line, at most 137 B for user ids up to 20 bytes (spec 7.3 item 1.3). */
|
|
210
|
+
export const repLine = (r) => `${clipBytes(r.name || 'Rep', 32)} (${r.userId}): ${STATUS_TEXT[r.status]}, ${r.passed}/${r.assignments} passed, ` +
|
|
211
|
+
`best ${bestToken(r.bestScore)}, ${r.sessions} sessions, last ${dateToken(r.lastSessionAt)}`;
|
|
212
|
+
export function repsText(raw) {
|
|
213
|
+
const d = raw;
|
|
214
|
+
const c = d.statusCounts;
|
|
215
|
+
const first = (d.page - 1) * d.pageSize + 1;
|
|
216
|
+
const range = d.reps.length ? `Reps ${first}-${first + d.reps.length - 1} of ${d.filteredReps}` : `No reps of ${d.filteredReps}`;
|
|
217
|
+
const head1 = `AI Roleplay reps (${scopeText(d)}): ${d.totalReps} reps; ${c.overdue} overdue, ${c.needs_coaching} needs coaching, ` +
|
|
218
|
+
`${c.in_progress} in progress, ${c.not_started} not started, ${c.passed} all passed. ` +
|
|
219
|
+
`${range} (status: ${d.status ? STATUS_TEXT[d.status] : 'all'}), most urgent first. ` +
|
|
220
|
+
'Line: name (userId): status, passed/assignments, best, sessions summed over assignments, last session (UTC).';
|
|
221
|
+
const hidden = Math.max(0, d.filteredReps - (first - 1) - d.reps.length);
|
|
222
|
+
return roleplayFitText({
|
|
223
|
+
heads: [
|
|
224
|
+
{ text: head1, maxBytes: 385 },
|
|
225
|
+
{ text: d.complete ? '' : cappedSentence(d.coverage), maxBytes: 120 },
|
|
226
|
+
],
|
|
227
|
+
body: d.reps.map((r) => ({ text: repLine(r) })),
|
|
228
|
+
hidden,
|
|
229
|
+
more: ({ lines, hidden: h }) => lines > 0
|
|
230
|
+
? `...${lines + h} more reps; this page was shortened, ask with a status filter`
|
|
231
|
+
: `...${h} more reps: pass page=${d.page + 1}${d.status ? ` with status=${d.status}` : ''}${d.scope === 'coach' ? ' and scope=coach' : ''}`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const STATUS_ORDER = ['overdue', 'needs_coaching', 'in_progress', 'not_started', 'passed'];
|
|
235
|
+
export function repAssignmentsText(raw) {
|
|
236
|
+
const d = raw;
|
|
237
|
+
const r = d.rep;
|
|
238
|
+
const head1 = `${name60(r.name, 'Rep')} (${r.userId}): ${r.passed} of ${r.assignments} assignments passed` +
|
|
239
|
+
`${d.scope === 'coach' ? ' (coach scope: assignments you coach only)' : ''}, most urgent ${STATUS_TEXT[r.status]}, ` +
|
|
240
|
+
`${r.sessions} sessions across assignments, best ${bestToken(r.bestScore)}, last ${dateToken(r.lastSessionAt)}` +
|
|
241
|
+
`${d.status ? `; listing status ${STATUS_TEXT[d.status]} only` : ''}. ` +
|
|
242
|
+
(d.status && d.assignments.length === 0
|
|
243
|
+
? `No assignment of this rep has status ${STATUS_TEXT[d.status]}.`
|
|
244
|
+
: 'Lines: assignmentId scenario: status, best of pass, sessions, last (UTC); open one with pitcher_roleplay_rep_sessions.');
|
|
245
|
+
const skills = (d.latestSkills?.skills ?? []).slice(0, 8).map((k) => `${clipBytes(k.title, 30)} ${fmtNum(k.score)}`);
|
|
246
|
+
let head2 = '';
|
|
247
|
+
for (let n = skills.length; n > 0; n -= 1) {
|
|
248
|
+
const t = `Latest skills: ${skills.slice(0, n).join(', ')}.`;
|
|
249
|
+
if (byteLen(t) <= 450) {
|
|
250
|
+
head2 = t;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const body = d.assignments.map((a) => ({
|
|
255
|
+
kind: a.status,
|
|
256
|
+
text: `${a.assignmentId} ${clipBytes(a.scenarioName || a.assignmentId, 40)}: ${STATUS_TEXT[a.status]}, best ${bestToken(a.bestScore)} ` +
|
|
257
|
+
`of pass ${bestToken(a.passingScore)}, ${a.sessions} sessions, last ${dateToken(a.lastSessionAt)}` +
|
|
258
|
+
`${a.dueDate ? `, due ${a.dueDate.slice(0, 10)}` : ''}${a.active ? '' : ', paused'}`,
|
|
259
|
+
}));
|
|
260
|
+
const hidden = Math.max(0, d.totalAssignments - d.assignments.length);
|
|
261
|
+
// Rows are in rank order, so hidden rows can only have the last sent row's status or a later one.
|
|
262
|
+
const last = d.assignments[d.assignments.length - 1]?.status;
|
|
263
|
+
const hiddenStatuses = hidden > 0 ? STATUS_ORDER.filter((s) => !last || STATUS_RANK[s] >= STATUS_RANK[last]) : [];
|
|
264
|
+
return roleplayFitText({
|
|
265
|
+
heads: [
|
|
266
|
+
{ text: head1, maxBytes: 450 },
|
|
267
|
+
{ text: head2, maxBytes: 450 },
|
|
268
|
+
{ text: d.complete ? '' : cappedSentence(d.coverage), maxBytes: 200 },
|
|
269
|
+
{ text: d.truncated ? `${d.truncated}.` : '', maxBytes: 120 },
|
|
270
|
+
],
|
|
271
|
+
body,
|
|
272
|
+
hidden,
|
|
273
|
+
more: ({ lines, hidden: h, kinds }) => {
|
|
274
|
+
// Filtered: every row left out has that status, so no other call reaches them (spec 7.3 item 2.5, stated limit).
|
|
275
|
+
if (d.status)
|
|
276
|
+
return `...${lines + h} more with status ${d.status} not listed`;
|
|
277
|
+
const left = STATUS_ORDER.filter((s) => kinds.includes(s) || hiddenStatuses.includes(s));
|
|
278
|
+
return `...${lines + h} more (least urgent): call with status=${left.join('|')}`;
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// ── tools ────────────────────────────────────────────────────────────────────
|
|
283
|
+
const VIEW = 'roleplay-manager';
|
|
284
|
+
export const roleplayRepsTools = [
|
|
285
|
+
{
|
|
286
|
+
name: 'pitcher_roleplay_reps',
|
|
287
|
+
title: 'AI Roleplay reps (interactive)',
|
|
288
|
+
description: 'Each rep in the AI Roleplay team view, once, across all of their roleplay assignments: assignments passed (x of y), ' +
|
|
289
|
+
'the most urgent status across those assignments, sessions summed across those assignments (a session can count ' +
|
|
290
|
+
'toward two), best score and last session, most urgent first, ' +
|
|
291
|
+
'25 reps a page with a status filter. Admins see every rep on an instance assignment; a coaching manager the reps ' +
|
|
292
|
+
'on the assignments they coach, with only those assignments counted. In hosts with UI it opens the roleplay team ' +
|
|
293
|
+
"view on its Reps tab. For one rep's assignments use pitcher_roleplay_rep_assignments with the userId.",
|
|
294
|
+
inputShape: {
|
|
295
|
+
instanceId,
|
|
296
|
+
status: statusEnum.optional().describe('Only reps whose most urgent status is this'),
|
|
297
|
+
page: z.number().int().min(1).optional().describe('1-based, 25 reps a page'),
|
|
298
|
+
scope: scopeEnum.optional().describe('Omit unless the user asks only for the reps they coach (then "coach")'),
|
|
299
|
+
},
|
|
300
|
+
run: runRoleplayReps,
|
|
301
|
+
toText: repsText,
|
|
302
|
+
readOnly: true,
|
|
303
|
+
access: 'read', // PIT-10114: explicit for every app tool
|
|
304
|
+
group: 'roleplay',
|
|
305
|
+
ui: { view: VIEW },
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: 'pitcher_roleplay_rep_assignments',
|
|
309
|
+
title: 'AI Roleplay assignments of one rep',
|
|
310
|
+
description: "One rep's AI Roleplay assignments in the team view: per assignment its status, best score, sessions and last " +
|
|
311
|
+
"session, most urgent first, plus the rep's latest skill scores. Works for the rep's coach (only the assignments " +
|
|
312
|
+
'they coach) and for instance or org admins. Take userId from pitcher_roleplay_reps or pitcher_roleplay_team. For ' +
|
|
313
|
+
'the sessions of one assignment use pitcher_roleplay_rep_sessions. In hosts with UI it opens the roleplay team view ' +
|
|
314
|
+
'on that rep.',
|
|
315
|
+
inputShape: {
|
|
316
|
+
instanceId,
|
|
317
|
+
userId: z.string().describe('Rep user ID (userId from pitcher_roleplay_reps or user_id from pitcher_roleplay_team)'),
|
|
318
|
+
status: statusEnum.optional().describe("Only this rep's assignments with this status"),
|
|
319
|
+
scope: scopeEnum.optional().describe('Omit unless the user asks only for the reps they coach (then "coach")'),
|
|
320
|
+
},
|
|
321
|
+
run: runRoleplayRepAssignments,
|
|
322
|
+
toText: repAssignmentsText,
|
|
323
|
+
readOnly: true,
|
|
324
|
+
access: 'read', // PIT-10114: explicit for every app tool
|
|
325
|
+
group: 'roleplay',
|
|
326
|
+
ui: { view: VIEW },
|
|
327
|
+
},
|
|
328
|
+
];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ToolDescriptor } from '../tools/types.js';
|
|
2
|
+
export declare const commentPropose: ToolDescriptor, commentCommit: ToolDescriptor;
|
|
3
|
+
export declare const commentDeletePropose: ToolDescriptor, commentDeleteCommit: ToolDescriptor;
|
|
4
|
+
export declare const pathPropose: ToolDescriptor, pathCommit: ToolDescriptor;
|
|
5
|
+
/** The six confirm tools (PIT-10113), registered in `apps/registry.ts` with the write-confirm view. */
|
|
6
|
+
export declare const roleplayWriteTools: ToolDescriptor[];
|