@app-vitals/agent-runtime 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.
- package/Dockerfile +1 -1
- package/default-specialty.shipped.integration.test.ts +18 -26
- package/package.json +3 -3
- package/setup.fixture.integration.test.ts +3 -12
- package/setup.integration.test.ts +49 -53
- package/setup.ts +38 -56
- package/setup.unit.test.ts +88 -192
- package/workspace/SPECIALTY.md.template.content.test.ts +1 -1
- package/fixtures/specialties/fixture/manifest.yaml +0 -35
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
|
|
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 {
|
|
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("
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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("
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
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.
|
|
44
|
-
"@app-vitals/agent-lib": "0.
|
|
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
|
-
|
|
95
|
+
it("resolves coding through the templates/ convention under the specialties dir", () => {
|
|
96
96
|
const specialties = join(root, "specialties");
|
|
97
|
-
|
|
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:
|
|
107
|
+
SHIPWRIGHT_SPECIALTIES_DIR: specialties,
|
|
117
108
|
});
|
|
118
109
|
expect(dir).toBe(tpl);
|
|
119
110
|
expect(warnings).toEqual([]);
|
|
@@ -894,6 +894,19 @@ describe("findStalePluginSpecs", () => {
|
|
|
894
894
|
// these tests depend on ambient sandbox/image state.
|
|
895
895
|
const NO_EXTRA_MARKETPLACES: string[] = [];
|
|
896
896
|
|
|
897
|
+
const SHIPWRIGHT_ROW: AgentPlugin = {
|
|
898
|
+
plugin: "shipwright",
|
|
899
|
+
marketplace: "shipwright",
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// A repo root carrying a local marketplace (coding image); platform images ship none.
|
|
903
|
+
const repoRoot = (): string => {
|
|
904
|
+
const root = join(testHome, "repo-root");
|
|
905
|
+
mkdirSync(join(root, ".claude-plugin"), { recursive: true });
|
|
906
|
+
writeFileSync(join(root, ".claude-plugin", "marketplace.json"), "{}");
|
|
907
|
+
return root;
|
|
908
|
+
};
|
|
909
|
+
|
|
897
910
|
describe("installPlugins", () => {
|
|
898
911
|
it("registers local marketplace then installs shipwright@shipwright using the shipwright marketplace", async () => {
|
|
899
912
|
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
@@ -909,20 +922,15 @@ describe("installPlugins", () => {
|
|
|
909
922
|
await installPlugins(
|
|
910
923
|
mockExec,
|
|
911
924
|
testHome,
|
|
912
|
-
[],
|
|
913
|
-
|
|
925
|
+
[SHIPWRIGHT_ROW],
|
|
926
|
+
repoRoot(),
|
|
914
927
|
join(testHome, "nonexistent.json"),
|
|
915
928
|
NO_EXTRA_MARKETPLACES,
|
|
916
929
|
);
|
|
917
930
|
|
|
918
931
|
// marketplace add + 1 install + 1 update = 3 calls (no stale paths)
|
|
919
932
|
expect(calls).toHaveLength(3);
|
|
920
|
-
expect(calls[0].args).toEqual([
|
|
921
|
-
"plugin",
|
|
922
|
-
"marketplace",
|
|
923
|
-
"add",
|
|
924
|
-
"/repo/root",
|
|
925
|
-
]);
|
|
933
|
+
expect(calls[0].args).toEqual(["plugin", "marketplace", "add", repoRoot()]);
|
|
926
934
|
expect(calls[1].args).toEqual([
|
|
927
935
|
"plugin",
|
|
928
936
|
"install",
|
|
@@ -935,7 +943,7 @@ describe("installPlugins", () => {
|
|
|
935
943
|
]);
|
|
936
944
|
});
|
|
937
945
|
|
|
938
|
-
it("installs
|
|
946
|
+
it("installs row plugins in row order", async () => {
|
|
939
947
|
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
940
948
|
const mockExec = async (
|
|
941
949
|
cmd: string,
|
|
@@ -947,6 +955,7 @@ describe("installPlugins", () => {
|
|
|
947
955
|
};
|
|
948
956
|
|
|
949
957
|
const agentPlugins: AgentPlugin[] = [
|
|
958
|
+
SHIPWRIGHT_ROW,
|
|
950
959
|
{ marketplace: "my-marketplace", plugin: "custom-plugin" },
|
|
951
960
|
{ marketplace: "other-market", plugin: "another-plugin" },
|
|
952
961
|
];
|
|
@@ -955,7 +964,7 @@ describe("installPlugins", () => {
|
|
|
955
964
|
mockExec,
|
|
956
965
|
testHome,
|
|
957
966
|
agentPlugins,
|
|
958
|
-
|
|
967
|
+
repoRoot(),
|
|
959
968
|
join(testHome, "nonexistent.json"),
|
|
960
969
|
NO_EXTRA_MARKETPLACES,
|
|
961
970
|
);
|
|
@@ -963,14 +972,9 @@ describe("installPlugins", () => {
|
|
|
963
972
|
// 1 marketplace add + 3 installs + 3 updates = 7 calls
|
|
964
973
|
expect(calls).toHaveLength(7);
|
|
965
974
|
|
|
966
|
-
expect(calls[0].args).toEqual([
|
|
967
|
-
"plugin",
|
|
968
|
-
"marketplace",
|
|
969
|
-
"add",
|
|
970
|
-
"/repo/root",
|
|
971
|
-
]);
|
|
975
|
+
expect(calls[0].args).toEqual(["plugin", "marketplace", "add", repoRoot()]);
|
|
972
976
|
|
|
973
|
-
// installs
|
|
977
|
+
// installs in row order
|
|
974
978
|
expect(calls[1].args).toEqual([
|
|
975
979
|
"plugin",
|
|
976
980
|
"install",
|
|
@@ -987,7 +991,7 @@ describe("installPlugins", () => {
|
|
|
987
991
|
"another-plugin@other-market",
|
|
988
992
|
]);
|
|
989
993
|
|
|
990
|
-
// updates
|
|
994
|
+
// updates in row order
|
|
991
995
|
expect(calls[4].args).toEqual([
|
|
992
996
|
"plugin",
|
|
993
997
|
"update",
|
|
@@ -1005,7 +1009,7 @@ describe("installPlugins", () => {
|
|
|
1005
1009
|
]);
|
|
1006
1010
|
});
|
|
1007
1011
|
|
|
1008
|
-
it("
|
|
1012
|
+
it("no plugin rows installs nothing (platform agents)", async () => {
|
|
1009
1013
|
const calls: Array<{ args: string[] }> = [];
|
|
1010
1014
|
const mockExec = async (
|
|
1011
1015
|
_cmd: string,
|
|
@@ -1020,15 +1024,12 @@ describe("installPlugins", () => {
|
|
|
1020
1024
|
mockExec,
|
|
1021
1025
|
testHome,
|
|
1022
1026
|
[],
|
|
1023
|
-
"/repo/root",
|
|
1027
|
+
"/repo/root-without-marketplace",
|
|
1024
1028
|
join(testHome, "nonexistent.json"),
|
|
1025
1029
|
NO_EXTRA_MARKETPLACES,
|
|
1026
1030
|
);
|
|
1027
1031
|
|
|
1028
|
-
|
|
1029
|
-
expect(calls).toHaveLength(3);
|
|
1030
|
-
expect(calls[1].args).toContain("shipwright@shipwright");
|
|
1031
|
-
expect(calls[2].args).toContain("shipwright@shipwright");
|
|
1032
|
+
expect(calls).toEqual([]);
|
|
1032
1033
|
});
|
|
1033
1034
|
|
|
1034
1035
|
it("is non-fatal when the claude binary isn't available", async () => {
|
|
@@ -1040,8 +1041,8 @@ describe("installPlugins", () => {
|
|
|
1040
1041
|
installPlugins(
|
|
1041
1042
|
throwingExec,
|
|
1042
1043
|
testHome,
|
|
1043
|
-
[],
|
|
1044
|
-
|
|
1044
|
+
[SHIPWRIGHT_ROW],
|
|
1045
|
+
repoRoot(),
|
|
1045
1046
|
join(testHome, "nonexistent.json"),
|
|
1046
1047
|
NO_EXTRA_MARKETPLACES,
|
|
1047
1048
|
),
|
|
@@ -1067,8 +1068,8 @@ describe("installPlugins", () => {
|
|
|
1067
1068
|
installPlugins(
|
|
1068
1069
|
mockExec,
|
|
1069
1070
|
testHome,
|
|
1070
|
-
[],
|
|
1071
|
-
|
|
1071
|
+
[SHIPWRIGHT_ROW],
|
|
1072
|
+
repoRoot(),
|
|
1072
1073
|
join(testHome, "nonexistent.json"),
|
|
1073
1074
|
NO_EXTRA_MARKETPLACES,
|
|
1074
1075
|
),
|
|
@@ -1097,8 +1098,8 @@ describe("installPlugins", () => {
|
|
|
1097
1098
|
installPlugins(
|
|
1098
1099
|
mockExec,
|
|
1099
1100
|
testHome,
|
|
1100
|
-
[],
|
|
1101
|
-
|
|
1101
|
+
[SHIPWRIGHT_ROW],
|
|
1102
|
+
repoRoot(),
|
|
1102
1103
|
join(testHome, "nonexistent.json"),
|
|
1103
1104
|
NO_EXTRA_MARKETPLACES,
|
|
1104
1105
|
),
|
|
@@ -1123,8 +1124,8 @@ describe("installPlugins", () => {
|
|
|
1123
1124
|
installPlugins(
|
|
1124
1125
|
mockExec,
|
|
1125
1126
|
testHome,
|
|
1126
|
-
[],
|
|
1127
|
-
|
|
1127
|
+
[SHIPWRIGHT_ROW],
|
|
1128
|
+
repoRoot(),
|
|
1128
1129
|
join(testHome, "nonexistent.json"),
|
|
1129
1130
|
NO_EXTRA_MARKETPLACES,
|
|
1130
1131
|
),
|
|
@@ -1163,8 +1164,8 @@ describe("installPlugins", () => {
|
|
|
1163
1164
|
await installPlugins(
|
|
1164
1165
|
mockExec,
|
|
1165
1166
|
testHome,
|
|
1166
|
-
[],
|
|
1167
|
-
|
|
1167
|
+
[SHIPWRIGHT_ROW],
|
|
1168
|
+
repoRoot(),
|
|
1168
1169
|
manifestPath,
|
|
1169
1170
|
NO_EXTRA_MARKETPLACES,
|
|
1170
1171
|
);
|
|
@@ -1224,20 +1225,15 @@ describe("installPlugins", () => {
|
|
|
1224
1225
|
await installPlugins(
|
|
1225
1226
|
mockExec,
|
|
1226
1227
|
testHome,
|
|
1227
|
-
[],
|
|
1228
|
-
|
|
1228
|
+
[SHIPWRIGHT_ROW],
|
|
1229
|
+
repoRoot(),
|
|
1229
1230
|
manifestPath,
|
|
1230
1231
|
NO_EXTRA_MARKETPLACES,
|
|
1231
1232
|
);
|
|
1232
1233
|
|
|
1233
1234
|
// marketplace add + uninstall (failed) = 2 calls; install and update are skipped
|
|
1234
1235
|
expect(calls).toHaveLength(2);
|
|
1235
|
-
expect(calls[0].args).toEqual([
|
|
1236
|
-
"plugin",
|
|
1237
|
-
"marketplace",
|
|
1238
|
-
"add",
|
|
1239
|
-
"/repo/root",
|
|
1240
|
-
]);
|
|
1236
|
+
expect(calls[0].args).toEqual(["plugin", "marketplace", "add", repoRoot()]);
|
|
1241
1237
|
expect(calls[1].args).toEqual([
|
|
1242
1238
|
"plugin",
|
|
1243
1239
|
"uninstall",
|
|
@@ -1277,8 +1273,8 @@ describe("installPlugins", () => {
|
|
|
1277
1273
|
await installPlugins(
|
|
1278
1274
|
mockExec,
|
|
1279
1275
|
testHome,
|
|
1280
|
-
[],
|
|
1281
|
-
|
|
1276
|
+
[SHIPWRIGHT_ROW],
|
|
1277
|
+
repoRoot(),
|
|
1282
1278
|
manifestPath,
|
|
1283
1279
|
NO_EXTRA_MARKETPLACES,
|
|
1284
1280
|
);
|
|
@@ -1392,8 +1388,8 @@ describe("installPlugins — extra marketplace discovery", () => {
|
|
|
1392
1388
|
await installPlugins(
|
|
1393
1389
|
mockExec,
|
|
1394
1390
|
testHome,
|
|
1395
|
-
[],
|
|
1396
|
-
|
|
1391
|
+
[SHIPWRIGHT_ROW],
|
|
1392
|
+
repoRoot(),
|
|
1397
1393
|
join(testHome, "nonexistent.json"),
|
|
1398
1394
|
undefined, // use default discovery — pass conventionRoot via extraMarketplacesRoot
|
|
1399
1395
|
conventionRoot,
|
|
@@ -1403,7 +1399,7 @@ describe("installPlugins — extra marketplace discovery", () => {
|
|
|
1403
1399
|
const addCalls = calls.filter((c) => c.args[1] === "marketplace");
|
|
1404
1400
|
expect(addCalls.length).toBeGreaterThanOrEqual(2);
|
|
1405
1401
|
expect(addCalls[0].args[3]).toBe(extraMarketDir);
|
|
1406
|
-
expect(addCalls[1].args[3]).toBe(
|
|
1402
|
+
expect(addCalls[1].args[3]).toBe(repoRoot());
|
|
1407
1403
|
|
|
1408
1404
|
// All install calls must come after all marketplace add calls
|
|
1409
1405
|
const installIdx = calls.findIndex((c) => c.args[1] === "install");
|
|
@@ -1444,8 +1440,8 @@ describe("installPlugins — extra marketplace discovery", () => {
|
|
|
1444
1440
|
await installPlugins(
|
|
1445
1441
|
mockExec,
|
|
1446
1442
|
testHome,
|
|
1447
|
-
[],
|
|
1448
|
-
|
|
1443
|
+
[SHIPWRIGHT_ROW],
|
|
1444
|
+
repoRoot(),
|
|
1449
1445
|
join(testHome, "nonexistent.json"),
|
|
1450
1446
|
undefined,
|
|
1451
1447
|
conventionRoot,
|
|
@@ -1457,7 +1453,7 @@ describe("installPlugins — extra marketplace discovery", () => {
|
|
|
1457
1453
|
const addDirs = addCalls.map((c) => c.args[3]);
|
|
1458
1454
|
expect(addDirs).toContain(validDir);
|
|
1459
1455
|
expect(addDirs).not.toContain(invalidDir);
|
|
1460
|
-
expect(addDirs).toContain(
|
|
1456
|
+
expect(addDirs).toContain(repoRoot());
|
|
1461
1457
|
});
|
|
1462
1458
|
|
|
1463
1459
|
it("is backward compatible when no extra marketplace dirs exist", async () => {
|
|
@@ -1478,8 +1474,8 @@ describe("installPlugins — extra marketplace discovery", () => {
|
|
|
1478
1474
|
await installPlugins(
|
|
1479
1475
|
mockExec,
|
|
1480
1476
|
testHome,
|
|
1481
|
-
[],
|
|
1482
|
-
|
|
1477
|
+
[SHIPWRIGHT_ROW],
|
|
1478
|
+
repoRoot(),
|
|
1483
1479
|
join(testHome, "nonexistent.json"),
|
|
1484
1480
|
undefined,
|
|
1485
1481
|
conventionRoot,
|
|
@@ -1488,7 +1484,7 @@ describe("installPlugins — extra marketplace discovery", () => {
|
|
|
1488
1484
|
// Only the /repo/root marketplace add — same as before this feature
|
|
1489
1485
|
const addCalls = calls.filter((c) => c.args[1] === "marketplace");
|
|
1490
1486
|
expect(addCalls).toHaveLength(1);
|
|
1491
|
-
expect(addCalls[0].args[3]).toBe(
|
|
1487
|
+
expect(addCalls[0].args[3]).toBe(repoRoot());
|
|
1492
1488
|
// marketplace add + install + update = 3 calls total
|
|
1493
1489
|
expect(calls).toHaveLength(3);
|
|
1494
1490
|
});
|
package/setup.ts
CHANGED
|
@@ -20,12 +20,13 @@ import {
|
|
|
20
20
|
unlinkSync,
|
|
21
21
|
writeFileSync,
|
|
22
22
|
} from "node:fs";
|
|
23
|
-
import {
|
|
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
|
|
@@ -57,12 +58,6 @@ export function resolveShipwrightRepoRoot(
|
|
|
57
58
|
return env.SHIPWRIGHT_REPO_ROOT || DEFAULT_SHIPWRIGHT_REPO_ROOT;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
// Local marketplace name — matches the `name` field in .claude-plugin/marketplace.json.
|
|
61
|
-
const SHIPWRIGHT_MARKETPLACE = "shipwright";
|
|
62
|
-
|
|
63
|
-
// Default plugin installed on all Shipwright agents.
|
|
64
|
-
const DEFAULT_PLUGINS: readonly string[] = ["shipwright"];
|
|
65
|
-
|
|
66
61
|
// The persona templates a type's templatesDir must provide or it is not usable.
|
|
67
62
|
// Everything else (VOICE, mise, requirements, BOOTSTRAP) falls back per file to
|
|
68
63
|
// the built-in workspace/ dir, and SPECIALTY.md.template is optional (ARD-6.2).
|
|
@@ -75,17 +70,17 @@ const REQUIRED_TEMPLATES: readonly string[] = [
|
|
|
75
70
|
/**
|
|
76
71
|
* Resolves the workspace template directory for a specialty (ARD-6.1).
|
|
77
72
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* (
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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.
|
|
89
84
|
*/
|
|
90
85
|
export function resolveTemplatesDir(
|
|
91
86
|
requestedType: string | undefined,
|
|
@@ -99,24 +94,13 @@ export function resolveTemplatesDir(
|
|
|
99
94
|
const specialtiesDir = resolveSpecialtiesDir(env);
|
|
100
95
|
|
|
101
96
|
const lookup = (name: string): string => {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
};
|
|
106
|
-
const templatesDir = manifest?.identity?.templatesDir;
|
|
107
|
-
if (typeof templatesDir !== "string" || templatesDir === "") {
|
|
108
|
-
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));
|
|
109
100
|
}
|
|
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
101
|
const dir =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
resolve(dirname(specialtiesDir), templatesDir),
|
|
118
|
-
].find((candidate) => existsSync(candidate)) ??
|
|
119
|
-
resolve(dirname(manifestPath), templatesDir);
|
|
102
|
+
candidates.find((candidate) => existsSync(candidate)) ??
|
|
103
|
+
(candidates[0] as string);
|
|
120
104
|
if (!statSync(dir).isDirectory()) {
|
|
121
105
|
throw new Error(`${dir} is not a directory`);
|
|
122
106
|
}
|
|
@@ -171,7 +155,7 @@ function readTemplate(
|
|
|
171
155
|
`Missing workspace template: ${path}. The runtime ships no product ` +
|
|
172
156
|
"templates of its own: set SHIPWRIGHT_SPECIALTIES_DIR to a directory " +
|
|
173
157
|
"of specialties and SHIPWRIGHT_DEFAULT_AGENT_TYPE to the specialty " +
|
|
174
|
-
"whose
|
|
158
|
+
"whose templates/ directory provides it.",
|
|
175
159
|
);
|
|
176
160
|
}
|
|
177
161
|
}
|
|
@@ -399,15 +383,15 @@ export function findStalePluginSpecs(
|
|
|
399
383
|
}
|
|
400
384
|
|
|
401
385
|
/**
|
|
402
|
-
* Installs the
|
|
403
|
-
*
|
|
386
|
+
* Installs the plugins in the agent's own AgentPlugin rows (seeded from the
|
|
387
|
+
* specialty manifest at create time). No plugin is installed by default. All commands are idempotent (exit 0 when already
|
|
404
388
|
* present / already at latest), so reboots are a silent no-op.
|
|
405
389
|
*
|
|
406
390
|
* Non-fatal — a missing `claude` binary or network blip logs and returns
|
|
407
391
|
* without throwing, so startup is never blocked.
|
|
408
392
|
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
393
|
+
* The built-in marketplace is only registered when `shipwrightRepoRoot` has a
|
|
394
|
+
* `.claude-plugin/marketplace.json` (the platform image ships none).
|
|
411
395
|
*
|
|
412
396
|
* Marketplace registration order:
|
|
413
397
|
* 1. Extra baked marketplaces (discovered from `extraMarketplacesRoot` or
|
|
@@ -463,24 +447,22 @@ export async function installPlugins(
|
|
|
463
447
|
|
|
464
448
|
// Register the local (built-in) shipwright marketplace so `claude plugin install`
|
|
465
449
|
// can resolve plugins by name. Idempotent — safe to call on every startup.
|
|
466
|
-
|
|
467
|
-
"claude",
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
`[agent] claude plugin marketplace add exited ${add.exitCode}: ${add.stdout}`,
|
|
450
|
+
if (
|
|
451
|
+
existsSync(join(shipwrightRepoRoot, ".claude-plugin", "marketplace.json"))
|
|
452
|
+
) {
|
|
453
|
+
const add = await execFn(
|
|
454
|
+
"claude",
|
|
455
|
+
["plugin", "marketplace", "add", shipwrightRepoRoot],
|
|
456
|
+
{ cwd },
|
|
474
457
|
);
|
|
458
|
+
if (add.exitCode !== 0) {
|
|
459
|
+
console.warn(
|
|
460
|
+
`[agent] claude plugin marketplace add exited ${add.exitCode}: ${add.stdout}`,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
475
463
|
}
|
|
476
464
|
|
|
477
|
-
const
|
|
478
|
-
(name) => `${name}@${SHIPWRIGHT_MARKETPLACE}`,
|
|
479
|
-
);
|
|
480
|
-
const agentPluginSpecs = agentPlugins.map(
|
|
481
|
-
(p) => `${p.plugin}@${p.marketplace}`,
|
|
482
|
-
);
|
|
483
|
-
const allSpecs = [...defaultPluginSpecs, ...agentPluginSpecs];
|
|
465
|
+
const allSpecs = agentPlugins.map((p) => `${p.plugin}@${p.marketplace}`);
|
|
484
466
|
|
|
485
467
|
// Uninstall any plugin whose recorded installPath no longer exists. This
|
|
486
468
|
// clears stale manifest entries left over from a different-HOME container
|
|
@@ -502,7 +484,7 @@ export async function installPlugins(
|
|
|
502
484
|
}
|
|
503
485
|
}
|
|
504
486
|
|
|
505
|
-
// Install all plugins
|
|
487
|
+
// Install all plugins .
|
|
506
488
|
// Skip any stale spec whose uninstall failed — install is idempotent and
|
|
507
489
|
// would silently succeed without fixing the stale path, hiding the breakage.
|
|
508
490
|
for (const spec of allSpecs) {
|
|
@@ -517,7 +499,7 @@ export async function installPlugins(
|
|
|
517
499
|
}
|
|
518
500
|
}
|
|
519
501
|
|
|
520
|
-
// Update all plugins
|
|
502
|
+
// Update all plugins .
|
|
521
503
|
// Skip specs whose uninstall failed for the same reason as the install loop.
|
|
522
504
|
for (const spec of allSpecs) {
|
|
523
505
|
if (failedUninstalls.has(spec)) continue;
|
package/setup.unit.test.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Pure logic tests: no I/O, no real binaries, injected exec doubles only.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { describe, expect, it } from "bun:test";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
8
8
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
@@ -22,56 +22,64 @@ import {
|
|
|
22
22
|
// these tests depend on ambient sandbox/image state.
|
|
23
23
|
const NO_EXTRA_MARKETPLACES: string[] = [];
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
const SHIPWRIGHT_ROW = { plugin: "shipwright", marketplace: "shipwright" };
|
|
26
|
+
|
|
27
|
+
// A repo root that carries a local marketplace (a coding image); the platform
|
|
28
|
+
// image has none.
|
|
29
|
+
const makeMarketplaceRoot = (): string => {
|
|
30
|
+
const root = mkdtempSync(join(tmpdir(), "mkt-root-"));
|
|
31
|
+
mkdirSync(join(root, ".claude-plugin"), { recursive: true });
|
|
32
|
+
writeFileSync(join(root, ".claude-plugin", "marketplace.json"), "{}");
|
|
33
|
+
return root;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const recordCalls = () => {
|
|
37
|
+
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
38
|
+
const exec = async (cmd: string, args: string[], _opts: { cwd: string }) => {
|
|
39
|
+
calls.push({ cmd, args });
|
|
40
|
+
return { stdout: "", exitCode: 0 };
|
|
41
|
+
};
|
|
42
|
+
return { calls, exec };
|
|
43
|
+
};
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
describe("installPlugins — local marketplace", () => {
|
|
46
|
+
let root: string;
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
root = makeMarketplaceRoot();
|
|
49
|
+
});
|
|
50
|
+
afterEach(() => rmSync(root, { recursive: true, force: true }));
|
|
51
|
+
|
|
52
|
+
const run = (
|
|
53
|
+
exec: ReturnType<typeof recordCalls>["exec"],
|
|
54
|
+
plugins: Array<{ plugin: string; marketplace: string }>,
|
|
55
|
+
repoRoot = root,
|
|
56
|
+
extra: string[] = NO_EXTRA_MARKETPLACES,
|
|
57
|
+
) =>
|
|
58
|
+
installPlugins(
|
|
59
|
+
exec,
|
|
39
60
|
"/tmp/cwd",
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
plugins,
|
|
62
|
+
repoRoot,
|
|
42
63
|
"/tmp/nonexistent-manifest.json",
|
|
43
|
-
|
|
64
|
+
extra,
|
|
44
65
|
);
|
|
45
66
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"add",
|
|
51
|
-
"/repo/root",
|
|
52
|
-
]);
|
|
67
|
+
it("registers the marketplace before installing plugins", async () => {
|
|
68
|
+
const { calls, exec } = recordCalls();
|
|
69
|
+
await run(exec, [SHIPWRIGHT_ROW]);
|
|
70
|
+
expect(calls[0].args).toEqual(["plugin", "marketplace", "add", root]);
|
|
53
71
|
});
|
|
54
72
|
|
|
55
|
-
it("installs
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
) => {
|
|
62
|
-
calls.push({ cmd, args });
|
|
63
|
-
return { stdout: "", exitCode: 0 };
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
await installPlugins(
|
|
67
|
-
mockExec,
|
|
68
|
-
"/tmp/cwd",
|
|
69
|
-
[],
|
|
70
|
-
"/repo/root",
|
|
71
|
-
"/tmp/nonexistent-manifest.json",
|
|
72
|
-
NO_EXTRA_MARKETPLACES,
|
|
73
|
-
);
|
|
73
|
+
it("installs nothing when the agent has no plugin rows", async () => {
|
|
74
|
+
const { calls, exec } = recordCalls();
|
|
75
|
+
await run(exec, []);
|
|
76
|
+
expect(calls.filter((c) => c.args[1] === "install")).toEqual([]);
|
|
77
|
+
expect(calls.filter((c) => c.args[1] === "update")).toEqual([]);
|
|
78
|
+
});
|
|
74
79
|
|
|
80
|
+
it("installs and updates shipwright@shipwright for a coding agent's shipwright row", async () => {
|
|
81
|
+
const { calls, exec } = recordCalls();
|
|
82
|
+
await run(exec, [SHIPWRIGHT_ROW]);
|
|
75
83
|
// marketplace add + install + update = 3 calls
|
|
76
84
|
expect(calls).toHaveLength(3);
|
|
77
85
|
expect(calls[1].args).toEqual([
|
|
@@ -87,29 +95,14 @@ describe("installPlugins — local marketplace", () => {
|
|
|
87
95
|
});
|
|
88
96
|
|
|
89
97
|
it("agent plugins use their own marketplace, not the local one", async () => {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
) => {
|
|
96
|
-
calls.push({ cmd, args });
|
|
97
|
-
return { stdout: "", exitCode: 0 };
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
await installPlugins(
|
|
101
|
-
mockExec,
|
|
102
|
-
"/tmp/cwd",
|
|
103
|
-
[{ plugin: "my-plugin", marketplace: "org/my-marketplace" }],
|
|
104
|
-
"/repo/root",
|
|
105
|
-
"/tmp/nonexistent-manifest.json",
|
|
106
|
-
NO_EXTRA_MARKETPLACES,
|
|
107
|
-
);
|
|
108
|
-
|
|
98
|
+
const { calls, exec } = recordCalls();
|
|
99
|
+
await run(exec, [
|
|
100
|
+
SHIPWRIGHT_ROW,
|
|
101
|
+
{ plugin: "my-plugin", marketplace: "org/my-marketplace" },
|
|
102
|
+
]);
|
|
109
103
|
// marketplace add + 2 installs + 2 updates = 5 calls
|
|
110
104
|
expect(calls).toHaveLength(5);
|
|
111
|
-
|
|
112
|
-
expect(specs).toEqual([
|
|
105
|
+
expect(calls.slice(1).map((c) => c.args[2])).toEqual([
|
|
113
106
|
"shipwright@shipwright",
|
|
114
107
|
"my-plugin@org/my-marketplace",
|
|
115
108
|
"shipwright@shipwright",
|
|
@@ -117,128 +110,30 @@ describe("installPlugins — local marketplace", () => {
|
|
|
117
110
|
]);
|
|
118
111
|
});
|
|
119
112
|
|
|
120
|
-
it("
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
args: string[],
|
|
125
|
-
_opts: { cwd: string },
|
|
126
|
-
) => {
|
|
127
|
-
calls.push({ cmd, args });
|
|
128
|
-
return { stdout: "", exitCode: 0 };
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
await installPlugins(
|
|
132
|
-
mockExec,
|
|
133
|
-
"/tmp/cwd",
|
|
134
|
-
[],
|
|
135
|
-
"/custom/repo/root",
|
|
136
|
-
"/tmp/nonexistent-manifest.json",
|
|
137
|
-
NO_EXTRA_MARKETPLACES,
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
expect(calls[0].args).toEqual([
|
|
141
|
-
"plugin",
|
|
142
|
-
"marketplace",
|
|
143
|
-
"add",
|
|
144
|
-
"/custom/repo/root",
|
|
145
|
-
]);
|
|
113
|
+
it("skips the local marketplace add when the repo root has no marketplace", async () => {
|
|
114
|
+
const { calls, exec } = recordCalls();
|
|
115
|
+
await run(exec, [], "/repo/root");
|
|
116
|
+
expect(calls).toEqual([]);
|
|
146
117
|
});
|
|
147
118
|
|
|
148
|
-
it("registers extra marketplaces before the
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
cmd: string,
|
|
152
|
-
args: string[],
|
|
153
|
-
_opts: { cwd: string },
|
|
154
|
-
) => {
|
|
155
|
-
calls.push({ cmd, args });
|
|
156
|
-
return { stdout: "", exitCode: 0 };
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// Pass a non-existent root so discoverBakedMarketplaces can't find dirs —
|
|
160
|
-
// we inject via extraMarketplaceDirs directly instead.
|
|
161
|
-
await installPlugins(
|
|
162
|
-
mockExec,
|
|
163
|
-
"/tmp/cwd",
|
|
164
|
-
[],
|
|
165
|
-
"/repo/root",
|
|
166
|
-
"/tmp/nonexistent-manifest.json",
|
|
167
|
-
["/opt/shipwright/marketplaces/my-marketplace"],
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
// Extra marketplace add must come BEFORE the /repo/root marketplace add
|
|
119
|
+
it("registers extra marketplaces before the local marketplace", async () => {
|
|
120
|
+
const { calls, exec } = recordCalls();
|
|
121
|
+
await run(exec, [], root, ["/opt/shipwright/marketplaces/my-marketplace"]);
|
|
171
122
|
const addCalls = calls.filter((c) => c.args[1] === "marketplace");
|
|
172
123
|
expect(addCalls[0].args[3]).toBe(
|
|
173
124
|
"/opt/shipwright/marketplaces/my-marketplace",
|
|
174
125
|
);
|
|
175
|
-
expect(addCalls[1].args[3]).toBe(
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("is a no-op when no extra marketplace dirs exist", async () => {
|
|
179
|
-
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
180
|
-
const mockExec = async (
|
|
181
|
-
cmd: string,
|
|
182
|
-
args: string[],
|
|
183
|
-
_opts: { cwd: string },
|
|
184
|
-
) => {
|
|
185
|
-
calls.push({ cmd, args });
|
|
186
|
-
return { stdout: "", exitCode: 0 };
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
await installPlugins(
|
|
190
|
-
mockExec,
|
|
191
|
-
"/tmp/cwd",
|
|
192
|
-
[],
|
|
193
|
-
"/repo/root",
|
|
194
|
-
"/tmp/nonexistent-manifest.json",
|
|
195
|
-
NO_EXTRA_MARKETPLACES,
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
// Only the /repo/root marketplace add — no extra adds
|
|
199
|
-
const addCalls = calls.filter((c) => c.args[1] === "marketplace");
|
|
200
|
-
expect(addCalls).toHaveLength(1);
|
|
201
|
-
expect(addCalls[0].args[3]).toBe("/repo/root");
|
|
126
|
+
expect(addCalls[1].args[3]).toBe(root);
|
|
202
127
|
});
|
|
203
128
|
|
|
204
129
|
it("never reads the real BAKED_MARKETPLACES_ROOT when extraMarketplaceDirs is injected (isolation regression)", async () => {
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// image/sandbox that bakes a marketplace there (this sandbox does), that
|
|
210
|
-
// extra ambient `marketplace add` call would silently appear before the
|
|
211
|
-
// expected calls and break exact-match assertions below.
|
|
212
|
-
//
|
|
213
|
-
// This test asserts the injected empty array wins deterministically,
|
|
214
|
-
// regardless of what (if anything) actually exists on disk at
|
|
215
|
-
// /opt/shipwright/marketplaces in the environment running the suite.
|
|
216
|
-
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
217
|
-
const mockExec = async (
|
|
218
|
-
cmd: string,
|
|
219
|
-
args: string[],
|
|
220
|
-
_opts: { cwd: string },
|
|
221
|
-
) => {
|
|
222
|
-
calls.push({ cmd, args });
|
|
223
|
-
return { stdout: "", exitCode: 0 };
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
await installPlugins(
|
|
227
|
-
mockExec,
|
|
228
|
-
"/tmp/cwd",
|
|
229
|
-
[],
|
|
230
|
-
"/repo/root",
|
|
231
|
-
"/tmp/nonexistent-manifest.json",
|
|
232
|
-
NO_EXTRA_MARKETPLACES,
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
// Exactly one marketplace add (the built-in shipwright one) — no extra
|
|
236
|
-
// marketplace add sneaks in from real fs discovery, no matter what's
|
|
237
|
-
// actually present on disk at BAKED_MARKETPLACES_ROOT in this environment.
|
|
130
|
+
// The injected empty array must win over real fs discovery of
|
|
131
|
+
// /opt/shipwright/marketplaces, whatever the sandbox has baked there.
|
|
132
|
+
const { calls, exec } = recordCalls();
|
|
133
|
+
await run(exec, [SHIPWRIGHT_ROW]);
|
|
238
134
|
const addCalls = calls.filter((c) => c.args[1] === "marketplace");
|
|
239
135
|
expect(addCalls).toHaveLength(1);
|
|
240
|
-
expect(addCalls[0].args[3]).toBe(
|
|
241
|
-
// Full call count is deterministic: marketplace add + install + update = 3.
|
|
136
|
+
expect(addCalls[0].args[3]).toBe(root);
|
|
242
137
|
expect(calls).toHaveLength(3);
|
|
243
138
|
});
|
|
244
139
|
});
|
|
@@ -267,7 +162,7 @@ describe("discoverBakedMarketplaces", () => {
|
|
|
267
162
|
});
|
|
268
163
|
|
|
269
164
|
describe("resolveTemplatesDir option resolution (AEP-1.2)", () => {
|
|
270
|
-
it("resolves the shipped specialist
|
|
165
|
+
it("resolves the shipped specialist type to agent-types/specialist/templates/ with no warning (SAI-1.2)", async () => {
|
|
271
166
|
const { resolveTemplatesDir } = await import("./setup.ts");
|
|
272
167
|
const repoRoot = join(import.meta.dir, "..");
|
|
273
168
|
const warnings: string[] = [];
|
|
@@ -339,10 +234,10 @@ describe("resolveTemplatesDir option resolution (AEP-1.2)", () => {
|
|
|
339
234
|
SHIPWRIGHT_SPECIALTIES_DIR: "/fixture/types",
|
|
340
235
|
},
|
|
341
236
|
);
|
|
342
|
-
// Missing fixture
|
|
237
|
+
// Missing fixture templates dir → warns naming the type, falls back built-in.
|
|
343
238
|
expect(warnings).toHaveLength(1);
|
|
344
239
|
expect(warnings[0]).toContain('"base"');
|
|
345
|
-
expect(warnings[0]).toContain("/fixture/types/base/
|
|
240
|
+
expect(warnings[0]).toContain("/fixture/types/base/templates");
|
|
346
241
|
expect(dir).toMatch(/workspace$/);
|
|
347
242
|
});
|
|
348
243
|
});
|
|
@@ -368,19 +263,24 @@ describe("SHIPWRIGHT_REPO_ROOT override (PCB-1.1)", () => {
|
|
|
368
263
|
"/tmp/nonexistent-baked-root",
|
|
369
264
|
env,
|
|
370
265
|
);
|
|
371
|
-
return calls[0][3];
|
|
266
|
+
return calls[0]?.[3];
|
|
372
267
|
};
|
|
373
268
|
|
|
374
269
|
it("installPlugins registers the marketplace from SHIPWRIGHT_REPO_ROOT when set", async () => {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
270
|
+
const custom = makeMarketplaceRoot();
|
|
271
|
+
try {
|
|
272
|
+
expect(await registerRoot({ SHIPWRIGHT_REPO_ROOT: custom })).toBe(custom);
|
|
273
|
+
} finally {
|
|
274
|
+
rmSync(custom, { recursive: true, force: true });
|
|
275
|
+
}
|
|
378
276
|
});
|
|
379
277
|
|
|
380
278
|
it("installPlugins keeps the computed default when unset or empty", async () => {
|
|
381
279
|
const unset = await registerRoot({});
|
|
382
|
-
|
|
383
|
-
expect(unset).
|
|
280
|
+
// Only registered when the computed default root carries a marketplace.
|
|
281
|
+
expect(unset === undefined || unset === resolveShipwrightRepoRoot({})).toBe(
|
|
282
|
+
true,
|
|
283
|
+
);
|
|
384
284
|
expect(await registerRoot({ SHIPWRIGHT_REPO_ROOT: "" })).toBe(unset);
|
|
385
285
|
});
|
|
386
286
|
|
|
@@ -388,22 +288,18 @@ describe("SHIPWRIGHT_REPO_ROOT override (PCB-1.1)", () => {
|
|
|
388
288
|
const makeRoot = () => {
|
|
389
289
|
const root = mkdtempSync(join(tmpdir(), "repo-root-"));
|
|
390
290
|
const dir = join(root, "types", "foo");
|
|
391
|
-
mkdirSync(join(dir, "
|
|
392
|
-
writeFileSync(
|
|
393
|
-
join(dir, "manifest.yaml"),
|
|
394
|
-
"identity:\n templatesDir: tpl\n",
|
|
395
|
-
);
|
|
291
|
+
mkdirSync(join(dir, "templates"), { recursive: true });
|
|
396
292
|
for (const f of [
|
|
397
293
|
"CLAUDE.md.template",
|
|
398
294
|
"SOUL.md.template",
|
|
399
295
|
"IDENTITY.md.template",
|
|
400
296
|
]) {
|
|
401
|
-
writeFileSync(join(dir, "
|
|
297
|
+
writeFileSync(join(dir, "templates", f), "x");
|
|
402
298
|
}
|
|
403
|
-
return { root, tpl: join(dir, "
|
|
299
|
+
return { root, tpl: join(dir, "templates") };
|
|
404
300
|
};
|
|
405
301
|
|
|
406
|
-
it("resolves
|
|
302
|
+
it("resolves a type's templates/ under SHIPWRIGHT_SPECIALTIES_DIR when SHIPWRIGHT_REPO_ROOT is set", () => {
|
|
407
303
|
const { root, tpl } = makeRoot();
|
|
408
304
|
try {
|
|
409
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
|
|
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
|