@app-vitals/agent-admin 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/e2e/test-server.ts +5 -5
  2. package/openapi.json +12 -19
  3. package/openapi.platform.json +11 -18
  4. package/package.json +2 -2
  5. package/prisma/client/internal/class.ts +4 -4
  6. package/prisma/client/internal/prismaNamespace.ts +0 -1
  7. package/prisma/client/internal/prismaNamespaceBrowser.ts +0 -1
  8. package/prisma/client/models/Agent.ts +62 -137
  9. package/prisma/migrations/20261030000000_drop_agent_type_name/migration.sql +8 -0
  10. package/prisma/schema/schema.prisma +3 -4
  11. package/src/account-lifecycle.integration.test.ts +1 -1
  12. package/src/account-trial-warning-sweeper.integration.test.ts +1 -1
  13. package/src/admin-local-agent.smoke.test.ts +12 -12
  14. package/src/admin-ui-pages.ts +2 -2
  15. package/src/admin-ui-pages.unit.test.ts +2 -2
  16. package/src/admin-ui.fixtures.ts +6 -6
  17. package/src/admin-ui.smoke.test.ts +39 -39
  18. package/src/admin-ui.ts +8 -8
  19. package/src/agent-cron-jobs.integration.test.ts +8 -7
  20. package/src/agent-cron-jobs.ts +4 -5
  21. package/src/agent-cron-jobs.unit.test.ts +23 -23
  22. package/src/agent-patch-author-allowlist.integration.test.ts +4 -4
  23. package/src/agent-specialty-backfill.integration.test.ts +126 -0
  24. package/src/agents-api.integration.test.ts +20 -20
  25. package/src/agents-api.smoke.test.ts +76 -77
  26. package/src/agents-api.ts +9 -14
  27. package/src/agents.integration.test.ts +40 -53
  28. package/src/agents.ts +33 -46
  29. package/src/agents.unit.test.ts +28 -35
  30. package/src/api.smoke.test.ts +24 -2
  31. package/src/api.ts +15 -16
  32. package/src/openapi-schemas.ts +14 -7
  33. package/src/slack-provisioning-service.integration.test.ts +2 -2
  34. package/src/slack-provisioning.integration.test.ts +2 -2
  35. package/src/specialty-manifest-loader.ts +32 -32
  36. package/src/specialty-manifest-loader.unit.test.ts +8 -8
  37. package/src/startup-policy-reconcile.unit.test.ts +1 -1
  38. package/src/trial-expiry-sweeper.integration.test.ts +1 -1
  39. package/src/trial-expiry-warning-sweeper.integration.test.ts +1 -1
  40. package/src/agent-specialty.ts +0 -22
  41. package/src/agent-specialty.unit.test.ts +0 -43
@@ -29,7 +29,7 @@ interface FakeAgentRow {
29
29
  reviewAuthorAllowlist: string[];
30
30
  patchAuthorAllowlist: string[];
31
31
  restrictSlackToMembers: boolean;
32
- typeName: string;
32
+ specialty: string;
33
33
  createdAt: Date;
34
34
  updatedAt: Date;
35
35
  // APM-1.3: the six real agent-policy fields (APM-1.1 DB columns).
@@ -93,7 +93,7 @@ function makeFakePrisma(
93
93
  reviewAuthorAllowlist: data.reviewAuthorAllowlist ?? [],
94
94
  patchAuthorAllowlist: data.patchAuthorAllowlist ?? [],
95
95
  restrictSlackToMembers: data.restrictSlackToMembers ?? false,
96
- typeName: "coding",
96
+ specialty: "coding",
97
97
  createdAt: new Date("2024-01-01"),
98
98
  updatedAt: new Date("2024-01-01"),
99
99
  autoPostReviews: true,
@@ -257,8 +257,8 @@ function fakeRegistry(
257
257
  requiredEnvByType: Record<string, string[]>,
258
258
  ): SpecialtyManifestResolver {
259
259
  return {
260
- getManifest(typeName: string): SpecialtyManifest {
261
- const required = requiredEnvByType[typeName] ?? [];
260
+ getManifest(specialty: string): SpecialtyManifest {
261
+ const required = requiredEnvByType[specialty] ?? [];
262
262
  return {
263
263
  env: {
264
264
  required: required.map((key) => ({
@@ -270,9 +270,9 @@ function fakeRegistry(
270
270
  },
271
271
  } as unknown as SpecialtyManifest;
272
272
  },
273
- tryGetManifest(typeName: string): SpecialtyManifest | undefined {
274
- if (!(typeName in requiredEnvByType)) return undefined;
275
- return this.getManifest(typeName);
273
+ tryGetManifest(specialty: string): SpecialtyManifest | undefined {
274
+ if (!(specialty in requiredEnvByType)) return undefined;
275
+ return this.getManifest(specialty);
276
276
  },
277
277
  listTypes() {
278
278
  return Object.keys(requiredEnvByType).map((name) => ({
@@ -293,7 +293,7 @@ function seedRow(overrides: Partial<FakeAgentRow> = {}): FakeAgentRow {
293
293
  reviewAuthorAllowlist: [],
294
294
  patchAuthorAllowlist: [],
295
295
  restrictSlackToMembers: false,
296
- typeName: "coding",
296
+ specialty: "coding",
297
297
  createdAt: new Date("2024-01-01"),
298
298
  updatedAt: new Date("2024-01-01"),
299
299
  autoPostReviews: true,
@@ -399,7 +399,7 @@ describe("AgentService.delete", () => {
399
399
  // ─── list ───────────────────────────────────────────────────────────────────
400
400
 
401
401
  describe("AgentService.list", () => {
402
- it("returns id/name/selfHosted/typeName for all agents, ordered by name asc", async () => {
402
+ it("returns id/name/selfHosted/specialty for all agents, ordered by name asc", async () => {
403
403
  const prisma = makeFakePrisma([
404
404
  seedRow({ id: "a1", name: "Zeta", selfHosted: false }),
405
405
  seedRow({ id: "a2", name: "Alpha", selfHosted: true }),
@@ -413,14 +413,12 @@ describe("AgentService.list", () => {
413
413
  id: "a2",
414
414
  name: "Alpha",
415
415
  selfHosted: true,
416
- typeName: "coding",
417
416
  specialty: "coding",
418
417
  },
419
418
  {
420
419
  id: "a1",
421
420
  name: "Zeta",
422
421
  selfHosted: false,
423
- typeName: "coding",
424
422
  specialty: "coding",
425
423
  },
426
424
  ]);
@@ -437,7 +435,7 @@ describe("AgentService.list", () => {
437
435
  // ─── getSummary ─────────────────────────────────────────────────────────────
438
436
 
439
437
  describe("AgentService.getSummary", () => {
440
- it("returns {id, name, selfHosted, typeName} for an existing agent", async () => {
438
+ it("returns {id, name, selfHosted, specialty} for an existing agent", async () => {
441
439
  const row = seedRow({ id: "a1", name: "Existing", selfHosted: true });
442
440
  const prisma = makeFakePrisma([row]) as unknown as FakePrisma;
443
441
  const service = new AgentService(prisma as never);
@@ -446,7 +444,6 @@ describe("AgentService.getSummary", () => {
446
444
  id: "a1",
447
445
  name: "Existing",
448
446
  selfHosted: true,
449
- typeName: "coding",
450
447
  specialty: "coding",
451
448
  });
452
449
  });
@@ -462,7 +459,7 @@ describe("AgentService.getSummary", () => {
462
459
  // ─── getDetail ──────────────────────────────────────────────────────────────
463
460
 
464
461
  describe("AgentService.getDetail", () => {
465
- it("returns the full record including repos, typeName, timestamps, and an empty missingRequiredEnv when the type has no required contract", async () => {
462
+ it("returns the full record including repos, specialty, timestamps, and an empty missingRequiredEnv when the type has no required contract", async () => {
466
463
  const row = seedRow({
467
464
  id: "a1",
468
465
  name: "Existing",
@@ -487,7 +484,6 @@ describe("AgentService.getDetail", () => {
487
484
  reviewAuthorAllowlist: ["octocat"],
488
485
  patchAuthorAllowlist: [],
489
486
  restrictSlackToMembers: false,
490
- typeName: "coding",
491
487
  specialty: "coding",
492
488
  createdAt: row.createdAt,
493
489
  updatedAt: row.updatedAt,
@@ -527,7 +523,7 @@ describe("AgentService.getDetail", () => {
527
523
  expect(detail?.cleanupAfterDays).toBe(30);
528
524
  });
529
525
 
530
- it("defaults typeName to 'coding' when the row was seeded without an explicit value", async () => {
526
+ it("defaults specialty to 'coding' when the row was seeded without an explicit value", async () => {
531
527
  const row = seedRow({ id: "a1" });
532
528
  const prisma = makeFakePrisma([row]) as unknown as FakePrisma;
533
529
  const service = new AgentService(
@@ -537,7 +533,7 @@ describe("AgentService.getDetail", () => {
537
533
 
538
534
  const detail = await service.getDetail("a1");
539
535
 
540
- expect(detail?.typeName).toBe("coding");
536
+ expect(detail?.specialty).toBe("coding");
541
537
  });
542
538
 
543
539
  it("returns null when the agent does not exist", async () => {
@@ -548,7 +544,7 @@ describe("AgentService.getDetail", () => {
548
544
  });
549
545
 
550
546
  it("lists a required env key in missingRequiredEnv when no AgentEnv row exists for it", async () => {
551
- const row = seedRow({ id: "a1", typeName: "coding" });
547
+ const row = seedRow({ id: "a1", specialty: "coding" });
552
548
  const prisma = makeFakePrisma([row]) as unknown as FakePrisma;
553
549
  const service = new AgentService(
554
550
  prisma as never,
@@ -561,7 +557,7 @@ describe("AgentService.getDetail", () => {
561
557
  });
562
558
 
563
559
  it("clears a required key from missingRequiredEnv once its AgentEnv row is seeded", async () => {
564
- const row = seedRow({ id: "a1", typeName: "coding" });
560
+ const row = seedRow({ id: "a1", specialty: "coding" });
565
561
  const prisma = makeFakePrisma([row], {
566
562
  a1: ["CLAUDE_CODE_OAUTH_TOKEN"],
567
563
  }) as unknown as FakePrisma;
@@ -576,7 +572,7 @@ describe("AgentService.getDetail", () => {
576
572
  });
577
573
 
578
574
  it("returns an empty missingRequiredEnv array (not undefined/null) when the manifest's required contract is empty, without querying AgentEnv", async () => {
579
- const row = seedRow({ id: "a1", typeName: "coding" });
575
+ const row = seedRow({ id: "a1", specialty: "coding" });
580
576
  const prisma = makeFakePrisma([row]) as unknown as FakePrisma;
581
577
  let findManyCalled = false;
582
578
  const originalFindMany = prisma.agentEnv.findMany.bind(prisma.agentEnv);
@@ -597,7 +593,7 @@ describe("AgentService.getDetail", () => {
597
593
  });
598
594
 
599
595
  it("never includes AgentEnv values in the computed result — only key names are read", async () => {
600
- const row = seedRow({ id: "a1", typeName: "coding" });
596
+ const row = seedRow({ id: "a1", specialty: "coding" });
601
597
  const prisma = makeFakePrisma([row], {
602
598
  a1: ["CLAUDE_CODE_OAUTH_TOKEN"],
603
599
  }) as unknown as FakePrisma;
@@ -747,8 +743,7 @@ describe("AgentService.getById", () => {
747
743
  restrictSlackToMembers: false,
748
744
  memberEmails: [],
749
745
  accountStatus: null,
750
- // ARD-6.1: getById() now selects typeName for the runtime config response.
751
- typeName: "coding",
746
+ // ARD-6.1: getById() now selects specialty for the runtime config response.
752
747
  specialty: "coding",
753
748
  selfHosted: false,
754
749
  // APM-1.5: getById() now selects these 6 policy fields too.
@@ -785,8 +780,7 @@ describe("AgentService.getById", () => {
785
780
  restrictSlackToMembers: false,
786
781
  memberEmails: [],
787
782
  accountStatus: null,
788
- // ARD-6.1: getById() now selects typeName for the runtime config response.
789
- typeName: "coding",
783
+ // ARD-6.1: getById() now selects specialty for the runtime config response.
790
784
  specialty: "coding",
791
785
  selfHosted: false,
792
786
  // APM-1.5: getById() now selects these 6 policy fields too.
@@ -1055,7 +1049,6 @@ describe("AgentService.updateFields", () => {
1055
1049
  reviewAuthorAllowlist: ["octocat"],
1056
1050
  patchAuthorAllowlist: [],
1057
1051
  restrictSlackToMembers: false,
1058
- typeName: "coding",
1059
1052
  specialty: "coding",
1060
1053
  createdAt: row.createdAt,
1061
1054
  updatedAt: new Date("2024-01-02"),
@@ -1151,14 +1144,14 @@ function fakeCreateSpecialtyRegistry(
1151
1144
  byType: Record<string, SpecialtyManifest> = { coding: CODING_MANIFEST },
1152
1145
  ): Pick<SpecialtyManifestResolver, "tryGetManifest"> {
1153
1146
  return {
1154
- tryGetManifest: (typeName: string) => byType[typeName],
1147
+ tryGetManifest: (specialty: string) => byType[specialty],
1155
1148
  };
1156
1149
  }
1157
1150
 
1158
1151
  interface CreateAgentHarnessCalls {
1159
1152
  created: Array<{
1160
1153
  name: string;
1161
- typeName?: string;
1154
+ specialty?: string;
1162
1155
  selfHosted?: boolean;
1163
1156
  }>;
1164
1157
  deleted: string[];
@@ -1211,7 +1204,7 @@ function makeCreateAgentHarness(
1211
1204
  reviewAuthorAllowlist: input.reviewAuthorAllowlist ?? [],
1212
1205
  patchAuthorAllowlist: input.patchAuthorAllowlist ?? [],
1213
1206
  restrictSlackToMembers: input.restrictSlackToMembers ?? false,
1214
- typeName: input.typeName ?? "coding",
1207
+ specialty: input.specialty ?? "coding",
1215
1208
  createdAt: new Date("2024-01-01"),
1216
1209
  updatedAt: new Date("2024-01-01"),
1217
1210
  missingRequiredEnv: [],
@@ -1219,7 +1212,7 @@ function makeCreateAgentHarness(
1219
1212
  agents.set(id, detail);
1220
1213
  calls.created.push({
1221
1214
  name: input.name,
1222
- typeName: input.typeName,
1215
+ specialty: input.specialty,
1223
1216
  selfHosted: input.selfHosted,
1224
1217
  });
1225
1218
  return detail;
@@ -1313,7 +1306,7 @@ function baseCreateAgentInput(
1313
1306
  ): CreateAgentFormInput {
1314
1307
  return {
1315
1308
  name: "Test Agent",
1316
- typeName: "coding",
1309
+ specialty: "coding",
1317
1310
  runtime: undefined,
1318
1311
  reposRaw: undefined,
1319
1312
  authorAllowlistRaw: undefined,
@@ -1434,24 +1427,24 @@ describe("createAgent()", () => {
1434
1427
  expect(calls.created).toEqual([]);
1435
1428
  });
1436
1429
 
1437
- it("returns invalid_type and creates nothing when typeName is unknown", async () => {
1430
+ it("returns invalid_type and creates nothing when specialty is unknown", async () => {
1438
1431
  const { deps, calls } = makeCreateAgentHarness();
1439
1432
 
1440
1433
  const result = await createAgent(
1441
1434
  deps,
1442
- baseCreateAgentInput({ typeName: "nonexistent" }),
1435
+ baseCreateAgentInput({ specialty: "nonexistent" }),
1443
1436
  );
1444
1437
 
1445
1438
  expect(result).toEqual({ ok: false, errorCode: "invalid_type" });
1446
1439
  expect(calls.created).toEqual([]);
1447
1440
  });
1448
1441
 
1449
- it("returns invalid_type and creates nothing when typeName is missing", async () => {
1442
+ it("returns invalid_type and creates nothing when specialty is missing", async () => {
1450
1443
  const { deps, calls } = makeCreateAgentHarness();
1451
1444
 
1452
1445
  const result = await createAgent(
1453
1446
  deps,
1454
- baseCreateAgentInput({ typeName: undefined }),
1447
+ baseCreateAgentInput({ specialty: undefined }),
1455
1448
  );
1456
1449
 
1457
1450
  expect(result).toEqual({ ok: false, errorCode: "invalid_type" });
@@ -39,6 +39,7 @@ interface MockAgent {
39
39
  restrictSlackToMembers: boolean;
40
40
  memberEmails: string[];
41
41
  accountStatus?: string | null;
42
+ specialty?: string;
42
43
  selfHosted?: boolean;
43
44
  // APM-1.5: the 6 agent-policy fields passed through by GET /:id/config.
44
45
  autoPostReviews: boolean;
@@ -94,6 +95,7 @@ function makeMockAgentService(agents: Map<string, MockAgent>): {
94
95
  restrictSlackToMembers: boolean;
95
96
  memberEmails: string[];
96
97
  accountStatus?: string | null;
98
+ specialty: string;
97
99
  selfHosted?: boolean;
98
100
  autoPostReviews: boolean;
99
101
  allowSelfReview: boolean;
@@ -112,6 +114,7 @@ function makeMockAgentService(agents: Map<string, MockAgent>): {
112
114
  restrictSlackToMembers: boolean;
113
115
  memberEmails: string[];
114
116
  accountStatus?: string | null;
117
+ specialty: string;
115
118
  selfHosted?: boolean;
116
119
  autoPostReviews: boolean;
117
120
  allowSelfReview: boolean;
@@ -130,6 +133,7 @@ function makeMockAgentService(agents: Map<string, MockAgent>): {
130
133
  restrictSlackToMembers: agent.restrictSlackToMembers,
131
134
  memberEmails: agent.memberEmails,
132
135
  accountStatus: agent.accountStatus,
136
+ specialty: agent.specialty ?? "coding",
133
137
  selfHosted: agent.selfHosted,
134
138
  autoPostReviews: agent.autoPostReviews,
135
139
  allowSelfReview: agent.allowSelfReview,
@@ -228,6 +232,7 @@ function buildApp(opts?: {
228
232
  restrictSlackToMembers?: boolean;
229
233
  memberEmails?: string[];
230
234
  accountStatus?: string | null;
235
+ specialty?: string;
231
236
  selfHosted?: boolean;
232
237
  anthropicFederationEnabled?: boolean;
233
238
  phaseMethodology?: MockPhaseMethodology[];
@@ -283,6 +288,7 @@ function buildApp(opts?: {
283
288
  restrictSlackToMembers,
284
289
  memberEmails,
285
290
  accountStatus: opts?.accountStatus,
291
+ specialty: opts?.specialty,
286
292
  selfHosted: opts?.selfHosted,
287
293
  autoPostReviews,
288
294
  allowSelfReview,
@@ -332,6 +338,21 @@ describe("GET /:id/config with no phase-methodology service (platform only)", ()
332
338
  });
333
339
  });
334
340
 
341
+ describe("GET /:id/config deprecated typeName alias (AGR-6.3)", () => {
342
+ // Pre-AGR-6.3 runtime images read only config.typeName to set
343
+ // SHIPWRIGHT_AGENT_TYPE; the alias must mirror specialty until they roll.
344
+ test("typeName equals specialty for a non-coding specialty", async () => {
345
+ const app = buildApp({ specialty: "research" });
346
+ const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
347
+ headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
348
+ });
349
+ expect(res.status).toBe(200);
350
+ const body = await res.json();
351
+ expect(body.specialty).toBe("research");
352
+ expect(body.typeName).toBe("research");
353
+ });
354
+ });
355
+
335
356
  describe("GET /:id/config features (COS-43.1)", () => {
336
357
  const get = (app: ReturnType<typeof buildApp>) =>
337
358
  app.request(`/${KNOWN_AGENT_ID}/config`, {
@@ -736,6 +757,7 @@ function buildCombinedApp() {
736
757
  patchAuthorAllowlist: [],
737
758
  restrictSlackToMembers: false,
738
759
  memberEmails: [],
760
+ specialty: "coding",
739
761
  // APM-1.5: required by AgentConfigResponseSchema.
740
762
  autoPostReviews: true,
741
763
  allowSelfReview: false,
@@ -772,7 +794,7 @@ function buildCombinedApp() {
772
794
  reviewAuthorAllowlist: [],
773
795
  patchAuthorAllowlist: [],
774
796
  restrictSlackToMembers: false,
775
- typeName: "coding",
797
+ specialty: "coding",
776
798
  createdAt: new Date(),
777
799
  updatedAt: new Date(),
778
800
  missingRequiredEnv: [],
@@ -791,7 +813,7 @@ function buildCombinedApp() {
791
813
  reviewAuthorAllowlist: [],
792
814
  patchAuthorAllowlist: [],
793
815
  restrictSlackToMembers: false,
794
- typeName: "coding",
816
+ specialty: "coding",
795
817
  createdAt: new Date(),
796
818
  updatedAt: new Date(),
797
819
  missingRequiredEnv: [],
package/src/api.ts CHANGED
@@ -23,7 +23,6 @@ import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
23
23
  import type { AgentCronJob } from "./agent-cron-jobs.ts";
24
24
  import type { AgentEnvBundle } from "./agent-envs.ts";
25
25
  import type { AgentTokenService } from "./agent-tokens.ts";
26
- import { resolveSpecialty } from "./agent-specialty.ts";
27
26
  import { anthropicCredentialKeysIn } from "./anthropic-credential-guard.ts";
28
27
  import { type AdminApiKey, createAdminAuthMiddleware } from "./api-auth.ts";
29
28
  import {
@@ -67,13 +66,16 @@ export interface AgentConfigResponse {
67
66
  */
68
67
  accountStatus?: string | null;
69
68
  /**
70
- * ARD-6.1: the agent's Specialty name (e.g. "coding"). Additive and
71
- * optional — fixtures/doubles built before this field keep compiling, and
72
- * the agent runtime treats an absent value as "coding".
69
+ * ARD-6.1: the agent's Specialty name (e.g. "coding"), read by the agent
70
+ * runtime to pick its workspace template directory.
71
+ */
72
+ specialty: string;
73
+ /**
74
+ * @deprecated Back-compat alias of `specialty` for pre-AGR-6.3 runtime
75
+ * images (their entrypoint reads `config.typeName`). Remove once every
76
+ * runtime image reads `specialty`.
73
77
  */
74
78
  typeName?: string;
75
- /** AGR-6.1: specialty (specialty ?? typeName); additive, optional for doubles. */
76
- specialty?: string;
77
79
  /**
78
80
  * PMC-1.1: phase -> subagentType (or null) map, covering all six pipeline
79
81
  * phases regardless of whether the agent has an explicit override row for
@@ -136,8 +138,7 @@ interface AgentServiceLike {
136
138
  trialExpiresAt?: Date | null;
137
139
  /** SSP-8.3: owning account status; optional for pre-existing test doubles. */
138
140
  accountStatus?: string | null;
139
- typeName?: string;
140
- specialty?: string | null;
141
+ specialty: string;
141
142
  /** WIF-2.1: optional so pre-existing test doubles keep compiling; absent is treated as not self-hosted. */
142
143
  selfHosted?: boolean;
143
144
  /** APM-1.5: the 6 agent-policy fields, direct-passthrough into the response. */
@@ -349,14 +350,12 @@ export function createAgentRuntimeApp(deps: AgentRuntimeDeps): OpenAPIHono {
349
350
  ? agent.trialExpiresAt.toISOString()
350
351
  : null,
351
352
  accountStatus: agent.accountStatus ?? null,
352
- typeName: agent.typeName,
353
- specialty:
354
- agent.typeName === undefined
355
- ? undefined
356
- : resolveSpecialty({
357
- typeName: agent.typeName,
358
- specialty: agent.specialty,
359
- }),
353
+ specialty: agent.specialty,
354
+ // DEPRECATED (AGR-6.3): back-compat alias for pre-AGR-6.3 runtime images,
355
+ // whose entrypoint only reads `config.typeName` to set
356
+ // SHIPWRIGHT_AGENT_TYPE. Without it those pods silently fall back to
357
+ // coding templates. Remove in a follow-up once all images have rolled.
358
+ typeName: agent.specialty,
360
359
  // Omitted entirely when no phase-methodology service was supplied
361
360
  // (platform-only admin, no coding extension).
362
361
  ...(agentPhaseMethodologyService ? { phaseMethodology } : {}),
@@ -34,7 +34,6 @@ export const AgentSummarySchema = z
34
34
  id: z.string().openapi({ example: "clx1234567890" }),
35
35
  name: z.string().openapi({ example: "Bodhi" }),
36
36
  selfHosted: z.boolean().openapi({ example: false }),
37
- typeName: z.string().openapi({ example: "coding" }),
38
37
  specialty: z.string().openapi({ example: "coding" }),
39
38
  })
40
39
  .openapi("AgentSummary");
@@ -148,7 +147,7 @@ export const PatchAgentBodySchema = z
148
147
  export const CreateAgentBodySchema = z
149
148
  .object({
150
149
  name: z.string().min(1).openapi({ example: "Bodhi" }),
151
- typeName: z.string().min(1).openapi({ example: "coding" }),
150
+ specialty: z.string().min(1).openapi({ example: "coding" }),
152
151
  /**
153
152
  * "in-cluster" means K8s-provisioned; anything else (including absent)
154
153
  * means self-hosted.
@@ -1341,12 +1340,20 @@ export const AgentConfigResponseSchema = z
1341
1340
  .optional()
1342
1341
  .openapi({ example: "active" }),
1343
1342
  /**
1344
- * ARD-6.1: the agent's Specialty name. Optional (additive) — the runtime
1345
- * falls back to "coding" templates when absent.
1343
+ * ARD-6.1: the agent's Specialty name, used by the runtime to pick its
1344
+ * workspace template directory.
1346
1345
  */
1347
- typeName: z.string().optional().openapi({ example: "coding" }),
1348
- /** AGR-6.1: specialty (specialty ?? typeName); additive next to typeName. */
1349
- specialty: z.string().optional().openapi({ example: "coding" }),
1346
+ specialty: z.string().openapi({ example: "coding" }),
1347
+ /**
1348
+ * DEPRECATED back-compat alias of `specialty` (always equal to it). Kept
1349
+ * only for pre-AGR-6.3 runtime images, whose entrypoint reads
1350
+ * `config.typeName` to set SHIPWRIGHT_AGENT_TYPE. Remove in a follow-up
1351
+ * once every runtime image reads `specialty`.
1352
+ */
1353
+ typeName: z
1354
+ .string()
1355
+ .optional()
1356
+ .openapi({ example: "coding", deprecated: true }),
1350
1357
  /**
1351
1358
  * PMC-1.1: phase -> subagentType (or null) map, covering all six pipeline
1352
1359
  * phases (see AGENT_PHASES) regardless of whether the agent has an
@@ -140,7 +140,7 @@ function makeService(opts?: {
140
140
  reviewAuthorAllowlist: [],
141
141
  patchAuthorAllowlist: [],
142
142
  restrictSlackToMembers: false,
143
- typeName: "coding",
143
+ specialty: "coding",
144
144
  missingRequiredEnv: [],
145
145
  })),
146
146
  updateFields: async (id: string, input: unknown) => {
@@ -156,7 +156,7 @@ function makeService(opts?: {
156
156
  reviewAuthorAllowlist: [],
157
157
  patchAuthorAllowlist: [],
158
158
  restrictSlackToMembers: false,
159
- typeName: "coding",
159
+ specialty: "coding",
160
160
  missingRequiredEnv: [],
161
161
  };
162
162
  },
@@ -242,7 +242,7 @@ function makeMockDeps(
242
242
  reviewAuthorAllowlist: [],
243
243
  patchAuthorAllowlist: [],
244
244
  restrictSlackToMembers: false,
245
- typeName: "coding",
245
+ specialty: "coding",
246
246
  missingRequiredEnv: [],
247
247
  }),
248
248
  updateFields: async (id: string) => ({
@@ -256,7 +256,7 @@ function makeMockDeps(
256
256
  reviewAuthorAllowlist: [],
257
257
  patchAuthorAllowlist: [],
258
258
  restrictSlackToMembers: false,
259
- typeName: "coding",
259
+ specialty: "coding",
260
260
  missingRequiredEnv: [],
261
261
  }),
262
262
  },
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * admin/src/specialty-manifest-loader.ts
3
3
  *
4
- * SpecialtyRegistry — resolves an agent's stored `typeName` to its parsed
5
- * Specialty manifest by loading `specialties/{typeName}/manifest.yaml` from
4
+ * SpecialtyRegistry — resolves an agent's stored `specialty` to its parsed
5
+ * Specialty manifest by loading `specialties/{specialty}/manifest.yaml` from
6
6
  * disk (relative to the repo root) and validating it via
7
7
  * parseSpecialtyManifest().
8
8
  *
@@ -10,7 +10,7 @@
10
10
  * (and, in time, its default plugins/tools/env contract). It replaces the
11
11
  * former hardcoded SYSTEM_CRONS array in admin/src/system-crons.ts.
12
12
  *
13
- * Fallback contract: an unknown `typeName` (a value with no matching
13
+ * Fallback contract: an unknown `specialty` (a value with no matching
14
14
  * directory under specialties/) resolves to the "coding" manifest with a
15
15
  * logged warning — never throws. This keeps the boot-path
16
16
  * POST /agents/:id/crons/reconcile route from beginning to 5xx for an agent
@@ -36,13 +36,13 @@ import {
36
36
  export const DEFAULT_SPECIALTY_NAME = "coding";
37
37
 
38
38
  /** Reads a manifest's raw YAML text for a type; throws if it does not exist. */
39
- type ManifestReader = (typeName: string) => string;
39
+ type ManifestReader = (specialty: string) => string;
40
40
 
41
- /** Sink for the unknown-typeName warning (defaults to console.warn). */
41
+ /** Sink for the unknown-specialty warning (defaults to console.warn). */
42
42
  type WarnFn = (message: string) => void;
43
43
 
44
44
  /**
45
- * Lists the typeName of every discoverable specialty directory (e.g. the
45
+ * Lists the name of every discoverable specialty directory (e.g. the
46
46
  * names of the subdirectories under specialties/). Used by listTypes() to
47
47
  * discover candidates before resolving each one's manifest via `read`.
48
48
  */
@@ -99,12 +99,12 @@ export function resolveDefaultSpecialtyName(
99
99
  return env.SHIPWRIGHT_DEFAULT_AGENT_TYPE || DEFAULT_SPECIALTY_NAME;
100
100
  }
101
101
 
102
- /** Default reader: loads {typesDir}/{typeName}/manifest.yaml from disk. */
102
+ /** Default reader: loads {typesDir}/{specialty}/manifest.yaml from disk. */
103
103
  export function defaultManifestReader(
104
- typeName: string,
104
+ specialty: string,
105
105
  typesDir: string = resolveSpecialtiesDir(),
106
106
  ): string {
107
- return readFileSync(join(typesDir, typeName, "manifest.yaml"), "utf-8");
107
+ return readFileSync(join(typesDir, specialty, "manifest.yaml"), "utf-8");
108
108
  }
109
109
 
110
110
  /** Default lister: the directory names under the specialties dir on disk. */
@@ -122,20 +122,20 @@ export function defaultTypeDirLister(
122
122
  */
123
123
  export interface SpecialtyManifestResolver {
124
124
  /**
125
- * Resolve `typeName` to its parsed manifest. Falls back to the "coding"
125
+ * Resolve `specialty` to its parsed manifest. Falls back to the "coding"
126
126
  * manifest (with a warning) when the type is unknown. Never throws for an
127
127
  * unknown type — only if the "coding" fallback itself fails to load.
128
128
  */
129
- getManifest(typeName: string): SpecialtyManifest;
129
+ getManifest(specialty: string): SpecialtyManifest;
130
130
  /**
131
- * Resolve `typeName` to its parsed manifest WITHOUT falling back to
131
+ * Resolve `specialty` to its parsed manifest WITHOUT falling back to
132
132
  * "coding" — returns undefined for an unknown type instead. No warning is
133
133
  * logged (an unknown type here is an expected, callers-handle-it outcome,
134
134
  * not a degraded-boot condition). Used by POST /admin/agents to reject an
135
135
  * unknown requested `type` with 400 rather than silently seeding a
136
136
  * "coding" agent under a different label.
137
137
  */
138
- tryGetManifest(typeName: string): SpecialtyManifest | undefined;
138
+ tryGetManifest(specialty: string): SpecialtyManifest | undefined;
139
139
  /**
140
140
  * Discover every specialty directory (via the injected `list` fn) and
141
141
  * resolve each to its {name, displayName} for populating the new-agent
@@ -178,18 +178,18 @@ export class SpecialtyRegistry implements SpecialtyManifestResolver {
178
178
  opts.defaultTypeName ?? resolveDefaultSpecialtyName();
179
179
  }
180
180
 
181
- getManifest(typeName: string): SpecialtyManifest {
182
- const resolved = this.tryLoad(typeName);
181
+ getManifest(specialty: string): SpecialtyManifest {
182
+ const resolved = this.tryLoad(specialty);
183
183
  if (resolved) return resolved;
184
184
 
185
185
  // Unknown type — warn once and fall back to "coding". The fallback load is
186
186
  // deliberately NOT wrapped in tryLoad: if "coding" can't load, that's a
187
187
  // hard startup bug and must surface, not silently become a no-op.
188
188
  this.warn(
189
- `SpecialtyRegistry: unknown agent typeName "${typeName}" — ` +
189
+ `SpecialtyRegistry: unknown agent specialty "${specialty}" — ` +
190
190
  `falling back to "${this.defaultTypeName}" manifest`,
191
191
  );
192
- if (typeName === this.defaultTypeName) {
192
+ if (specialty === this.defaultTypeName) {
193
193
  // Guard against infinite intent: the default itself failed to load.
194
194
  throw missingDefaultSpecialtyError(this.defaultTypeName, this.typesDir);
195
195
  }
@@ -205,19 +205,19 @@ export class SpecialtyRegistry implements SpecialtyManifestResolver {
205
205
  }
206
206
 
207
207
  /**
208
- * Resolve `typeName` without falling back to "coding" — see the interface
208
+ * Resolve `specialty` without falling back to "coding" — see the interface
209
209
  * doc comment. Thin public wrapper around the existing tryLoad() used
210
210
  * internally by getManifest().
211
211
  */
212
- tryGetManifest(typeName: string): SpecialtyManifest | undefined {
213
- return this.tryLoad(typeName);
212
+ tryGetManifest(specialty: string): SpecialtyManifest | undefined {
213
+ return this.tryLoad(specialty);
214
214
  }
215
215
 
216
216
  listTypes(): SpecialtyOption[] {
217
- const typeNames = this.list();
217
+ const specialties = this.list();
218
218
  const options: SpecialtyOption[] = [];
219
- for (const typeName of typeNames) {
220
- const manifest = this.tryLoad(typeName);
219
+ for (const specialty of specialties) {
220
+ const manifest = this.tryLoad(specialty);
221
221
  if (!manifest) continue;
222
222
  options.push({
223
223
  name: manifest.metadata.name,
@@ -228,28 +228,28 @@ export class SpecialtyRegistry implements SpecialtyManifestResolver {
228
228
  }
229
229
 
230
230
  /** Load + cache a manifest, returning undefined only if the file is missing. */
231
- private tryLoad(typeName: string): SpecialtyManifest | undefined {
232
- const cached = this.cache.get(typeName);
231
+ private tryLoad(specialty: string): SpecialtyManifest | undefined {
232
+ const cached = this.cache.get(specialty);
233
233
  if (cached) return cached;
234
234
  let raw: string;
235
235
  try {
236
- raw = this.read(typeName);
236
+ raw = this.read(specialty);
237
237
  } catch {
238
238
  return undefined;
239
239
  }
240
- return this.parseAndCache(typeName, raw);
240
+ return this.parseAndCache(specialty, raw);
241
241
  }
242
242
 
243
243
  /** Load + cache a manifest, propagating any read/parse error. */
244
- private loadOrThrow(typeName: string): SpecialtyManifest {
245
- const cached = this.cache.get(typeName);
244
+ private loadOrThrow(specialty: string): SpecialtyManifest {
245
+ const cached = this.cache.get(specialty);
246
246
  if (cached) return cached;
247
- return this.parseAndCache(typeName, this.read(typeName));
247
+ return this.parseAndCache(specialty, this.read(specialty));
248
248
  }
249
249
 
250
- private parseAndCache(typeName: string, raw: string): SpecialtyManifest {
250
+ private parseAndCache(specialty: string, raw: string): SpecialtyManifest {
251
251
  const manifest = parseSpecialtyManifest(raw);
252
- this.cache.set(typeName, manifest);
252
+ this.cache.set(specialty, manifest);
253
253
  return manifest;
254
254
  }
255
255
  }