@app-vitals/agent-admin 0.16.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/e2e/agents-page.e2e.ts +2 -2
  2. package/openapi.json +3 -3
  3. package/openapi.platform.json +7 -7
  4. package/package.json +3 -5
  5. package/prisma/client/internal/class.ts +1 -1
  6. package/prisma/migrations/20261030100000_set_agent_specialty_default_specialist/migration.sql +4 -0
  7. package/prisma/schema/schema.prisma +1 -1
  8. package/src/account-agent-create.ts +1 -1
  9. package/src/admin-boot.ts +12 -6
  10. package/src/admin-extension.ts +51 -5
  11. package/src/admin-extension.unit.test.ts +61 -0
  12. package/src/admin-local-agent.smoke.test.ts +8 -5
  13. package/src/admin-ui-pages.ts +4 -4
  14. package/src/admin-ui.smoke.test.ts +54 -34
  15. package/src/admin-ui.ts +12 -12
  16. package/src/agent-chat-tokens.smoke.test.ts +2 -2
  17. package/src/agent-cron-jobs.integration.test.ts +23 -21
  18. package/src/agent-cron-jobs.ts +30 -40
  19. package/src/agent-cron-jobs.unit.test.ts +51 -39
  20. package/src/agent-cron-run-stats.smoke.test.ts +2 -2
  21. package/src/agent-manifest.ts +2 -6
  22. package/src/agent-patch-author-allowlist.integration.test.ts +16 -27
  23. package/src/agent-specialty-backfill.integration.test.ts +30 -0
  24. package/src/agents-api.integration.test.ts +29 -40
  25. package/src/agents-api.sentry.smoke.test.ts +2 -2
  26. package/src/agents-api.smoke.test.ts +17 -30
  27. package/src/agents-api.ts +13 -13
  28. package/src/agents.integration.test.ts +23 -33
  29. package/src/agents.ts +35 -34
  30. package/src/agents.unit.test.ts +35 -55
  31. package/src/api.smoke.test.ts +2 -2
  32. package/src/chat-service-provisioning-client.integration.test.ts +1 -1
  33. package/src/openapi-schemas.ts +4 -4
  34. package/src/server.smoke.test.ts +2 -2
  35. package/src/specialty-defaults.ts +4 -4
  36. package/src/specialty-types.ts +92 -0
  37. package/src/specialty-types.unit.test.ts +67 -0
  38. package/e2e/fixtures/specialties/coding/manifest.yaml +0 -33
  39. package/src/coding-task-store-provisioning-client.integration.test.ts +0 -372
  40. package/src/coding-task-store-provisioning-client.ts +0 -156
  41. package/src/fixtures/task-store-provisioning-cassette.json +0 -85
  42. package/src/specialty-manifest-loader.fixture.integration.test.ts +0 -67
  43. package/src/specialty-manifest-loader.ts +0 -245
  44. package/src/specialty-manifest-loader.unit.test.ts +0 -299
  45. package/src/specialty-registry.schema.unit.test.ts +0 -35
  46. package/src/specialty-registry.ts +0 -236
  47. package/src/specialty-registry.unit.test.ts +0 -272
@@ -28,7 +28,7 @@ import type {
28
28
  ThreadStats,
29
29
  } from "./http-chat-client.ts";
30
30
  import { renderPushToggle } from "./push-toggle.ts";
31
- import type { SpecialtyOption } from "./specialty-manifest-loader.ts";
31
+ import type { SpecialtyOption } from "./specialty-types.ts";
32
32
 
33
33
  // ─── Helpers ──────────────────────────────────────────────────────────────────
34
34
 
@@ -83,7 +83,7 @@ export interface AgentDetail {
83
83
  /** The agent's type name (e.g. "specialist"), resolved at creation time. */
84
84
  specialty: string;
85
85
  /**
86
- * Required env keys declared by the agent's type manifest with no
86
+ * Required env keys declared by the agent's type with no
87
87
  * corresponding AgentEnv row yet — key names only, never values. Always
88
88
  * present; purely informational (ATS-4.2), see renderAgentDetailPage's Env
89
89
  * Vars card.
@@ -2351,7 +2351,7 @@ const STALL_INDICATOR_CLASS = "chat-stall-indicator";
2351
2351
  * hard stop.
2352
2352
  */
2353
2353
  export const STALL_WARN_AFTER_MS = 120_000;
2354
- // 65min — mirrors lib/claim-ttl.ts's DEFAULT_CLAIM_TTL_MS
2354
+ // 65min — mirrors DEFAULT_CLAIM_TTL_MS (Shipwright claim-ttl)
2355
2355
  // (DEFAULT_CLAUDE_TIMEOUT_MS 1hr + CLAIM_TTL_BUFFER_MS 5min). Inline JS can't
2356
2356
  // import the lib file, so the value is hardcoded here with this reference.
2357
2357
  export const ABSOLUTE_MAX_MS = 3_900_000;
@@ -2886,7 +2886,7 @@ export function renderChatThreadPage(
2886
2886
 
2887
2887
  // Stall-warning threshold — 120s in prod; e2e overrides to a tiny value.
2888
2888
  var STALL_WARN_AFTER_MS = ${stallWarnAfterMs};
2889
- // Hard ceiling: 65min, mirrors lib/claim-ttl.ts DEFAULT_CLAIM_TTL_MS
2889
+ // Hard ceiling: 65min, mirrors Shipwright's DEFAULT_CLAIM_TTL_MS
2890
2890
  // (1hr DEFAULT_CLAUDE_TIMEOUT_MS + 5min CLAIM_TTL_BUFFER_MS). Only hard stop.
2891
2891
  var ABSOLUTE_MAX_MS = ${ABSOLUTE_MAX_MS};
2892
2892
 
@@ -7,11 +7,11 @@
7
7
  */
8
8
 
9
9
  import { beforeAll, describe, expect, it } from "bun:test";
10
- import { existsSync } from "node:fs";
10
+ import { existsSync, mkdtempSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
11
12
  import { join } from "node:path";
12
13
  import { sign } from "hono/jwt";
13
- import { createAdminUIApp } from "./admin-ui.ts";
14
- import { SpecialtyRegistry } from "./specialty-manifest-loader.ts";
14
+ import { withExtensionAgentTypes } from "./admin-extension.ts";
15
15
  import {
16
16
  AGENT_ID,
17
17
  CRON_ID,
@@ -26,16 +26,22 @@ import {
26
26
  TOKEN_ID,
27
27
  TOOL_ID,
28
28
  } from "./admin-ui.fixtures.ts";
29
+ import { createAdminUIApp } from "./admin-ui.ts";
29
30
  import { AnthropicCredentialsBlockedError } from "./anthropic-credential-guard.ts";
30
31
  import type { PushService } from "./push-service.ts";
32
+ import {
33
+ resolveDefaultSpecialtyName,
34
+ SPECIALIST_TYPE,
35
+ SpecialtyTypes,
36
+ } from "./specialty-types.ts";
31
37
 
32
38
  // The specialty the create-agent flows use: the configured default type (the
33
39
  // platform test run points it at the fixture specialty; the monorepo falls
34
40
  // back to "coding").
35
- const TYPE_MANIFEST = new SpecialtyRegistry().getManifest(
36
- process.env.SHIPWRIGHT_DEFAULT_AGENT_TYPE || "coding",
37
- );
38
- const TYPE = TYPE_MANIFEST.metadata.name;
41
+ const TYPE = resolveDefaultSpecialtyName();
42
+ const TYPE_RECORD = new SpecialtyTypes()
43
+ .listTypes()
44
+ .find((t) => t.name === TYPE) as { name: string; displayName: string };
39
45
 
40
46
  // ─── Auth redirect tests ──────────────────────────────────────────────────────
41
47
 
@@ -1288,7 +1294,7 @@ describe("admin UI — authenticated pages", () => {
1288
1294
  '<select id="type" name="type" class="form-input" required>',
1289
1295
  );
1290
1296
  expect(html).toContain(
1291
- `<option value="${TYPE}">${TYPE_MANIFEST.metadata.displayName}</option>`,
1297
+ `<option value="${TYPE}">${TYPE_RECORD.displayName}</option>`,
1292
1298
  );
1293
1299
  });
1294
1300
 
@@ -5549,14 +5555,14 @@ describe("admin UI — create agent with patch author allowlist", () => {
5549
5555
  });
5550
5556
  });
5551
5557
 
5552
- describe("admin UI — create agent seeds manifest tools and plugins", () => {
5558
+ describe("admin UI — create agent seeds type tools and extension plugins", () => {
5553
5559
  let cookie: string;
5554
5560
 
5555
5561
  beforeAll(async () => {
5556
5562
  cookie = await makeSessionCookie();
5557
5563
  });
5558
5564
 
5559
- it("POST /admin/agents with the default type seeds an AgentTool row for every manifest tool and an AgentPlugin row for every manifest plugin", async () => {
5565
+ it("POST /admin/agents with the default type seeds an AgentTool row for every type tool and no AgentPlugin rows", async () => {
5560
5566
  const seededTools: string[] = [];
5561
5567
  const seededPlugins: string[] = [];
5562
5568
  const deps = makeMockDeps();
@@ -5594,28 +5600,13 @@ describe("admin UI — create agent seeds manifest tools and plugins", () => {
5594
5600
  });
5595
5601
  expect(res.status).toBe(302);
5596
5602
  expect(res.headers.get("Location")).toBe(`/admin/agents/${AGENT_ID}`);
5597
- expect(seededTools).toEqual(TYPE_MANIFEST.tools);
5603
+ expect(seededTools).toEqual([...SPECIALIST_TYPE.tools]);
5598
5604
  expect(seededTools).toContain("Bash");
5599
- expect(seededPlugins).toEqual(TYPE_MANIFEST.plugins);
5600
- });
5601
-
5602
- it("POST /admin/agents with the fixture specialty (AGR-1.9) seeds from its manifest and needs no product specialty", async () => {
5603
- const fixtureDir = [
5604
- join(
5605
- import.meta.dir,
5606
- "..",
5607
- "..",
5608
- "agent",
5609
- "src",
5610
- "fixtures",
5611
- "specialties",
5612
- ),
5613
- join(import.meta.dir, "..", "..", "runtime", "fixtures", "specialties"),
5614
- ].find((dir) => existsSync(dir)) as string;
5615
- const registry = new SpecialtyRegistry({
5616
- typesDir: fixtureDir,
5617
- defaultTypeName: "fixture",
5618
- });
5605
+ expect(seededPlugins).toEqual([]);
5606
+ });
5607
+
5608
+ it("POST /admin/agents with the configured default type (AGR-1.9) seeds the specialist floor and needs no product specialty", async () => {
5609
+ const registry = new SpecialtyTypes({ defaultTypeName: "fixture" });
5619
5610
  const seededTools: string[] = [];
5620
5611
  const seededPlugins: string[] = [];
5621
5612
  const deps = makeMockDeps();
@@ -5647,7 +5638,7 @@ describe("admin UI — create agent seeds manifest tools and plugins", () => {
5647
5638
  headers: { Cookie: `admin_session=${cookie}` },
5648
5639
  });
5649
5640
  expect(await form.text()).toContain(
5650
- '<option value="fixture">Fixture Agent</option>',
5641
+ '<option value="fixture">fixture</option>',
5651
5642
  );
5652
5643
  const res = await app.request("/admin/agents", {
5653
5644
  method: "POST",
@@ -5662,8 +5653,36 @@ describe("admin UI — create agent seeds manifest tools and plugins", () => {
5662
5653
  });
5663
5654
  expect(res.status).toBe(302);
5664
5655
  expect(res.headers.get("Location")).toBe(`/admin/agents/${AGENT_ID}`);
5665
- expect(seededTools).toEqual(registry.getManifest("fixture").tools);
5666
- expect(seededPlugins).toEqual(["fixture-plugin"]);
5656
+ expect(seededTools).toEqual([...registry.getType("fixture").tools]);
5657
+ expect(seededPlugins).toEqual([]);
5658
+ });
5659
+
5660
+ it("an extension-contributed type (ETP-1.2) shows in the picker and POST /admin/agents accepts it", async () => {
5661
+ const registry = withExtensionAgentTypes(new SpecialtyTypes(), [
5662
+ { agentTypes: () => [{ name: "coding", displayName: "Coding Agent" }] },
5663
+ ]);
5664
+ const deps = makeMockDeps();
5665
+ deps.specialtyRegistry = registry;
5666
+ const app = createAdminUIApp(deps);
5667
+ const form = await app.request("/admin/agents/new", {
5668
+ headers: { Cookie: `admin_session=${cookie}` },
5669
+ });
5670
+ expect(await form.text()).toContain(
5671
+ '<option value="coding">Coding Agent</option>',
5672
+ );
5673
+ const res = await app.request("/admin/agents", {
5674
+ method: "POST",
5675
+ body: new URLSearchParams({
5676
+ name: "Ext Agent",
5677
+ type: "coding",
5678
+ }).toString(),
5679
+ headers: {
5680
+ "Content-Type": "application/x-www-form-urlencoded",
5681
+ Cookie: `admin_session=${cookie}`,
5682
+ },
5683
+ });
5684
+ expect(res.status).toBe(302);
5685
+ expect(res.headers.get("Location")).toBe(`/admin/agents/${AGENT_ID}`);
5667
5686
  });
5668
5687
 
5669
5688
  it("POST /admin/agents rolls back the agent and redirects with error when tool seeding fails", async () => {
@@ -5701,6 +5720,7 @@ describe("admin UI — create agent seeds manifest tools and plugins", () => {
5701
5720
  it("POST /admin/agents rolls back the agent and redirects with error when plugin seeding fails", async () => {
5702
5721
  let deletedId: string | undefined;
5703
5722
  const deps = makeMockDeps();
5723
+ deps.specialtyDefaults = () => ({ plugins: ["ext-plugin"] });
5704
5724
  deps.agentPluginService = {
5705
5725
  ...deps.agentPluginService,
5706
5726
  add: async () => {
package/src/admin-ui.ts CHANGED
@@ -74,11 +74,6 @@ import type { AgentPluginService } from "./agent-plugins.ts";
74
74
  import type { AgentProvisioner } from "./agent-provisioner.ts";
75
75
  import type { AgentTokenService } from "./agent-tokens.ts";
76
76
  import type { AgentToolService } from "./agent-tools.ts";
77
- import {
78
- type SpecialtyManifestResolver,
79
- SpecialtyRegistry,
80
- } from "./specialty-manifest-loader.ts";
81
- import type { SpecialtyDefaultsResolver } from "./specialty-defaults.ts";
82
77
  import type { AgentService } from "./agents.ts";
83
78
  import { createAgent, describeRepoNameCollision } from "./agents.ts";
84
79
  import { AnthropicCredentialsBlockedError } from "./anthropic-credential-guard.ts";
@@ -86,7 +81,6 @@ import {
86
81
  audioContentTypeForFilename,
87
82
  validateAttachment,
88
83
  } from "./attachment-validation.ts";
89
- import { parseByteRange } from "./http-range.ts";
90
84
  import {
91
85
  type CallerScopeResolver,
92
86
  memberOnlyCallerScopeResolver,
@@ -105,6 +99,7 @@ import {
105
99
  filterSince,
106
100
  toPolledMessage,
107
101
  } from "./http-chat-client.ts";
102
+ import { parseByteRange } from "./http-range.ts";
108
103
  import type { OktaAuthClient } from "./okta-auth-client.ts";
109
104
  import type { PrismaTransactionClient } from "./prisma-tx.ts";
110
105
  import type { PushService } from "./push-service.ts";
@@ -132,6 +127,11 @@ import {
132
127
  PROVISION_STATE_TTL_SECONDS,
133
128
  SlackProvisioningService,
134
129
  } from "./slack-provisioning-service.ts";
130
+ import type { SpecialtyDefaultsResolver } from "./specialty-defaults.ts";
131
+ import {
132
+ type SpecialtyTypeResolver,
133
+ SpecialtyTypes,
134
+ } from "./specialty-types.ts";
135
135
 
136
136
  export type AdminUIEnv = { Variables: { userEmail: string; isAdmin: boolean } };
137
137
 
@@ -347,13 +347,13 @@ export interface AdminUIDeps {
347
347
  /** WIF-2.1: Anthropic federation is enabled; defaults to false. */
348
348
  anthropicFederationEnabled?: boolean;
349
349
  /**
350
- * Resolves an agent's specialty to its parsed Specialty manifest and lists
351
- * the registry's discoverable types (for the new-specialty picker).
352
- * Defaults to a disk-backed SpecialtyRegistry, matching the DI pattern
350
+ * Resolves an agent's specialty to its type record and lists
351
+ * the registry's built-in types (for the new-specialty picker).
352
+ * Defaults to a disk-backed SpecialtyTypes, matching the DI pattern
353
353
  * already used by agents-api.ts's AdminDeps.
354
354
  */
355
- specialtyRegistry?: SpecialtyManifestResolver;
356
- /** Extension-supplied specialty defaults, consulted before the manifest. */
355
+ specialtyRegistry?: SpecialtyTypeResolver;
356
+ /** Extension-supplied specialty defaults, consulted before the built-in type. */
357
357
  specialtyDefaults?: SpecialtyDefaultsResolver;
358
358
  /**
359
359
  * Cleanup deps for the full delete-agent orchestration (POST
@@ -784,7 +784,7 @@ export function createAdminUIApp(deps: AdminUIDeps): Hono<AdminUIEnv> {
784
784
  accountOnboarding,
785
785
  accountCreatedNotifier,
786
786
  accountServices,
787
- specialtyRegistry = new SpecialtyRegistry(),
787
+ specialtyRegistry = new SpecialtyTypes(),
788
788
  specialtyDefaults,
789
789
  callerScopeResolver = memberOnlyCallerScopeResolver(agentMemberService),
790
790
  serviceIntegrations,
@@ -222,10 +222,10 @@ function makeMockDeps(opts?: {
222
222
  listByAgentId: async () => [],
223
223
  },
224
224
  specialtyRegistry: {
225
- getManifest: () => {
225
+ getType: () => {
226
226
  throw new Error("not implemented");
227
227
  },
228
- tryGetManifest: () => undefined,
228
+ tryGetType: () => undefined,
229
229
  listTypes: () => [],
230
230
  },
231
231
  agentChatTokenService: {
@@ -18,8 +18,14 @@ import {
18
18
  import { AgentCronJobService } from "./agent-cron-jobs.ts";
19
19
  import { NotFoundError, UnprocessableEntityError } from "./errors.ts";
20
20
  import { createAdminPrismaClient } from "./prisma-client.ts";
21
- import type { SpecialtyManifestResolver } from "./specialty-manifest-loader.ts";
22
- import type { SpecialtyManifest } from "./specialty-registry.ts";
21
+ import type {
22
+ SpecialtyDefaultCron,
23
+ SpecialtyDefaultsResolver,
24
+ } from "./specialty-defaults.ts";
25
+ import {
26
+ SPECIALIST_TYPE,
27
+ type SpecialtyTypeResolver,
28
+ } from "./specialty-types.ts";
23
29
 
24
30
  const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
25
31
 
@@ -34,9 +40,7 @@ function makePrisma(): PrismaClient {
34
40
  // cron, covering every field reconcile maps into a row.
35
41
  const FIXTURE_TYPE = "fixture";
36
42
 
37
- type ManifestCron = SpecialtyManifest["crons"][number];
38
-
39
- const FIXTURE_CRONS: ManifestCron[] = [
43
+ const FIXTURE_CRONS: SpecialtyDefaultCron[] = [
40
44
  {
41
45
  name: "fixture-loop",
42
46
  schedule: "*/30 * * * *",
@@ -70,23 +74,16 @@ const FIXTURE_CRONS: ManifestCron[] = [
70
74
  },
71
75
  ];
72
76
 
73
- const fixtureManifest = {
74
- crons: FIXTURE_CRONS,
75
- } as unknown as SpecialtyManifest;
77
+ /** The extension hook supplying FIXTURE_TYPE's crons. */
78
+ const fixtureDefaults: SpecialtyDefaultsResolver = (specialty) =>
79
+ specialty === FIXTURE_TYPE ? { crons: FIXTURE_CRONS } : undefined;
76
80
 
77
81
  /** Resolves only FIXTURE_TYPE; every other type is unknown. */
78
- const fixtureResolver: SpecialtyManifestResolver = {
79
- getManifest(specialty: string): SpecialtyManifest {
80
- if (specialty !== FIXTURE_TYPE)
81
- throw new Error(`no manifest: ${specialty}`);
82
- return fixtureManifest;
83
- },
84
- tryGetManifest(specialty: string): SpecialtyManifest | undefined {
85
- return specialty === FIXTURE_TYPE ? fixtureManifest : undefined;
86
- },
87
- listTypes() {
88
- return [{ name: FIXTURE_TYPE, displayName: FIXTURE_TYPE }];
89
- },
82
+ const fixtureResolver: SpecialtyTypeResolver = {
83
+ getType: () => SPECIALIST_TYPE,
84
+ tryGetType: (specialty) =>
85
+ specialty === FIXTURE_TYPE ? SPECIALIST_TYPE : undefined,
86
+ listTypes: () => [{ name: FIXTURE_TYPE, displayName: FIXTURE_TYPE }],
90
87
  };
91
88
 
92
89
  async function createAgent(
@@ -109,7 +106,12 @@ describeOrSkip("AgentCronJobService (integration)", () => {
109
106
  await prisma.agentTool.deleteMany();
110
107
  await prisma.agentEnv.deleteMany();
111
108
  await prisma.agent.deleteMany();
112
- service = new AgentCronJobService(prisma, undefined, fixtureResolver);
109
+ service = new AgentCronJobService(
110
+ prisma,
111
+ undefined,
112
+ fixtureResolver,
113
+ fixtureDefaults,
114
+ );
113
115
  });
114
116
 
115
117
  afterEach(async () => {
@@ -15,17 +15,9 @@ import type {
15
15
  SpecialtyDefaultsResolver,
16
16
  } from "./specialty-defaults.ts";
17
17
  import {
18
- type SpecialtyManifestResolver,
19
- SpecialtyRegistry,
20
- } from "./specialty-manifest-loader.ts";
21
- import type { SpecialtyManifest } from "./specialty-registry.ts";
22
-
23
- /**
24
- * The shape of a single cron template resolved from a specialty's manifest.
25
- * Structurally identical to the former SYSTEM_CRONS SystemCron entries — the
26
- * manifest is now the source of truth (see specialty-manifest-loader.ts).
27
- */
28
- type ManifestCron = SpecialtyManifest["crons"][number];
18
+ type SpecialtyTypeResolver,
19
+ SpecialtyTypes,
20
+ } from "./specialty-types.ts";
29
21
 
30
22
  export type { AgentCronJob };
31
23
 
@@ -87,7 +79,7 @@ export class AgentCronJobService {
87
79
  constructor(
88
80
  private prisma: PrismaClient,
89
81
  private clock: Clock = SystemClock(),
90
- private specialtyRegistry: SpecialtyManifestResolver = new SpecialtyRegistry(),
82
+ private specialtyRegistry: SpecialtyTypeResolver = new SpecialtyTypes(),
91
83
  private specialtyDefaults: SpecialtyDefaultsResolver = () => undefined,
92
84
  ) {}
93
85
 
@@ -442,26 +434,25 @@ export class AgentCronJobService {
442
434
  /**
443
435
  * Reconcile system crons for the given agent.
444
436
  *
445
- * The cron list is resolved from the agent's type manifest — the agent's
446
- * stored `specialty` is looked up via the injected SpecialtyRegistry's
447
- * `tryGetManifest()` (the parsed `specialties/{specialty}/manifest.yaml`),
448
- * deliberately WITHOUT the default-type fallback: any type (the default
449
- * included) with no loadable manifest logs an error and reconciles nothing
450
- * (no seeding of another type's crons, no deletion of existing system
451
- * crons). This replaced the former hardcoded
452
- * SYSTEM_CRONS array as the source of truth (ATS-3.2).
437
+ * The cron list comes from the extension's specialtyDefaults() for the
438
+ * agent's stored `specialty`. Without extension crons the built-in types
439
+ * (looked up via the injected resolver's `tryGetType()`, deliberately
440
+ * WITHOUT a fallback) declare none, so their system crons are cleared; a
441
+ * specialty that is neither registered nor built in logs an error and
442
+ * reconciles nothing (no seeding of another type's crons, no deletion of
443
+ * existing system crons).
453
444
  *
454
- * Two passes so parent/child links resolve regardless of the manifest's
445
+ * Two passes so parent/child links resolve regardless of the default
455
446
  * cron order or whether this is the agent's very first reconcile (i.e.
456
447
  * the parent row may not exist yet when a child entry is processed):
457
448
  *
458
- * Pass 1 — for each manifest cron entry:
449
+ * Pass 1 — for each default cron entry:
459
450
  * - If an existing system cron with matching name exists: update it in
460
- * place with the current manifest definition, preserving its id and
451
+ * place with the current default definition, preserving its id and
461
452
  * existing enabled state. Updating in place (rather than delete+create)
462
453
  * keeps the row's id stable across restarts so AgentCronRun history
463
454
  * (FK onDelete: Cascade) is never wiped out from under it.
464
- * - If no matching cron exists: create it with the manifest's default
455
+ * - If no matching cron exists: create it with the default
465
456
  * enabled state.
466
457
  * Records each entry's resulting row id in a name → id map as it goes.
467
458
  *
@@ -472,10 +463,10 @@ export class AgentCronJobService {
472
463
  * non-null parentCronId on that row is cleared back to null. Self-heals
473
464
  * the link on every reconcile call in both directions — null→set (e.g.
474
465
  * a pre-existing row from before parentCron was introduced) and set→null
475
- * (e.g. a parentCron declaration removed from the manifest).
466
+ * (e.g. a parentCron declaration removed from the defaults).
476
467
  *
477
468
  * Orphan pass: delete any cron with system=true whose name is no longer in
478
- * the resolved manifest cron list.
469
+ * the resolved default cron list.
479
470
  *
480
471
  * All three passes run inside a single transaction for atomicity.
481
472
  *
@@ -484,7 +475,7 @@ export class AgentCronJobService {
484
475
  async reconcileSystemCrons(
485
476
  agentId: string,
486
477
  ): Promise<{ created: number; updated: number; deleted: number }> {
487
- // Resolve the agent's type manifest → its declared cron list. No default-type
478
+ // Resolve the agent's default cron list. No default-type
488
479
  // fallback: seeding another type's crons on an agent is worse than
489
480
  // leaving its crons untouched.
490
481
  const agent = await this.prisma.agent.findUnique({
@@ -495,19 +486,18 @@ export class AgentCronJobService {
495
486
  throw new NotFoundError(`agent ${agentId} not found`);
496
487
  }
497
488
  const { specialty } = agent;
498
- // Extension-supplied crons win; the manifest is the fallback.
489
+ // Extension-supplied crons win; built-in types declare none.
499
490
  const extensionCrons = this.specialtyDefaults(specialty)?.crons;
500
- const manifest = extensionCrons
491
+ const knownType = extensionCrons
501
492
  ? undefined
502
- : this.specialtyRegistry.tryGetManifest(specialty);
503
- if (!extensionCrons && !manifest) {
493
+ : this.specialtyRegistry.tryGetType(specialty);
494
+ if (!extensionCrons && !knownType) {
504
495
  console.error(
505
- `reconcileSystemCrons: no loadable manifest for specialty "${specialty}" (agent ${agentId}) — leaving crons untouched`,
496
+ `reconcileSystemCrons: no known type for specialty "${specialty}" (agent ${agentId}) — leaving crons untouched`,
506
497
  );
507
498
  return { created: 0, updated: 0, deleted: 0 };
508
499
  }
509
- const manifestCrons: readonly (ManifestCron | SpecialtyDefaultCron)[] =
510
- extensionCrons ?? manifest?.crons ?? [];
500
+ const defaultCrons: readonly SpecialtyDefaultCron[] = extensionCrons ?? [];
511
501
 
512
502
  // Fetch all existing system crons for this agent
513
503
  const existingSystemCrons = await this.prisma.agentCronJob.findMany({
@@ -522,8 +512,8 @@ export class AgentCronJobService {
522
512
  }
523
513
  }
524
514
 
525
- // Build set of valid manifest cron names for orphan detection
526
- const systemCronNames = new Set(manifestCrons.map((c) => c.name));
515
+ // Build set of valid default cron names for orphan detection
516
+ const systemCronNames = new Set(defaultCrons.map((c) => c.name));
527
517
 
528
518
  let created = 0;
529
519
  let updated = 0;
@@ -532,9 +522,9 @@ export class AgentCronJobService {
532
522
  // Wrap all mutations in a transaction so a crash mid-loop cannot leave
533
523
  // the agent missing required system crons (e.g. shipwright-dev-task).
534
524
  await this.prisma.$transaction(async (tx) => {
535
- // Pass 1: reconcile each manifest cron entry, recording name → row id.
525
+ // Pass 1: reconcile each default cron entry, recording name → row id.
536
526
  const idByName = new Map<string, string>();
537
- for (const systemCron of manifestCrons) {
527
+ for (const systemCron of defaultCrons) {
538
528
  const existing = existingByName.get(systemCron.name);
539
529
 
540
530
  if (existing) {
@@ -577,7 +567,7 @@ export class AgentCronJobService {
577
567
  // Also clears parentCronId back to null when an entry no longer
578
568
  // declares a resolvable parentCron, so the link self-heals in both
579
569
  // directions rather than only null→set.
580
- for (const systemCron of manifestCrons) {
570
+ for (const systemCron of defaultCrons) {
581
571
  const childId = idByName.get(systemCron.name);
582
572
  if (!childId) continue;
583
573
  const parentId = systemCron.parentCron
@@ -600,7 +590,7 @@ export class AgentCronJobService {
600
590
  }
601
591
  }
602
592
 
603
- // Orphan pass: delete any system cron whose name is not in the manifest.
593
+ // Orphan pass: delete any system cron whose name is not in the defaults.
604
594
  for (const cron of existingSystemCrons) {
605
595
  if (!cron.name || !systemCronNames.has(cron.name)) {
606
596
  await tx.agentCronJob.delete({ where: { id: cron.id } });