@app-vitals/agent-runtime 0.15.0 → 0.16.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.
- package/default-specialty.shipped.integration.test.ts +61 -0
- package/package.json +3 -3
- package/setup.fixture.integration.test.ts +42 -0
- package/setup.ts +52 -32
- package/setup.unit.test.ts +52 -6
- package/workspace/SPECIALTY.md.template +5 -36
- package/workspace/SPECIALTY.md.template.content.test.ts +21 -30
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-only boot against the REAL shipped agent-types/ directory with an
|
|
3
|
+
* empty environment (PCD-3.1). Env is passed explicitly: the test preload
|
|
4
|
+
* presets fixture env vars on process.env, so nothing here reads it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "bun:test";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { SpecialtyRegistry } from "@app-vitals/agent-admin/specialty-manifest-loader";
|
|
11
|
+
import {
|
|
12
|
+
resolveDefaultSpecialtyName,
|
|
13
|
+
resolveSpecialtiesDir,
|
|
14
|
+
SHIPPED_AGENT_TYPES_DIR,
|
|
15
|
+
} from "@app-vitals/agent-lib/default-specialty";
|
|
16
|
+
import { resolveTemplatesDir } from "./setup.ts";
|
|
17
|
+
|
|
18
|
+
const EMPTY_ENV: Record<string, string | undefined> = {};
|
|
19
|
+
|
|
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");
|
|
34
|
+
});
|
|
35
|
+
|
|
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);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("runtime resolveTemplatesDir agrees: the default type's shipped templates, no warning", () => {
|
|
50
|
+
const warnings: string[] = [];
|
|
51
|
+
const dir = resolveTemplatesDir(
|
|
52
|
+
undefined,
|
|
53
|
+
join(import.meta.dir, ".."),
|
|
54
|
+
(m) => warnings.push(m),
|
|
55
|
+
EMPTY_ENV,
|
|
56
|
+
);
|
|
57
|
+
expect(warnings).toEqual([]);
|
|
58
|
+
expect(dir).toBe(join(SHIPPED_AGENT_TYPES_DIR, "specialist", "templates"));
|
|
59
|
+
expect(existsSync(join(dir, "CLAUDE.md.template"))).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@app-vitals/agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
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.
|
|
44
|
-
"@app-vitals/agent-lib": "0.
|
|
43
|
+
"@app-vitals/agent-admin": "0.16.1",
|
|
44
|
+
"@app-vitals/agent-lib": "0.16.1",
|
|
45
45
|
"@octokit/auth-app": "^8.2.0",
|
|
46
46
|
"@sentry/bun": "^10.63.0",
|
|
47
47
|
"@slack/bolt": "^5.0.0",
|
|
@@ -91,6 +91,48 @@ describe("fixture specialty templates", () => {
|
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
describe("coding is not special-cased (PCD-2.2)", () => {
|
|
95
|
+
const writeCodingManifest = (templatesDir: string) => {
|
|
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");
|
|
107
|
+
mkdirSync(tpl, { recursive: true });
|
|
108
|
+
for (const f of [
|
|
109
|
+
"CLAUDE.md.template",
|
|
110
|
+
"SOUL.md.template",
|
|
111
|
+
"IDENTITY.md.template",
|
|
112
|
+
])
|
|
113
|
+
writeFileSync(join(tpl, f), "x");
|
|
114
|
+
const warnings: string[] = [];
|
|
115
|
+
const dir = resolveTemplatesDir("coding", root, (m) => warnings.push(m), {
|
|
116
|
+
SHIPWRIGHT_SPECIALTIES_DIR: writeCodingManifest(tpl),
|
|
117
|
+
});
|
|
118
|
+
expect(dir).toBe(tpl);
|
|
119
|
+
expect(warnings).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("falls back to the built-in dir with a warning when coding is unresolvable", () => {
|
|
123
|
+
const warnings: string[] = [];
|
|
124
|
+
const dir = resolveTemplatesDir("coding", root, (m) => warnings.push(m), {
|
|
125
|
+
SHIPWRIGHT_SPECIALTIES_DIR: join(root, "no-specialties"),
|
|
126
|
+
});
|
|
127
|
+
expect(dir).toMatch(/workspace$/);
|
|
128
|
+
// Unknown "coding" warns and tries the default type, which is also absent
|
|
129
|
+
// here, so a second warning precedes the built-in fallback.
|
|
130
|
+
expect(warnings).toHaveLength(2);
|
|
131
|
+
expect(warnings[0]).toContain('"coding"');
|
|
132
|
+
expect(warnings[1]).toContain("built-in");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
94
136
|
describe("no default specialty configured", () => {
|
|
95
137
|
it("names the env vars when the default specialty's templates cannot be found", () => {
|
|
96
138
|
const emptyDir = join(root, "empty-templates");
|
package/setup.ts
CHANGED
|
@@ -22,6 +22,10 @@ import {
|
|
|
22
22
|
} from "node:fs";
|
|
23
23
|
import { dirname, join, resolve } from "node:path";
|
|
24
24
|
import type { AgentPlugin } from "@app-vitals/agent-admin";
|
|
25
|
+
import {
|
|
26
|
+
resolveDefaultSpecialtyName,
|
|
27
|
+
resolveSpecialtiesDir,
|
|
28
|
+
} from "@app-vitals/agent-lib/default-specialty";
|
|
25
29
|
|
|
26
30
|
// ---------------------------------------------------------------------------
|
|
27
31
|
// Template loading
|
|
@@ -59,10 +63,6 @@ const SHIPWRIGHT_MARKETPLACE = "shipwright";
|
|
|
59
63
|
// Default plugin installed on all Shipwright agents.
|
|
60
64
|
const DEFAULT_PLUGINS: readonly string[] = ["shipwright"];
|
|
61
65
|
|
|
62
|
-
// The universal fallback type — mirrors admin's DEFAULT_SPECIALTY_NAME. Its
|
|
63
|
-
// templates are the built-in workspace/ dir, so it never needs a manifest lookup.
|
|
64
|
-
const DEFAULT_SPECIALTY_NAME = "coding";
|
|
65
|
-
|
|
66
66
|
// The persona templates a type's templatesDir must provide or it is not usable.
|
|
67
67
|
// Everything else (VOICE, mise, requirements, BOOTSTRAP) falls back per file to
|
|
68
68
|
// the built-in workspace/ dir, and SPECIALTY.md.template is optional (ARD-6.2).
|
|
@@ -75,12 +75,17 @@ const REQUIRED_TEMPLATES: readonly string[] = [
|
|
|
75
75
|
/**
|
|
76
76
|
* Resolves the workspace template directory for a specialty (ARD-6.1).
|
|
77
77
|
*
|
|
78
|
-
* Reads `identity.templatesDir` from `<
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
78
|
+
* Reads `identity.templatesDir` from `<specialtiesDir>/<specialty>/manifest.yaml`,
|
|
79
|
+
* where specialtiesDir is SHIPWRIGHT_SPECIALTIES_DIR or the shipped
|
|
80
|
+
* `agent-types/` (shared helper: @app-vitals/agent-lib/default-specialty). A
|
|
81
|
+
* relative templatesDir resolves against the repo root, then the specialties
|
|
82
|
+
* dir's parent (how the shipped manifests spell it: `agent-types/<t>/templates/`),
|
|
83
|
+
* then the manifest's own directory. An absent type resolves as the default
|
|
84
|
+
* type (`specialist`, or SHIPWRIGHT_DEFAULT_AGENT_TYPE). Returns the built-in
|
|
85
|
+
* (generic) workspace/ dir for any type that does not resolve (no manifest,
|
|
86
|
+
* unparseable, or the directory is missing or lacks a required template),
|
|
87
|
+
* logging a warning. An unknown requested type first falls back to the default
|
|
88
|
+
* type's templates (with a warning) before the built-in dir. Never throws: a template lookup must not fail boot.
|
|
84
89
|
*/
|
|
85
90
|
export function resolveTemplatesDir(
|
|
86
91
|
requestedType: string | undefined,
|
|
@@ -89,19 +94,12 @@ export function resolveTemplatesDir(
|
|
|
89
94
|
env: Record<string, string | undefined> = process.env,
|
|
90
95
|
): string {
|
|
91
96
|
const repoRoot = repoRootOverride ?? resolveShipwrightRepoRoot(env);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
try {
|
|
100
|
-
const manifestPath = join(
|
|
101
|
-
env.SHIPWRIGHT_SPECIALTIES_DIR || join(repoRoot, "specialties"),
|
|
102
|
-
specialty,
|
|
103
|
-
"manifest.yaml",
|
|
104
|
-
);
|
|
97
|
+
const defaultSpecialty = resolveDefaultSpecialtyName(env);
|
|
98
|
+
const specialty = requestedType || defaultSpecialty;
|
|
99
|
+
const specialtiesDir = resolveSpecialtiesDir(env);
|
|
100
|
+
|
|
101
|
+
const lookup = (name: string): string => {
|
|
102
|
+
const manifestPath = join(specialtiesDir, name, "manifest.yaml");
|
|
105
103
|
const manifest = Bun.YAML.parse(readFileSync(manifestPath, "utf8")) as {
|
|
106
104
|
identity?: { templatesDir?: unknown };
|
|
107
105
|
};
|
|
@@ -109,27 +107,49 @@ export function resolveTemplatesDir(
|
|
|
109
107
|
if (typeof templatesDir !== "string" || templatesDir === "") {
|
|
110
108
|
throw new Error("manifest has no identity.templatesDir");
|
|
111
109
|
}
|
|
112
|
-
// Manifests
|
|
113
|
-
// repo root
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
const dir =
|
|
117
|
-
|
|
118
|
-
|
|
110
|
+
// Manifests spell templatesDir relative to the repo root, or (shipped
|
|
111
|
+
// agent-types/, where the repo root may not be the image root) relative to
|
|
112
|
+
// the specialties dir's parent; a self-contained specialty (e.g. the
|
|
113
|
+
// platform test fixture) spells it relative to its own directory.
|
|
114
|
+
const dir =
|
|
115
|
+
[
|
|
116
|
+
resolve(repoRoot, templatesDir),
|
|
117
|
+
resolve(dirname(specialtiesDir), templatesDir),
|
|
118
|
+
].find((candidate) => existsSync(candidate)) ??
|
|
119
|
+
resolve(dirname(manifestPath), templatesDir);
|
|
119
120
|
if (!statSync(dir).isDirectory()) {
|
|
120
121
|
throw new Error(`${dir} is not a directory`);
|
|
121
122
|
}
|
|
122
123
|
const missing = REQUIRED_TEMPLATES.filter(
|
|
123
|
-
(
|
|
124
|
+
(file) => !existsSync(join(dir, file)),
|
|
124
125
|
);
|
|
125
126
|
if (missing.length > 0) {
|
|
126
127
|
throw new Error(`${dir} is missing templates: ${missing.join(", ")}`);
|
|
127
128
|
}
|
|
128
129
|
return dir;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
return lookup(specialty);
|
|
129
134
|
} catch (err) {
|
|
135
|
+
if (specialty !== defaultSpecialty) {
|
|
136
|
+
warn(
|
|
137
|
+
`[setup] could not resolve workspace templates for specialty "${specialty}" ` +
|
|
138
|
+
`(${(err as Error).message}) — falling back to the default type "${defaultSpecialty}"`,
|
|
139
|
+
);
|
|
140
|
+
try {
|
|
141
|
+
return lookup(defaultSpecialty);
|
|
142
|
+
} catch (defaultErr) {
|
|
143
|
+
warn(
|
|
144
|
+
`[setup] could not resolve workspace templates for default specialty "${defaultSpecialty}" ` +
|
|
145
|
+
`(${(defaultErr as Error).message}) — falling back to the built-in workspace templates`,
|
|
146
|
+
);
|
|
147
|
+
return WORKSPACE_TEMPLATE_DIR;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
130
150
|
warn(
|
|
131
151
|
`[setup] could not resolve workspace templates for specialty "${specialty}" ` +
|
|
132
|
-
`(${(err as Error).message}) — falling back to
|
|
152
|
+
`(${(err as Error).message}) — falling back to the built-in workspace templates`,
|
|
133
153
|
);
|
|
134
154
|
return WORKSPACE_TEMPLATE_DIR;
|
|
135
155
|
}
|
package/setup.unit.test.ts
CHANGED
|
@@ -281,9 +281,49 @@ describe("resolveTemplatesDir option resolution (AEP-1.2)", () => {
|
|
|
281
281
|
expect(warnings).toEqual([]);
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
-
it("resolves an absent type
|
|
284
|
+
it("resolves an absent type as the shipped specialist type with no env and no warning", async () => {
|
|
285
285
|
const { resolveTemplatesDir } = await import("./setup.ts");
|
|
286
|
-
const
|
|
286
|
+
const warnings: string[] = [];
|
|
287
|
+
const dir = resolveTemplatesDir(
|
|
288
|
+
undefined,
|
|
289
|
+
join(import.meta.dir, ".."),
|
|
290
|
+
(m) => warnings.push(m),
|
|
291
|
+
{},
|
|
292
|
+
);
|
|
293
|
+
expect(warnings).toEqual([]);
|
|
294
|
+
expect(dir).toBe(
|
|
295
|
+
join(import.meta.dir, "..", "agent-types", "specialist", "templates"),
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("warns and falls back to the default type's templates for an unknown requested type", async () => {
|
|
300
|
+
const { resolveTemplatesDir } = await import("./setup.ts");
|
|
301
|
+
const warnings: string[] = [];
|
|
302
|
+
const dir = resolveTemplatesDir(
|
|
303
|
+
"renamed-type",
|
|
304
|
+
join(import.meta.dir, ".."),
|
|
305
|
+
(m) => warnings.push(m),
|
|
306
|
+
{},
|
|
307
|
+
);
|
|
308
|
+
expect(warnings).toHaveLength(1);
|
|
309
|
+
expect(warnings[0]).toContain('"renamed-type"');
|
|
310
|
+
expect(warnings[0]).toContain('"specialist"');
|
|
311
|
+
expect(dir).toBe(
|
|
312
|
+
join(import.meta.dir, "..", "agent-types", "specialist", "templates"),
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("falls back to the built-in templates when the default type cannot load either", async () => {
|
|
317
|
+
const { resolveTemplatesDir } = await import("./setup.ts");
|
|
318
|
+
const warnings: string[] = [];
|
|
319
|
+
const dir = resolveTemplatesDir(
|
|
320
|
+
"renamed-type",
|
|
321
|
+
"/nonexistent",
|
|
322
|
+
(m) => warnings.push(m),
|
|
323
|
+
{ SHIPWRIGHT_SPECIALTIES_DIR: "/nonexistent/types" },
|
|
324
|
+
);
|
|
325
|
+
expect(warnings).toHaveLength(2);
|
|
326
|
+
expect(warnings[1]).toContain("built-in");
|
|
287
327
|
expect(dir).toMatch(/workspace$/);
|
|
288
328
|
});
|
|
289
329
|
|
|
@@ -347,9 +387,12 @@ describe("SHIPWRIGHT_REPO_ROOT override (PCB-1.1)", () => {
|
|
|
347
387
|
describe("resolveTemplatesDir", () => {
|
|
348
388
|
const makeRoot = () => {
|
|
349
389
|
const root = mkdtempSync(join(tmpdir(), "repo-root-"));
|
|
350
|
-
const dir = join(root, "
|
|
390
|
+
const dir = join(root, "types", "foo");
|
|
351
391
|
mkdirSync(join(dir, "tpl"), { recursive: true });
|
|
352
|
-
writeFileSync(
|
|
392
|
+
writeFileSync(
|
|
393
|
+
join(dir, "manifest.yaml"),
|
|
394
|
+
"identity:\n templatesDir: tpl\n",
|
|
395
|
+
);
|
|
353
396
|
for (const f of [
|
|
354
397
|
"CLAUDE.md.template",
|
|
355
398
|
"SOUL.md.template",
|
|
@@ -360,7 +403,7 @@ describe("SHIPWRIGHT_REPO_ROOT override (PCB-1.1)", () => {
|
|
|
360
403
|
return { root, tpl: join(dir, "tpl") };
|
|
361
404
|
};
|
|
362
405
|
|
|
363
|
-
it("
|
|
406
|
+
it("resolves templatesDir against SHIPWRIGHT_REPO_ROOT when set", () => {
|
|
364
407
|
const { root, tpl } = makeRoot();
|
|
365
408
|
try {
|
|
366
409
|
const warnings: string[] = [];
|
|
@@ -368,7 +411,10 @@ describe("SHIPWRIGHT_REPO_ROOT override (PCB-1.1)", () => {
|
|
|
368
411
|
"foo",
|
|
369
412
|
undefined,
|
|
370
413
|
(m) => warnings.push(m),
|
|
371
|
-
{
|
|
414
|
+
{
|
|
415
|
+
SHIPWRIGHT_REPO_ROOT: root,
|
|
416
|
+
SHIPWRIGHT_SPECIALTIES_DIR: join(root, "types"),
|
|
417
|
+
},
|
|
372
418
|
);
|
|
373
419
|
expect(warnings).toEqual([]);
|
|
374
420
|
expect(dir).toBe(tpl);
|
|
@@ -1,37 +1,6 @@
|
|
|
1
|
-
###
|
|
2
|
-
|
|
3
|
-
You are a software engineering agent. All planning and code changes use the shipwright workflow:
|
|
4
|
-
|
|
5
|
-
- **Planning:** `/shipwright:plan-session`
|
|
6
|
-
- **Execution:** `/shipwright:dev-task` (single) or `/shipwright:dev-loop` (continuous)
|
|
7
|
-
- **Review:** `/shipwright:review`
|
|
8
|
-
|
|
9
|
-
The work queue lives in the hosted task-store service — query it directly:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
curl -sf -H "Authorization: Bearer $SHIPWRIGHT_TASK_STORE_TOKEN" \
|
|
13
|
-
"$SHIPWRIGHT_TASK_STORE_URL/tasks?ready=true" | jq '.tasks'
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
`dev-task`, `review`, `patch`, and `deploy` are **explicit-target-only executors** — none of
|
|
17
|
-
them self-discovers work. Candidate selection happens upstream, once per tick, in the
|
|
18
|
-
`shipwright-loop` cron: its candidate providers merge dev-task/review/patch/deploy
|
|
19
|
-
candidates and pick exactly one winning item via strict age-based FIFO, then dispatch the
|
|
20
|
-
matching command with the winning task id or `org/repo#number` embedded directly in the
|
|
21
|
-
prompt. `dev-task`, `review`, and `patch` are enabled by default; `deploy` is opt-in. A
|
|
22
|
-
standalone phase cron with `shipwright-loop` disabled is **silently inert** — its prompt
|
|
23
|
-
carries no target, so every tick responds `[silent]` and does nothing.
|
|
24
|
-
|
|
25
|
-
### Policy
|
|
26
|
-
|
|
27
|
-
Agent behavior is controlled by the policy fields on the agent record (review
|
|
28
|
-
posting, self-review, confidence and finding limits, worktree cleanup),
|
|
29
|
-
delivered via `GET /agents/:id/config` and edited through the agents UI or
|
|
30
|
-
`PATCH /agents/:id`.
|
|
31
|
-
|
|
32
|
-
### Reviews
|
|
33
|
-
|
|
34
|
-
Review state is tracked in the task store's `PullRequest` record (`staged` +
|
|
35
|
-
`commitSha` fields). Review artifacts (PR_REVIEW files and review JSON) live in
|
|
36
|
-
`state/reviews/`. See `/shipwright:review` for the full flow.
|
|
1
|
+
### Specialty
|
|
37
2
|
|
|
3
|
+
This workspace was seeded from the platform's generic fallback template because
|
|
4
|
+
no specialty-specific workspace content was available for this agent type.
|
|
5
|
+
Describe this agent's specialty, tools, and workflow here. Existing workspace
|
|
6
|
+
files are never rewritten, so edits to this file persist.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SPECIALTY.md.template content regression guard —
|
|
2
|
+
* SPECIALTY.md.template content regression guard — PCD-2.2
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the
|
|
7
|
-
*
|
|
4
|
+
* The platform's SPECIALTY.md.template is the generic fallback for every agent
|
|
5
|
+
* type, so it must stay a neutral stub. Specialty-specific pipeline content
|
|
6
|
+
* (e.g. the coding specialty's Shipwright workflow) is shipped by the specialty
|
|
7
|
+
* itself through its manifest's identity.templatesDir.
|
|
8
8
|
*
|
|
9
9
|
* Content-assertion only: readFileSync, no I/O beyond local file reads.
|
|
10
10
|
*/
|
|
@@ -19,32 +19,23 @@ function readTemplate(): string {
|
|
|
19
19
|
return readFileSync(TEMPLATE_PATH, "utf8");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
describe("SPECIALTY.md.template —
|
|
23
|
-
it("
|
|
24
|
-
expect(readTemplate()).
|
|
22
|
+
describe("SPECIALTY.md.template — generic stub", () => {
|
|
23
|
+
it("is a non-empty stub", () => {
|
|
24
|
+
expect(readTemplate().trim().length).toBeGreaterThan(0);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
expect(readTemplate()).not.toContain(
|
|
41
|
-
"picks up the next pending item every 30 minutes",
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("does not describe review/patch as independently self-discovering phases", () => {
|
|
46
|
-
expect(readTemplate()).not.toContain(
|
|
47
|
-
"run as independent phases — review posts",
|
|
48
|
-
);
|
|
27
|
+
it.each([
|
|
28
|
+
"shipwright",
|
|
29
|
+
"dev-task",
|
|
30
|
+
"dev-loop",
|
|
31
|
+
"plan-session",
|
|
32
|
+
"task-store",
|
|
33
|
+
"TASK_STORE",
|
|
34
|
+
"PullRequest",
|
|
35
|
+
"explicit-target-only",
|
|
36
|
+
"fifo",
|
|
37
|
+
"curl",
|
|
38
|
+
])("does not mention Shipwright-pipeline term %p", (term) => {
|
|
39
|
+
expect(readTemplate().toLowerCase()).not.toContain(term.toLowerCase());
|
|
49
40
|
});
|
|
50
41
|
});
|