@app-vitals/agent-runtime 0.16.2 → 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.
package/Dockerfile CHANGED
@@ -175,7 +175,7 @@ RUN if [ -f bun.lock ]; then bun install --frozen-lockfile; else bun install; fi
175
175
  COPY runtime/ ./runtime/
176
176
  COPY admin/ ./admin/
177
177
  COPY lib/ ./lib/
178
- # Agent-type definitions (setup.ts reads identity.templatesDir from them).
178
+ # Agent-type definitions (setup.ts reads each type's templates/ from them).
179
179
  COPY agent-types/ ./agent-types/
180
180
  # Composition root spawned by runtime/entrypoint.ts (../index.ts).
181
181
  COPY index.ts ./index.ts
@@ -7,43 +7,29 @@
7
7
  import { describe, expect, it } from "bun:test";
8
8
  import { existsSync } from "node:fs";
9
9
  import { join } from "node:path";
10
- import { SpecialtyRegistry } from "@app-vitals/agent-admin/specialty-manifest-loader";
10
+ import { SpecialtyTypes } from "@app-vitals/agent-admin/specialty-types";
11
11
  import {
12
12
  resolveDefaultSpecialtyName,
13
- resolveSpecialtiesDir,
14
13
  SHIPPED_AGENT_TYPES_DIR,
15
14
  } from "@app-vitals/agent-lib/default-specialty";
15
+ import { SPECIALIST_TYPE } from "@app-vitals/agent-lib/specialist-type";
16
16
  import { resolveTemplatesDir } from "./setup.ts";
17
17
 
18
18
  const EMPTY_ENV: Record<string, string | undefined> = {};
19
19
 
20
20
  describe("platform-only boot with empty env", () => {
21
- it("SpecialtyRegistry resolves the default specialist type from the shipped agent-types/ without warning", () => {
22
- const warnings: string[] = [];
23
- const registry = new SpecialtyRegistry({
24
- typesDir: resolveSpecialtiesDir(EMPTY_ENV),
25
- defaultTypeName: resolveDefaultSpecialtyName(EMPTY_ENV),
26
- warn: (m) => warnings.push(m),
27
- });
28
- const manifest = registry.getManifest(
29
- resolveDefaultSpecialtyName(EMPTY_ENV),
30
- );
31
- expect(manifest.metadata.name).toBe("specialist");
32
- expect(warnings).toEqual([]);
33
- expect(registry.listTypes().map((t) => t.name)).toContain("specialist");
21
+ it("the default type resolves to the built-in specialist type", () => {
22
+ const defaultName = resolveDefaultSpecialtyName(EMPTY_ENV);
23
+ const types = new SpecialtyTypes({ defaultTypeName: defaultName });
24
+ expect(defaultName).toBe("specialist");
25
+ expect(types.tryGetType(defaultName)).toBe(SPECIALIST_TYPE);
26
+ expect(types.listTypes().map((t) => t.name)).toEqual(["specialist"]);
34
27
  });
35
28
 
36
- it("an unknown type warns and resolves to the shipped specialist manifest", () => {
37
- const warnings: string[] = [];
38
- const registry = new SpecialtyRegistry({
39
- typesDir: SHIPPED_AGENT_TYPES_DIR,
40
- defaultTypeName: "specialist",
41
- warn: (m) => warnings.push(m),
42
- });
43
- expect(registry.getManifest("no-such-type").metadata.name).toBe(
44
- "specialist",
45
- );
46
- expect(warnings).toHaveLength(1);
29
+ it("a stored legacy type still resolves to the specialist floor, but is not a valid new type", () => {
30
+ const types = new SpecialtyTypes({ defaultTypeName: "specialist" });
31
+ expect(types.getType("no-such-type")).toBe(SPECIALIST_TYPE);
32
+ expect(types.tryGetType("no-such-type")).toBeUndefined();
47
33
  });
48
34
 
49
35
  it("runtime resolveTemplatesDir agrees: the default type's shipped templates, no warning", () => {
@@ -56,6 +42,12 @@ describe("platform-only boot with empty env", () => {
56
42
  );
57
43
  expect(warnings).toEqual([]);
58
44
  expect(dir).toBe(join(SHIPPED_AGENT_TYPES_DIR, "specialist", "templates"));
45
+ expect(dir).toBe(
46
+ join(import.meta.dir, "..", SPECIALIST_TYPE.templatesDir).replace(
47
+ /\/$/,
48
+ "",
49
+ ),
50
+ );
59
51
  expect(existsSync(join(dir, "CLAUDE.md.template"))).toBe(true);
60
52
  });
61
53
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-vitals/agent-runtime",
3
- "version": "0.16.2",
3
+ "version": "1.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/app-vitals/agent.git",
@@ -40,8 +40,8 @@
40
40
  "./workspace-paths": "./workspace-paths.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@app-vitals/agent-admin": "0.16.2",
44
- "@app-vitals/agent-lib": "0.16.2",
43
+ "@app-vitals/agent-admin": "1.0.0",
44
+ "@app-vitals/agent-lib": "1.0.0",
45
45
  "@octokit/auth-app": "^8.2.0",
46
46
  "@sentry/bun": "^10.63.0",
47
47
  "@slack/bolt": "^5.0.0",
@@ -92,18 +92,9 @@ describe("fixture specialty templates", () => {
92
92
  });
93
93
 
94
94
  describe("coding is not special-cased (PCD-2.2)", () => {
95
- const writeCodingManifest = (templatesDir: string) => {
95
+ it("resolves coding through the templates/ convention under the specialties dir", () => {
96
96
  const specialties = join(root, "specialties");
97
- mkdirSync(join(specialties, "coding"), { recursive: true });
98
- writeFileSync(
99
- join(specialties, "coding", "manifest.yaml"),
100
- `identity:\n templatesDir: ${templatesDir}\n`,
101
- );
102
- return specialties;
103
- };
104
-
105
- it("resolves coding through its manifest identity.templatesDir", () => {
106
- const tpl = join(root, "coding-tpl");
97
+ const tpl = join(specialties, "coding", "templates");
107
98
  mkdirSync(tpl, { recursive: true });
108
99
  for (const f of [
109
100
  "CLAUDE.md.template",
@@ -113,7 +104,7 @@ describe("coding is not special-cased (PCD-2.2)", () => {
113
104
  writeFileSync(join(tpl, f), "x");
114
105
  const warnings: string[] = [];
115
106
  const dir = resolveTemplatesDir("coding", root, (m) => warnings.push(m), {
116
- SHIPWRIGHT_SPECIALTIES_DIR: writeCodingManifest(tpl),
107
+ SHIPWRIGHT_SPECIALTIES_DIR: specialties,
117
108
  });
118
109
  expect(dir).toBe(tpl);
119
110
  expect(warnings).toEqual([]);
package/setup.ts CHANGED
@@ -20,12 +20,13 @@ import {
20
20
  unlinkSync,
21
21
  writeFileSync,
22
22
  } from "node:fs";
23
- import { dirname, join, resolve } from "node:path";
23
+ import { join, resolve } from "node:path";
24
24
  import type { AgentPlugin } from "@app-vitals/agent-admin";
25
25
  import {
26
26
  resolveDefaultSpecialtyName,
27
27
  resolveSpecialtiesDir,
28
28
  } from "@app-vitals/agent-lib/default-specialty";
29
+ import { SPECIALIST_TYPE } from "@app-vitals/agent-lib/specialist-type";
29
30
 
30
31
  // ---------------------------------------------------------------------------
31
32
  // Template loading
@@ -69,17 +70,17 @@ const REQUIRED_TEMPLATES: readonly string[] = [
69
70
  /**
70
71
  * Resolves the workspace template directory for a specialty (ARD-6.1).
71
72
  *
72
- * Reads `identity.templatesDir` from `<specialtiesDir>/<specialty>/manifest.yaml`,
73
- * where specialtiesDir is SHIPWRIGHT_SPECIALTIES_DIR or the shipped
74
- * `agent-types/` (shared helper: @app-vitals/agent-lib/default-specialty). A
75
- * relative templatesDir resolves against the repo root, then the specialties
76
- * dir's parent (how the shipped manifests spell it: `agent-types/<t>/templates/`),
77
- * then the manifest's own directory. An absent type resolves as the default
78
- * type (`specialist`, or SHIPWRIGHT_DEFAULT_AGENT_TYPE). Returns the built-in
79
- * (generic) workspace/ dir for any type that does not resolve (no manifest,
80
- * unparseable, or the directory is missing or lacks a required template),
81
- * logging a warning. An unknown requested type first falls back to the default
82
- * type's templates (with a warning) before the built-in dir. Never throws: a template lookup must not fail boot.
73
+ * A type's templates live at `<specialtiesDir>/<specialty>/templates/`, where
74
+ * specialtiesDir is SHIPWRIGHT_SPECIALTIES_DIR or the shipped `agent-types/`
75
+ * (shared helper: @app-vitals/agent-lib/default-specialty). The built-in
76
+ * specialist type additionally resolves its record's `templatesDir`
77
+ * (lib/specialist-type.ts) against the repo root. An absent type resolves as
78
+ * the default type (`specialist`, or SHIPWRIGHT_DEFAULT_AGENT_TYPE). Returns
79
+ * the built-in (generic) workspace/ dir for any type that does not resolve
80
+ * (the directory is missing or lacks a required template), logging a warning.
81
+ * An unknown requested type first falls back to the default type's templates
82
+ * (with a warning) before the built-in dir. Never throws: a template lookup
83
+ * must not fail boot.
83
84
  */
84
85
  export function resolveTemplatesDir(
85
86
  requestedType: string | undefined,
@@ -93,24 +94,13 @@ export function resolveTemplatesDir(
93
94
  const specialtiesDir = resolveSpecialtiesDir(env);
94
95
 
95
96
  const lookup = (name: string): string => {
96
- const manifestPath = join(specialtiesDir, name, "manifest.yaml");
97
- const manifest = Bun.YAML.parse(readFileSync(manifestPath, "utf8")) as {
98
- identity?: { templatesDir?: unknown };
99
- };
100
- const templatesDir = manifest?.identity?.templatesDir;
101
- if (typeof templatesDir !== "string" || templatesDir === "") {
102
- throw new Error("manifest has no identity.templatesDir");
97
+ const candidates = [join(specialtiesDir, name, "templates")];
98
+ if (name === SPECIALIST_TYPE.name) {
99
+ candidates.push(resolve(repoRoot, SPECIALIST_TYPE.templatesDir));
103
100
  }
104
- // Manifests spell templatesDir relative to the repo root, or (shipped
105
- // agent-types/, where the repo root may not be the image root) relative to
106
- // the specialties dir's parent; a self-contained specialty (e.g. the
107
- // platform test fixture) spells it relative to its own directory.
108
101
  const dir =
109
- [
110
- resolve(repoRoot, templatesDir),
111
- resolve(dirname(specialtiesDir), templatesDir),
112
- ].find((candidate) => existsSync(candidate)) ??
113
- resolve(dirname(manifestPath), templatesDir);
102
+ candidates.find((candidate) => existsSync(candidate)) ??
103
+ (candidates[0] as string);
114
104
  if (!statSync(dir).isDirectory()) {
115
105
  throw new Error(`${dir} is not a directory`);
116
106
  }
@@ -165,7 +155,7 @@ function readTemplate(
165
155
  `Missing workspace template: ${path}. The runtime ships no product ` +
166
156
  "templates of its own: set SHIPWRIGHT_SPECIALTIES_DIR to a directory " +
167
157
  "of specialties and SHIPWRIGHT_DEFAULT_AGENT_TYPE to the specialty " +
168
- "whose identity.templatesDir provides it.",
158
+ "whose templates/ directory provides it.",
169
159
  );
170
160
  }
171
161
  }
@@ -162,7 +162,7 @@ describe("discoverBakedMarketplaces", () => {
162
162
  });
163
163
 
164
164
  describe("resolveTemplatesDir option resolution (AEP-1.2)", () => {
165
- it("resolves the shipped specialist manifest to agent-types/specialist/templates/ with no warning (SAI-1.2)", async () => {
165
+ it("resolves the shipped specialist type to agent-types/specialist/templates/ with no warning (SAI-1.2)", async () => {
166
166
  const { resolveTemplatesDir } = await import("./setup.ts");
167
167
  const repoRoot = join(import.meta.dir, "..");
168
168
  const warnings: string[] = [];
@@ -234,10 +234,10 @@ describe("resolveTemplatesDir option resolution (AEP-1.2)", () => {
234
234
  SHIPWRIGHT_SPECIALTIES_DIR: "/fixture/types",
235
235
  },
236
236
  );
237
- // Missing fixture manifest → warns naming the type, falls back built-in.
237
+ // Missing fixture templates dir → warns naming the type, falls back built-in.
238
238
  expect(warnings).toHaveLength(1);
239
239
  expect(warnings[0]).toContain('"base"');
240
- expect(warnings[0]).toContain("/fixture/types/base/manifest.yaml");
240
+ expect(warnings[0]).toContain("/fixture/types/base/templates");
241
241
  expect(dir).toMatch(/workspace$/);
242
242
  });
243
243
  });
@@ -288,22 +288,18 @@ describe("SHIPWRIGHT_REPO_ROOT override (PCB-1.1)", () => {
288
288
  const makeRoot = () => {
289
289
  const root = mkdtempSync(join(tmpdir(), "repo-root-"));
290
290
  const dir = join(root, "types", "foo");
291
- mkdirSync(join(dir, "tpl"), { recursive: true });
292
- writeFileSync(
293
- join(dir, "manifest.yaml"),
294
- "identity:\n templatesDir: tpl\n",
295
- );
291
+ mkdirSync(join(dir, "templates"), { recursive: true });
296
292
  for (const f of [
297
293
  "CLAUDE.md.template",
298
294
  "SOUL.md.template",
299
295
  "IDENTITY.md.template",
300
296
  ]) {
301
- writeFileSync(join(dir, "tpl", f), "x");
297
+ writeFileSync(join(dir, "templates", f), "x");
302
298
  }
303
- return { root, tpl: join(dir, "tpl") };
299
+ return { root, tpl: join(dir, "templates") };
304
300
  };
305
301
 
306
- it("resolves templatesDir against SHIPWRIGHT_REPO_ROOT when set", () => {
302
+ it("resolves a type's templates/ under SHIPWRIGHT_SPECIALTIES_DIR when SHIPWRIGHT_REPO_ROOT is set", () => {
307
303
  const { root, tpl } = makeRoot();
308
304
  try {
309
305
  const warnings: string[] = [];
@@ -4,7 +4,7 @@
4
4
  * The platform's SPECIALTY.md.template is the generic fallback for every agent
5
5
  * type, so it must stay a neutral stub. Specialty-specific pipeline content
6
6
  * (e.g. the coding specialty's Shipwright workflow) is shipped by the specialty
7
- * itself through its manifest's identity.templatesDir.
7
+ * itself in its templates/ directory.
8
8
  *
9
9
  * Content-assertion only: readFileSync, no I/O beyond local file reads.
10
10
  */
@@ -1,35 +0,0 @@
1
- # Minimal fixture specialty (AGR-1.9). Platform tests point
2
- # SHIPWRIGHT_SPECIALTIES_DIR at the parent of this directory and
3
- # SHIPWRIGHT_DEFAULT_AGENT_TYPE at "fixture", so the platform test suite needs
4
- # no coding specialty. It is test data, not a product specialty: no crons, no
5
- # plugins, no GitHub scope.
6
- apiVersion: shipwright.dev/v1alpha1
7
- kind: Specialty
8
- metadata:
9
- name: fixture
10
- displayName: Fixture Agent
11
- description: Minimal specialty used by the platform test suite.
12
- version: 1.0.0
13
- skills: []
14
- identity:
15
- # Resolved against the repo root first, then against this manifest's own directory.
16
- templatesDir: templates/
17
- crons: []
18
- plugins:
19
- - fixture-plugin
20
- tools:
21
- - Bash
22
- - WebSearch
23
- env:
24
- required:
25
- - key: CLAUDE_CODE_OAUTH_TOKEN
26
- description: Claude Code OAuth token used to authenticate the agent's Claude sessions.
27
- secret: true
28
- optional:
29
- - key: GH_TOKEN
30
- description: GitHub Personal Access Token.
31
- secret: true
32
- members: []
33
- repos: []
34
- chat: true
35
- voice: true